Exemple d'application Flutter Client Server



Dans ce tutoriel, nous allons développer une application qui reçoit des données via Internet et les affiche dans une liste. Quelque chose comme ça



Ok, commençons par créer un projet. Écrivez ce qui suit sur la ligne de commande

flutter create flutter_infinite_list 

Ensuite, allez dans notre fichier de dépendances pubspec.yaml et ajoutez ceux dont nous avons besoin

 name: flutter_infinite_list description: A new Flutter project. version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_bloc: 0.4.11 http: 0.12.0 equatable: 0.1.1 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true 

Après cela, installez ces dépendances avec la commande suivante

 flutter packages get 

Pour cette application, nous utiliserons jsonplaceholder pour obtenir des données mocha . Si vous n'êtes pas familier avec ce service, il s'agit d'un service d'API REST en ligne qui peut envoyer de fausses données. Ceci est très utile pour construire des prototypes d'application.

En ouvrant le lien suivant jsonplaceholder.typicode.com/posts?_start=0&_limit=2, vous verrez la réponse JSON avec laquelle nous travaillerons.

 [ { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }, { "userId": 1, "id": 2, "title": "qui est esse", "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" } ] 

Notez que dans notre demande GET, nous avons spécifié la contrainte de début et de fin en tant que paramètre.

Génial, nous savons maintenant à quoi ressemblera la structure de nos données! Créons un modèle pour eux.

Créez un fichier post.dart avec le contenu suivant

 import 'package:equatable/equatable.dart'; class Post extends Equatable { final int id; final String title; final String body; Post({this.id, this.title, this.body}) : super([id, title, body]); @override String toString() => 'Post { id: $id }'; } 

Post est juste une classe avec id, titre et corps. Nous pouvons également remplacer la fonction toString pour afficher une chaîne pratique ultérieurement. De plus, nous étendons la classe Equatable afin de pouvoir comparer les objets Posts.

Nous avons maintenant un modèle de réponse du serveur, implémentons la logique métier (Business Logic Component (bloc)).

Avant de nous lancer dans le développement d'applications, nous devons déterminer ce que fera notre PostBloc.

Au niveau supérieur, il sera responsable du traitement des actions des utilisateurs (défilement) et de la réception de nouveaux messages lorsque la couche de présentation les demandera. Commençons à mettre en œuvre cela.

Notre PostBloc ne répondra qu'à un seul événement. Réception de données qui seront affichées à l'écran selon les besoins. Créons une classe post_event.dart et implémentons notre événement.

 import 'package:equatable/equatable.dart'; abstract class PostEvent extends Equatable {} class Fetch extends PostEvent { @override String toString() => 'Fetch'; } 

Encore une fois, redéfinissez toString pour lire plus facilement la ligne qui affiche notre événement. Nous devons également étendre la classe Equatable pour comparer des objets.

En résumé, notre PostBloc recevra PostEvents et les convertira en PostStates. Nous avons développé tous les événements PostEvents (Fetch), nous allons passer à PostState.

Notre couche de présentation doit avoir plusieurs états pour s'afficher correctement.

isInitializing - informe la couche de présentation qu'il est nécessaire d'afficher un indicateur de chargement pendant le chargement des données.

posts - affiche une liste des objets Post

isError - informe la couche que des erreurs se sont produites lors du chargement des données

hasReachedMax - indication du dernier enregistrement disponible

Créez une classe post_state.dart avec le contenu suivant

 import 'package:equatable/equatable.dart'; import 'package:flutter_infinite_list/models/models.dart'; abstract class PostState extends Equatable { PostState([Iterable props]) : super(props); } class PostUninitialized extends PostState { @override String toString() => 'PostUninitialized'; } class PostInitialized extends PostState { final List<Post> posts; final bool hasError; final bool hasReachedMax; PostInitialized({ this.hasError, this.posts, this.hasReachedMax, }) : super([posts, hasError, hasReachedMax]); factory PostInitialized.success(List<Post> posts) { return PostInitialized( posts: posts, hasError: false, hasReachedMax: false, ); } factory PostInitialized.failure() { return PostInitialized( posts: [], hasError: true, hasReachedMax: false, ); } PostInitialized copyWith({ List<Post> posts, bool hasError, bool hasReachedMax, }) { return PostInitialized( posts: posts ?? this.posts, hasError: hasError ?? this.hasError, hasReachedMax: hasReachedMax ?? this.hasReachedMax, ); } @override String toString() => 'PostInitialized { posts: ${posts.length}, hasError: $hasError, hasReachedMax: $hasReachedMax }'; } 

Nous avons utilisé le modèle Factory pour plus de commodité et de lisibilité. Au lieu de créer manuellement des entités PostState, nous pouvons utiliser diverses usines, telles que PostState.initial ()

Maintenant que nous avons des événements et des conditions, il est temps de créer notre PostBloc
Pour simplifier, notre PostBloc aura une dépendance directe du client http, mais en production, vous devez l'envelopper dans une dépendance externe dans le client api et utiliser le modèle de référentiel.

Créer post_bloc.dart

 import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override // TODO: implement initialState PostState get initialState => null; @override Stream<PostState> mapEventToState( PostState currentState, PostEvent event, ) async* { // TODO: implement mapEventToState yield null; } } 

Notez que ce n'est qu'à partir de la déclaration de notre classe que nous pouvons dire qu'elle acceptera PostEvents et donnera PostStates

Passons au développement de initialState.

 import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override PostState get initialState => PostState.initial(); @override Stream<PostState> mapEventToState( PostState currentState, PostEvent event, ) async* { // TODO: implement mapEventToState yield null; } } 

Ensuite, vous devez implémenter mapEventToState, qui se déclenchera chaque fois qu'un événement est envoyé.

 import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:bloc/bloc.dart'; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override get initialState => PostState.initial(); @override Stream<PostState> mapEventToState(currentState, event) async* { if (event is Fetch && !currentState.hasReachedMax) { try { final posts = await _fetchPosts(currentState.posts.length, 20); if (posts.isEmpty) { yield currentState.copyWith(hasReachedMax: true); } else { yield PostState.success(currentState.posts + posts); } } catch (_) { yield PostState.failure(); } } } Future<List<Post>> _fetchPosts(int startIndex, int limit) async { final response = await httpClient.get( 'https://jsonplaceholder.typicode.com/posts?_start=$startIndex&_limit=$limit'); if (response.statusCode == 200) { final data = json.decode(response.body) as List; return data.map((rawPost) { return Post( id: rawPost['id'], title: rawPost['title'], body: rawPost['body'], ); }).toList(); } else { throw Exception('error fetching posts'); } } } 

Désormais, chaque fois que PostEvent est distribué, s'il s'agit d'un événement d'échantillonnage et que nous n'avons pas atteint la fin de la liste, les 20 entrées suivantes seront affichées.

Modifions un peu notre PostBloc

 import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:http/http.dart' as http; import 'package:bloc/bloc.dart'; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override Stream<PostEvent> transform(Stream<PostEvent> events) { return (events as Observable<PostEvent>) .debounce(Duration(milliseconds: 500)); } @override get initialState => PostState.initial(); @override Stream<PostState> mapEventToState(currentState, event) async* { if (event is Fetch && !currentState.hasReachedMax) { try { final posts = await _fetchPosts(currentState.posts.length, 20); if (posts.isEmpty) { yield currentState.copyWith(hasReachedMax: true); } else { yield PostState.success(currentState.posts + posts); } } catch (_) { yield PostState.failure(); } } } Future<List<Post>> _fetchPosts(int startIndex, int limit) async { final response = await httpClient.get( 'https://jsonplaceholder.typicode.com/posts?_start=$startIndex&_limit=$limit'); if (response.statusCode == 200) { final data = json.decode(response.body) as List; return data.map((rawPost) { return Post( id: rawPost['id'], title: rawPost['title'], body: rawPost['body'], ); }).toList(); } else { throw Exception('error fetching posts'); } } } 

Très bien, nous avons terminé la mise en œuvre de la logique métier!

Créez la classe main.dart et implémentez runApp dedans pour dessiner notre interface utilisateur

 import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Infinite Scroll', home: Scaffold( appBar: AppBar( title: Text('Posts'), ), body: HomePage(), ), ); } } 

Ensuite, créez une page d'accueil qui affiche nos publications et se connecte à PostBloc

 class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _scrollController = ScrollController(); final PostBloc _postBloc = PostBloc(httpClient: http.Client()); final _scrollThreshold = 200.0; _HomePageState() { _scrollController.addListener(_onScroll); _postBloc.dispatch(Fetch()); } @override Widget build(BuildContext context) { return BlocBuilder( bloc: _postBloc, builder: (BuildContext context, PostState state) { if (state.isInitializing) { return Center( child: CircularProgressIndicator(), ); } if (state.isError) { return Center( child: Text('failed to fetch posts'), ); } if (state.posts.isEmpty) { return Center( child: Text('no posts'), ); } return ListView.builder( itemBuilder: (BuildContext context, int index) { return index >= state.posts.length ? BottomLoader() : PostWidget(post: state.posts[index]); }, itemCount: state.hasReachedMax ? state.posts.length : state.posts.length + 1, controller: _scrollController, ); }, ); } @override void dispose() { _postBloc.dispose(); super.dispose(); } void _onScroll() { final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; if (maxScroll - currentScroll <= _scrollThreshold) { _postBloc.dispatch(Fetch()); } } } 

Ensuite, nous implémentons BottomLoader, qui montrera à l'utilisateur le chargement de nouveaux messages.

 class BottomLoader extends StatelessWidget { @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, child: Center( child: SizedBox( width: 33, height: 33, child: CircularProgressIndicator( strokeWidth: 1.5, ), ), ), ); } } 

Enfin, nous implémentons un PostWidget qui dessinera un seul objet de type Post

 class PostWidget extends StatelessWidget { final Post post; const PostWidget({Key key, @required this.post}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( leading: Text( post.id.toString(), style: TextStyle(fontSize: 10.0), ), title: Text('${post.title}'), isThreeLine: true, subtitle: Text(post.body), dense: true, ); } } 

C'est tout, vous pouvez maintenant exécuter l'application et voir le résultat.

Les sources du projet peuvent être téléchargées sur Github

Source: https://habr.com/ru/post/fr435688/


All Articles