Exemplo de aplicativo Flutter Client Server



Neste tutorial, desenvolveremos um aplicativo que recebe dados pela Internet e os exibe em uma lista. Algo assim



Ok, vamos começar criando um projeto. Escreva o seguinte na linha de comando

flutter create flutter_infinite_list 

Em seguida, acesse nosso arquivo de dependências pubspec.yaml e adicione os que precisamos

 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 

Depois disso, instale essas dependências com o seguinte comando

 flutter packages get 

Para esta aplicação, usaremos jsonplaceholder para obter dados do mocha. Se você não estiver familiarizado com este serviço, este é um serviço de API REST online que pode enviar dados falsos. Isso é muito útil para criar protótipos de aplicativos.

Ao abrir o link a seguir jsonplaceholder.typicode.com/posts?_start=0&_limit=2, você verá a resposta JSON com a qual trabalharemos.

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

Observe que em nossa solicitação GET, especificamos a restrição de início e fim como um parâmetro.

Ótimo, agora sabemos como será a estrutura de nossos dados! Vamos criar um modelo para eles.

Crie um arquivo post.dart com o seguinte conteúdo

 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 é apenas uma classe com id, título e corpo. Também podemos substituir a função toString para exibir uma sequência conveniente mais tarde. Além disso, estendemos a classe Equatable para que possamos comparar os objetos Posts.

Agora, como temos um modelo de resposta do servidor, vamos implementar a lógica de negócios (Business Logic Component (bloco)).

Antes de mergulharmos no desenvolvimento de aplicativos, precisamos determinar o que nosso PostBloc fará.

No nível superior, ele será responsável por processar as ações do usuário (rolagem) e receber novas postagens quando a camada de apresentação solicitar. Vamos começar a implementar isso.

Nosso PostBloc responderá a apenas um evento. Recebendo dados que serão exibidos na tela conforme necessário. Vamos criar uma classe post_event.dart e implementar nosso evento.

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

Novamente, redefina toString para ler com mais facilidade a linha que exibe nosso evento. Também precisamos estender a classe Equatable para comparar objetos.

Em resumo, nosso PostBloc receberá PostEvents e os converterá em PostStates. Desenvolvemos todos os eventos PostEvents (Fetch), passaremos para o PostState.

Nossa camada de apresentação deve ter vários estados para exibir corretamente.

isInitializing - informa à camada de apresentação que é necessário exibir um indicador de carregamento enquanto os dados estão sendo carregados.

posts - exibe uma lista de objetos Post

isError - informa à camada que ocorreram erros ao carregar dados

hasReachedMax - indicação do último registro disponível

Crie uma classe post_state.dart com o seguinte conteúdo

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

Usamos o padrão de fábrica por conveniência e legibilidade. Em vez de criar manualmente entidades PostState, podemos usar várias fábricas, como PostState.initial ()

Agora que temos eventos e condições, é hora de criar nosso PostBloc
Para simplificar, nosso PostBloc terá uma dependência direta do cliente http; no entanto, na produção, você deve envolvê-lo em uma dependência externa no cliente api e usar o padrão Repository.

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

Observe que somente a partir da declaração de nossa classe podemos dizer que ele aceitará PostEvents e fornecerá PostStates

Vamos seguir para o desenvolvimento 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; } } 

Em seguida, você precisa implementar o mapEventToState, que será acionado toda vez que um evento for enviado.

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

Agora, toda vez que um PostEvent for enviado, se este for um evento de amostragem e não chegarmos ao final da lista, as próximas 20 entradas serão exibidas.

Vamos modificar um pouco o nosso 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'); } } } 

Ótimo, concluímos a implementação da lógica de negócios!

Crie a classe main.dart e implemente runApp nela para desenhar nossa interface do usuário

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

Em seguida, crie uma HomePage que exiba nossas postagens e se conecte ao 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()); } } } 

Em seguida, implementamos o BottomLoader, que mostrará ao usuário o carregamento de novas postagens.

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

Por fim, implementamos um PostWidget que desenha um único objeto do 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, ); } } 

Isso é tudo, agora você pode executar o aplicativo e ver o resultado.

As fontes do projeto podem ser baixadas no Github

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


All Articles