
(artigo original publicado em inglês no Medium )
O Flutter fornece uma estrutura responsiva moderna, um grande conjunto de widgets e ferramentas. Mas, infelizmente, a documentação não é nada como um guia para a arquitetura recomendada do aplicativo Android .
Não existe uma arquitetura universal ideal que possa atender a qualquer requisito concebível de uma tarefa técnica, mas vamos admitir que a maioria dos aplicativos móveis em que estamos trabalhando possui as seguintes funcionalidades:
- Solicitar e baixar dados.
- Transformação e preparação de dados para o usuário.
- Escrevendo e lendo dados de um banco de dados ou sistema de arquivos.
Por tudo isso, criei um aplicativo de demonstração que resolve o mesmo problema usando abordagens diferentes da arquitetura.
Inicialmente, o usuário recebe uma tela com o botão "Carregar dados do usuário" localizado no centro. Quando o usuário clica no botão, ocorre o carregamento assíncrono de dados e o botão é substituído por um indicador de carregamento. Quando o download dos dados é concluído, o indicador de download é substituído por dados.
Então, vamos começar.

Dados
Para simplificar a tarefa, criei a classe Repository
, que contém o método getUser()
. Este método simula o carregamento assíncrono de dados da rede e retorna Future<User>
.
Se você não estiver familiarizado com futuros e programação assíncrona no Dart, podemos ler mais sobre isso aqui e ler a documentação da classe Future .
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 desenvolver o aplicativo, como um desenvolvedor faria se ele lesse a documentação do Flutter no site oficial.
Abra a tela VanillaScreen
usando o Navigator
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
Como o estado de um widget pode mudar várias vezes durante seu ciclo de vida, precisamos herdar do StatefulWidget
. Para implementar seu widget com estado, você também precisa da classe State
. Os 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 pela primeira vez.
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(), ); } }
Após a criação do objeto de estado do widget, o método build(BuildContext context)
é chamado para construir a interface do usuário. Todas as decisões sobre qual widget deve ser mostrado na tela são tomadas corretamente no código de declaração da interface do usuário.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
Para exibir um indicador de progresso, quando o usuário clica no botão "Carregar detalhes do usuário", fazemos o seguinte.
setState(() { _isLoading = true; });
Da documentação (tradução):
Uma chamada para o método setState () notifica a estrutura que o estado interno deste objeto foi alterado e pode afetar a interface do usuário na subárvore. Essa é a razão pela qual a estrutura chama o método de construção nesse objeto de estado.
Isso significa que, depois de chamar o método setState()
, a estrutura chamará o método build(BuildContext context)
novamente, o que recriará toda a árvore do widget . Como o valor do campo _isLoading
mudou para true
, em vez do método _buildBody()
, o método _buildLoading()
será chamado e um indicador de progresso será exibido na tela.
Exatamente o mesmo acontecerá quando getUser()
um retorno de chamada de getUser()
e chamarmos o método
setState()
para atribuir novos valores aos campos _user
e _user
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
Prós
- Baixo limiar de entrada.
- Não são necessárias bibliotecas de terceiros.
Contras
- Quando o estado do widget muda, a árvore do widget é completamente recriada a cada vez.
- Viola o princípio da responsabilidade exclusiva. O widget é responsável não apenas pela criação da interface do usuário, mas também pelo carregamento de dados, lógica de negócios e gerenciamento de estado.
- As decisões sobre como exibir o estado atual são tomadas diretamente no código da interface do usuário. Se o estado se tornar mais complexo, a legibilidade do código diminuirá bastante.
Modelo com escopo
O modelo com escopo é uma biblioteca de terceiros . Veja como os desenvolvedores o descrevem:
Um conjunto de utilitários que permitem transferir o Modelo de Dados do widget ancestral para todos os seus descendentes. Além disso, quando os dados do modelo mudarem, todos os descendentes que usarem o modelo serão recriados. Esta biblioteca foi originalmente retirada do código do projeto Fuchsia .
Vamos criar a mesma tela do exemplo anterior, mas usando o modelo com escopo. Primeiro, precisamos adicionar a biblioteca Modelo de Escopo ao projeto. Inclua a dependência scoped_model
no arquivo scoped_model
na seção dependencies
.
scoped_model: ^1.0.1
Vamos examinar o código UserModelScreen
e compará-lo com o exemplo anterior, no qual não usamos o modelo de escopo. Para tornar nosso modelo acessível aos descendentes do widget, precisamos ScopedModel
o widget e o modelo no ScopedModel
.
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, sempre que um widget muda de estado, a árvore de widgets era completamente recriada. Mas realmente precisamos recriar toda a árvore de widgets (tela cheia)? Por exemplo, o AppBar não muda nada e não faz sentido recriá-lo. Idealmente, você deve recriar apenas os widgets que devem mudar de acordo com a alteração de estado. E o modelo com escopo pode nos ajudar a resolver esse problema.
O ScopedModelDescendant<UserModel>
é usado para localizar o UserModel
na árvore de widgets. Ele será recriado automaticamente sempre que o UserModel
notificar que houve uma alteração.
Outra melhoria é que o UserModelScreen
não UserModelScreen
mais responsável pelo gerenciamento de estado, lógica de negócios e carregamento de dados.
Vamos dar uma olhada no código da classe 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); }
UserModel
agora contém e gerencia o estado. Para notificar os ouvintes (e recriar descendentes) que uma alteração ocorreu, você deve chamar o método notifyListeners()
.
Prós
- Gerenciamento de estado, lógica comercial e carregamento de dados são separados do código da interface do usuário.
- Baixo limiar de entrada.
Contras
- Dependência de biblioteca de terceiros.
- Se o modelo se tornar complexo o suficiente, será difícil acompanhar quando é realmente necessário chamar o método
notifyListeners()
para evitar recriações desnecessárias.
BLoC
BLoC (componentes de lógica de negócios C ) é um padrão recomendado pelos desenvolvedores do Google. Os fluxos são usados para gerenciar o estado e notificar uma alteração de estado.
Para desenvolvedores do Android: você pode imaginar que o Bloc
é um ViewModel
e o StreamController
é um LiveData
. Isso facilitará a compreensão do código a seguir, pois você já conhece os princípios básicos.
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; }
O código mostra que não há mais necessidade de chamar métodos adicionais para notificar sobre alterações de estado.
Criei 3 classes para representar possíveis estados:
UserInitState
para o estado em que o usuário abre uma tela com um botão no centro.
UserLoadingState
para o estado em que o indicador de carregamento é exibido enquanto os dados estão sendo carregados.
UserDataState
para o estado em que os dados já estão carregados e exibidos na tela.
Passar o estado dessa maneira nos permite livrar-nos completamente da lógica no código da interface do usuário. No exemplo de modelo com escopo, ainda verificamos se o valor do campo _isLoading
true
ou false
para determinar qual widget criar. No caso do BLoC, passamos o novo estado para o fluxo, e a única tarefa do widget UserBlocScreen
criar uma interface do usuário para o estado atual.
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(); } }
O código para o widget UserBlocScreen
é ainda mais simples do que nos exemplos anteriores. Para ouvir as alterações de estado, o StreamBuilder é usado. StreamBuilder
é um StatefulWidget
que se cria de acordo com o último valor (Snapshot) do fluxo ( Stream ).
Prós
- Não são necessárias bibliotecas de terceiros.
- Lógica comercial, gerenciamento de estado e carregamento de dados são separados do código da interface do usuário.
- Reatividade Não há necessidade de chamar métodos adicionais, como no exemplo do modelo com escopo
notifyListeners()
.
Contras
- O limite de entrada é um pouco maior. Precisa de experiência com fluxos ou rxdart.
Ligações
Você pode ler o código completo baixando-o do meu repositório do github .
Artigo original publicado no Medium