Arquitetura de aplicativo Flutter 101: Baunilha, modelo com escopo, BLoC


(publicado originalmente no Medium )


O Flutter fornece uma estrutura moderna de estilo reativo, coleção de ferramentas e ferramentas avançadas, mas não há nada semelhante ao guia do Android para a arquitetura de aplicativos .


De fato, não existe uma arquitetura definitiva que atenda a todos os requisitos possíveis, mas vamos encarar o fato de que a maioria dos aplicativos móveis em que estamos trabalhando possui pelo menos algumas das seguintes funcionalidades:


  1. Solicitar / fazer upload de dados de / para a rede.
  2. Mapeie, transforme, prepare dados e apresente-os ao usuário.
  3. Coloque / obtenha dados de / para o banco de dados.

Levando isso em consideração, criei um aplicativo de amostra que está resolvendo exatamente o mesmo problema usando três abordagens diferentes para a arquitetura.


O usuário recebe um botão "Carregar dados do usuário" no centro da tela. Quando o usuário clica no botão, o carregamento de dados assíncronos é acionado e o botão é substituído por um indicador de carregamento. Após o carregamento dos dados, o indicador de carregamento é substituído pelos dados.


Vamos começar.



Dados


Para fins de simplicidade, criei a classe Repository que contém o método getUser() que emula uma chamada de rede assíncrona e retorna o objeto Future<User> com valores codificados.


Se você não estiver familiarizado com futuros e programação assíncrona no Dart, poderá aprender mais sobre isso, seguindo este tutorial e lendo um documento .


 class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } } 

 class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; } 

Baunilha


Vamos criar o aplicativo da maneira que a maioria dos desenvolvedores faria depois de ler a documentação oficial do Flutter.


Navegando para a tela VanillaScreen usando o Navigator


 Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), ); 

Como o estado do widget pode mudar várias vezes durante a vida útil do widget, devemos estender o StatefulWidget . A implementação de um widget com estado também requer uma classe State . Os campos bool _isLoading e User _user na classe _VanillaScreenState representam o estado do widget. Ambos os campos são inicializados antes que o método build(BuildContext context) seja chamado.


 class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

Quando o objeto de estado do widget é criado, o método build(BuildContext context) é chamado para criar a interface do usuário. Todas as decisões sobre os widgets que devem ser construídos para representar o estado atual são tomadas no código de declaração da UI.


 body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ) 

Para exibir o indicador de progresso quando o usuário clicar no botão "Carregar detalhes do usuário", faremos o seguinte.


 setState(() { _isLoading = true; }); 

A chamada de setState () notifica a estrutura de que o estado interno deste objeto foi alterado de uma maneira que pode impactar a interface do usuário nessa subárvore, o que faz com que a estrutura agende uma construção para esse objeto de Estado.

Isso significa que, depois de chamar o método setState() método build(BuildContext context) é chamado pela estrutura novamente e toda a árvore do widget é reconstruída . Como _isLoading agora está definido como true método _buildLoading() é chamado em vez de _buildBody() e o indicador de carregamento é exibido na tela. Exatamente o mesmo acontece quando manipulamos o retorno de chamada de getUser() e chamamos setState() para reatribuir os campos _user e _user .


 widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); 

Prós


  1. Fácil de aprender e entender.
  2. Nenhuma biblioteca de terceiros é necessária.

Contras


  1. Toda a árvore do widget é reconstruída sempre que o estado do widget é alterado.
  2. Está quebrando o princípio da responsabilidade única. O Widget não é apenas responsável pela criação da interface do usuário, mas também pelo carregamento de dados, lógica de negócios e gerenciamento de estado.
  3. As decisões sobre como o estado atual deve ser representado são tomadas no código de declaração da interface do usuário. Se tivéssemos um código de estado um pouco mais complexo, a legibilidade diminuiria.

Modelo com escopo


Modelo com escopo definido é um pacote de terceiros que não está incluído na estrutura do Flutter. É assim que os desenvolvedores do Scoped Model o descrevem:


Um conjunto de utilitários que permitem passar facilmente um Modelo de dados de um Widget pai para seus descendentes. Além disso, ele também reconstrói todos os filhos que usam o modelo quando o modelo é atualizado. Esta biblioteca foi originalmente extraída da base de código fúcsia.

Vamos construir a mesma tela usando o modelo com escopo. Primeiro, precisamos instalar o pacote scoped_model Model adicionando a dependência pubspec.yaml ao pubspec.yaml na seção dependencies .


 scoped_model: ^1.0.1 

Vamos dar uma olhada no widget UserModelScreen e compará-lo com o exemplo anterior que foi criado sem o uso do modelo com escopo. Vamos dar uma olhada no widget UserModelScreen e compará-lo com o exemplo anterior que foi criado sem o uso do modelo com escopo. Como queremos disponibilizar nosso modelo para todos os descendentes do widget, devemos envolvê-lo com o ScopedModel genérico e fornecer um widget e um modelo.


 class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } } 

No exemplo anterior, toda a árvore do widget foi reconstruída quando o estado do widget foi alterado. Mas nós realmente precisamos reconstruir a tela inteira? Por exemplo, o AppBar não deve mudar, portanto, não há sentido em reconstruí-lo. Idealmente, devemos reconstruir apenas os widgets atualizados. O modelo com escopo pode nos ajudar a resolver isso.


ScopedModelDescendant<UserModel> é usado para localizar UserModel na árvore de Widgets. Ele será reconstruído automaticamente sempre que o UserModel notificar que a alteração ocorreu.


Outra melhoria é que o UserModelScreen não é mais responsável pelo gerenciamento de estado e pela lógica de negócios.


Vamos dar uma olhada no código do UserModel .


 class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); } 

Agora, o UserModel mantém e gerencia o estado. Para notificar os ouvintes (e reconstruir os descendentes) que a mudança ocorreu, o método notifyListeners() deve ser chamado.


Prós


  1. Lógica de negócios, gerenciamento de estado e separação de código da interface do usuário.
  2. Fácil de aprender.

    Contras

  3. Requer biblioteca de terceiros.
  4. À medida que o modelo se torna cada vez mais complexo, é difícil acompanhar quando você deve chamar notifyListeners() .

BLoC


BLoC (componentes de lógica de negócios C ) é um padrão recomendado pelos desenvolvedores do Google. Ele aproveita a funcionalidade de fluxos para gerenciar e propagar alterações de estado.


Para desenvolvedores do Android: você pode pensar no objeto Bloc como um ViewModel e no StreamController como um LiveData . Isso tornará o código a seguir muito simples, pois você já está familiarizado com os conceitos.


 class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; } 

Nenhuma chamada de método adicional é necessária para notificar os assinantes quando o estado mudar.


Eu criei 3 classes para representar possíveis estados da tela:


UserInitState para o estado, quando o usuário abre uma tela com um botão no centro.


UserLoadingState para o estado, quando o indicador de carregamento é exibido enquanto os dados estão sendo carregados.


UserDataState para o estado, quando os dados são carregados e exibidos na tela.


Propagar mudanças de estado dessa maneira nos permite livrar-se de toda a lógica no código de declaração da interface do usuário. No exemplo do modelo com escopo, ainda estávamos verificando se _isLoading é true no código de declaração da interface do usuário para decidir qual widget devemos renderizar. No caso do BLoC, estamos propagando o estado da tela e a única responsabilidade do widget UserBlocScreen é renderizar a interface do usuário para esse estado.


 class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } } 

UserBlocScreen código UserBlocScreen ficou ainda mais simples em comparação com os exemplos anteriores. Para ouvir as mudanças de estado, estamos usando o StreamBuilder . StreamBuilder é um StatefulWidget que se constrói com base no instantâneo mais recente da interação com um Stream .


Prós


Não são necessárias bibliotecas de terceiros.
Lógica de negócios, gerenciamento de estado e separação da lógica da interface do usuário.
É reativo. Nenhuma chamada adicional é necessária, como no caso dos notifyListeners() do notifyListeners() Model notifyListeners() .


Contras


É necessária experiência no trabalho com fluxos ou rxdart.



Você pode conferir o código-fonte dos exemplos acima deste repositório do github.


Originalmente, o artigo é publicado no Medium

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


All Articles