
(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:
- Solicitar / fazer upload de dados de / para a rede.
- Mapeie, transforme, prepare dados e apresente-os ao usuário.
- 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
- Fácil de aprender e entender.
- Nenhuma biblioteca de terceiros é necessária.
Contras
- Toda a árvore do widget é reconstruída sempre que o estado do widget é alterado.
- 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.
- 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
- Lógica de negócios, gerenciamento de estado e separação de código da interface do usuário.
- Fácil de aprender.
Contras
- Requer biblioteca de terceiros.
- À 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.
Ligações
Você pode conferir o código-fonte dos exemplos acima deste repositório do github.
Originalmente, o artigo é publicado no Medium