Ejemplo de aplicación del servidor cliente Flutter



En este tutorial, vamos a desarrollar una aplicación que recibe datos a través de Internet y los muestra en una lista. Algo como esto



Ok, comencemos creando un proyecto. Escriba lo siguiente en la línea de comando

flutter create flutter_infinite_list 

A continuación, vaya a nuestro archivo de dependencias pubspec.yaml y agregue los que necesitamos

 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 

Después de eso, instale estas dependencias con el siguiente comando

 flutter packages get 

Para esta aplicación, usaremos jsonplaceholder para obtener datos de mocha. Si no está familiarizado con este servicio, este es un servicio REST API en línea que puede enviar datos falsos. Esto es muy útil para construir prototipos de aplicaciones.

Al abrir el siguiente enlace jsonplaceholder.typicode.com/posts?_start=0&_limit=2 , verá la respuesta JSON con la que trabajaremos.

 [ { "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" } ] 

Tenga en cuenta que en nuestra solicitud GET especificamos la restricción de inicio y fin como un parámetro.

¡Genial, ahora sabemos cómo se verá la estructura de nuestros datos! Creemos un modelo para ellos.

Cree un archivo post.dart con los siguientes contenidos

 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 }'; } 

La publicación es solo una clase con id, título y cuerpo. También podemos anular la función toString para mostrar una cadena conveniente más adelante. Además, ampliamos la clase Equatable para que podamos comparar objetos Posts.

Ahora que tenemos un modelo de respuesta del servidor, implementemos la lógica de negocios (Componente de lógica de negocios (bloque)).

Antes de sumergirnos en el desarrollo de aplicaciones, debemos determinar qué hará nuestro PostBloc.

En el nivel superior, será responsable de procesar las acciones del usuario (desplazamiento) y de recibir nuevas publicaciones cuando la capa de presentación las solicite. Comencemos a implementar esto.

Nuestro PostBloc responderá a un solo evento. Recibir datos que se mostrarán en la pantalla según sea necesario. Creemos una clase post_event.dart e implementemos nuestro evento.

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

Nuevamente, redefina toString para leer más fácilmente la línea que muestra nuestro evento. También necesitamos extender la clase Equatable para comparar objetos.

En resumen, nuestro PostBloc recibirá PostEvents y los convertirá en PostStates. Hemos desarrollado todos los eventos de PostEvents (Fetch), pasaremos a PostState.

Nuestra capa de presentación debe tener varios estados para mostrarse correctamente.

isInitializing : informa a la capa de presentación que es necesario mostrar un indicador de carga mientras se cargan los datos.

publicaciones : muestra una lista de objetos Publicar

isError : informa a la capa que se produjeron errores al cargar datos

hasReachedMax : indicación del último registro disponible

Cree una clase post_state.dart con el siguiente contenido

 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 }'; } 

Utilizamos el patrón Factory para mayor comodidad y legibilidad. En lugar de crear manualmente entidades PostState, podemos usar varias fábricas, como PostState.initial ()

Ahora que tenemos eventos y condiciones, es hora de crear nuestro PostBloc
Para simplificar, nuestro PostBloc tendrá una dependencia directa del cliente http, sin embargo, en producción, debe envolverlo en una dependencia externa en el cliente api y usar el patrón Repository.

Crear 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; } } 

Tenga en cuenta que solo desde la declaración de nuestra clase podemos decir que aceptará PostEvents y dará PostStates

Pasemos a desarrollar 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; } } 

A continuación, debe implementar mapEventToState, que se activará cada vez que se envíe un evento.

 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'); } } } 

Ahora, cada vez que se envía PostEvent, si se trata de un evento de muestreo y no hemos llegado al final de la lista, se mostrarán las siguientes 20 entradas.

Modifiquemos un poco nuestro 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'); } } } 

¡Genial, hemos completado la implementación de la lógica de negocios!

Cree la clase main.dart e implemente runApp para dibujar nuestra interfaz de usuario

 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(), ), ); } } 

Luego, cree una página de inicio que muestre nuestras publicaciones y se conecte a 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()); } } } 

A continuación, implementamos BottomLoader, que mostrará al usuario la carga de nuevas publicaciones.

 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, ), ), ), ); } } 

Finalmente, implementamos un PostWidget que dibujará un solo objeto de tipo 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, ); } } 

Eso es todo, ahora puede ejecutar la aplicación y ver el resultado.

Las fuentes del proyecto se pueden descargar en Github

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


All Articles