Uma Idéia para Arquitetura de Aplicativos Flutter
Tags: flutter, architecture, mobile
Published: 2021-02-17 02:22:28

Como este texto tem como ponto central uma idéia de arquitetura para aplicações Flutter, deixe-me explicar o que uma imagem de uma espécie de casa está fazendo aqui. Ela é uma das igrejas de madeiras encontradas na Noruega. Para ser mais específico, na foto estamos vendo a Borgund Stave church. Para saber mais sobre estas igrejas impressionantes visite esta <A href="https://www.visitnorway.com/typically-norwegian/stave-churches/">página</a>. Devido a sua arquitetura ímpar, ela estampou a capa deste texto :).

###Arquitetura base
Estudando possíveis arquiteturas já existentes para desenvolvimento Flutter, encontrei o artigo "A importância da Clean Architecture no Flutter", escrito por Fabio Maia e disponível <a href="https://www.objective.com.br/insights/a-importancia-da-clean-architecture-no-flutter/">aqui</a>. O principal motivo por esta arquitetura chamar a minha atenção foi a semelhança com o JetPack Architecture Componentes, sendo assim, seria de fácil entendimento pelos desenvolvedores oriundos do nativo, principalmente do Android nativo. Abaixo, a imagem encontrada no artigo do Maia:

<img src="https://www.objective.com.br/wp-content/uploads/2020/03/Imagem3.png"/> 

E abaixo você encontra um resumo alto nível das pastas que criei no projeto Flutter seguindo a estrutura proposta por Maia. Porém, não utilizei exatamente o mesmo fluxo de dados. Usei um pacote gerenciador de estados para não precisar ter o fluxo partindo dos dados até os Widgets.

<img src="https://dev-to-uploads.s3.amazonaws.com/i/zicctq56zvivg6b03uso.png"/>

Para ficar mais claro, veja como fica a estrutura de pastas do projeto base:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ye6ljqe8coz0fuxl7eu3.png)

###Aplicativo Base
O aplicativo aqui proposto é algo bem simples. Uma tela de login que leva para uma lista dos filmes preferidos, lendo os dados de um serviço web.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9mc190603u2ad6qmaac.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmpd9zf70scalwt0xos6.png)

###i18n no projeto
Começamos explicando o padrão usado para internacionalização. Seguimos a documentação entitulado como "Internationalizing Flutter apps", disponível no site de desenvolvedores Flutter. Link <a href="https://flutter.dev/docs/development/accessibility-and-localization/internationalization">aqui</a>.

Devido ao uso deste guia, temos uma pasta no diretório lib chamada l10n. Dentro desta, podemos encontrar 3 arquivos extensão **.arb**, definindo o conteúdo dos textos internacionalizados na aplicação. Sendo eles: **app_en.arb**, **app_es.arb** e **app_pt.arb**:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1x4oxjztu7etb9doxe4i.png)

Abaixo, veja o conteúdo do app_en.arb:

```
{
  "login": "Log in",
  "@login": {},

  "user": "User",
  "@user": {},
  "enterUser": "Enter your user",
  "@enterUser": {},
  "pleaseEnterUser": "Please, enter your user",
  "@pleaseEnterUser": {},

  "password": "Password",
  "@password": {},
  "enterPassword": "Enter your password",
  "@enterPassword": {},
  "pleaseEnterPassword": "Please, enter your password",
  "@pleaseEnterPassword": {}
}
```

O uso do arroba no nome do recurso de texto serve para adição de uma possível descrição do que é este recurso de texto. Aqui não foi usado, porém, é perfeitamente possível sua utilização. Outro ponto. Estas descrições só são exigidas no template do arquivo de internacionalização (vamos falar sobre isso na sequência). Ou seja, o conteúdo do arquivo **app_es.arb** não contém os recursos precedidos pelo arroba:

```
{
  "login": "Iniciar Sessión",

  "user": "Usuario",
  "enterUser": "Ingrese su  usuario",
  "pleaseEnterUser": "Por favor, ingrese su usuario!",

  "password": "Contraseña",
  "enterPassword": "Ingressa tu contraseña",
  "pleaseEnterPassword": "Por favor, introduzca su contraseña!"
}
```

Na pasta raiz do projeto, também temos um arquivo **l10n.yaml**, crucial para a internacionalização. Seu conteúdo clarifica algumas coisas que mostramos anteriormente:

```
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
```

Falta pouco. Outra alteração é necessária no **pubspec.yaml**. Na sub-seção de **dependencies** adicionamos o **flutter_localizations**. Na sub-seção **flutter**, colocamos a propriedade **generate** com valor **true**.

```
...

dependencies:
  flutter_localizations: # Add this line
    sdk: flutter

...

# The following section is specific to Flutter.
flutter:
  generate: true
```

E para completar a configuração de i18n é necessário a configuração de alguns **delegates** na MaterialApp. Os três pontos indicam parte do código que será visto na sequência e não importância neste momento.

```
...

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      ...
    );
  }

}
```

### Configurações Globais
Neste projeto também tomei o cuidado de encontrar um pacote que facilitasse a criação de configurações globais, como definição de url´s, pensando da divisão do projeto em flavors, ou ainda, alguma chave de api usada na aplicação, como Google Maps ou alguma chave de web service, por exemplo.

O primeiro passo é criar uma pasta **assets->cgf** com um arquivo **app_settings.json**.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yzbpcayuch3sg4bme0cf.png)

O conteúdo deste arquivo pode ser visto aqui:

```
{
  "api_key": "ae5319efd54af97f99f70e5******"
}
```

No momento só temos a chave de api para consumir um serviço web, que veremos na sequência. Porém, qualquer outra configuração global pode ser inserida aqui respeitando a estrutura do arquivo json.

Algumas alterações também são necessárias no **pubspec.yaml**. Na parte de dependências, adicione o pacote **global_configuration** seguido pela sua última versão. Na sub-seção **flutter** -> **assets** adicione o caminho do arquivo de configurações json.

```
dependencies:
  ...
  global_configuration: ^1.6.0

flutter:
  ...
  assets:
    - assets/cfg/
```

Por fim, precisamos apenas carregar essas configurações em algum momento na nossa árvore de widgets. Eu preferi fazer isso logo na inicialização da aplicação:


```
void main() async{
  WidgetsFlutterBinding.ensureInitialized();
  await GlobalConfiguration().loadFromAsset("app_settings");
  runApp(MyApp());
}

class MyApp extends StatelessWidget {

  ...

}
```

Link para o pacote **global_configuration** [aqui](https://pub.dev/packages/global_configuration).

Posteriormente, vamos usar os valores destas configurações na aplicação.

###Menção honrosa: Get 
Usamos diversos pacotes nesse modelo de arquitetura, porém, um deles merece destaque, porque foi usado em diversos momentos da aplicação. Estamos falando do *Get*. Sua documentação pode ser encontrada [aqui](https://pub.dev/packages/get). Usamos este pacote para gerenciamento de estados, controle de rotas e controle de dependências.

###Rotas e Injeção de Dependência com Get
No arquivo *main.dart*, veremos o uso do Widget GetMaterialApp, um pouco incomum, visto que, o “normal” seria o MaterialApp. Isso foi necessário para a configuração de rotas com o *Get*. Perceba as propriedades *initialRoute* e *getPages*. Este último, por sua vez, recebeu o valor routes, que está definido no arquivo *routesDefinitions*, na pasta *lib -> core*.

```
class MyApp extends StatelessWidget {

 @override
 Widget build(BuildContext context) {

   return GetMaterialApp(
     ...
     initialRoute: loginPageName,
     getPages: routes,
   );
 }

}
```
 
O *routesDefinitions.dart* possui a definição de rotas, que no pacote Get é chamado de *page*. Perceba dois pontos importantes aqui. Um deles é a definição de constantes para o nome das páginas. No momento temos apenas duas. O segundo ponto é a propriedade *binding* na primeira página. Desta forma, criamos uma vinculação e uma dependência automática entre esta página e uma instância de *CoreBinding*.

```
const String loginPageName = "/login";
const String listPageName = "/list";

final routes = [
 GetPage(
     name: loginPageName,
     page: () => LoginScreen(),
     binding: CoreBinding()
 ),
 GetPage(
   name: listPageName,
   page: () => ListScreen.initialize(),
 )
];
```

Finalmente, o *CoreBinding* tem um código um pouco mais simples. Mas, perceba a herança com *Bindings*. E também, a sobrescrita do *dependencies*. Desta forma, vinculamos este método como dependência neste binding. Dentro do método estamos usando o *lazyPut* do *Get*, para que, quando necessário, uma instância de *RestClient* seja fornecida, já usando o design pattern *Singleton*. Assim, na primeira tela da aplicação já teremos criado uma instância da classe que é o ponto de conexão da aplicação com o mundo do webservice.

```
class CoreBinding extends Bindings{

 @override
 void dependencies() {
   Get.lazyPut<RestClient>(() => RestClient(Dio()));
 }

}
```

###Camada de Dados
Agora que falamos sobre alguns tópicos mais gerais, podemos entrar nas camadas que fazem parte da espinha dorsal da arquitetura, e, que foi mostrada nas figuras iniciais deste texto. Na pasta lib temos uma pasta filha, chamada *dataSources*. Dentro desta, por sua vez, temos uma pasta database para dados locais, salvos no banco de dados *NoSQL* com um *ORM*. E, uma pasta *webServices* para acesso a dados de um serviço REST. 

No *webServices*, usei o pacote *retrofit*, documentação [aqui](https://pub.dev/packages/retrofit). A principal razão foi pela similaridade com uma biblioteca homônima, disponível para desenvolvimento nativo Android.

Veja o conteúdo do arquivo *restClient.dart*, que está na pasta *lib -> dataSources -> webServices*. 

```
part 'restClient.g.dart';

@RestApi(baseUrl: "https://api.themoviedb.org/3")
abstract class RestClient {
 factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

 @GET("/movie/top_rated")
 Future<ResultGroup> getTopRated();

}
```

O pacote *retrofit* usa o *build_runner* para geração de código automático. No *restClient.g.dart* tem um detalhe muito importante. Estou colocando a *apikey* do webservice *themoviedb*, lendo esta informação do pacote de configurações globais. Veja o trecho do qual estou me referindo.

```
@override
Future<ResultGroup> getTopRated() async {
 const _extra = <String, dynamic>{};
 final queryParameters = <String, dynamic>{};
 final _data = <String, dynamic>{};
 final _result = await _dio.request<Map<String, dynamic>>(
     '/movie/top_rated?api_key=${GlobalConfiguration().getValue("api_key")}',
     queryParameters: queryParameters,
     options: RequestOptions(
         method: 'GET',
         headers: <String, dynamic>{},
         extra: _extra,
         baseUrl: baseUrl),
     data: _data);
 final value = ResultGroup.fromJson(_result.data);
 return value;
}
```

Já na camada de dados local, estamos usando o *Floor*. Sendo assim, precisamos criar uma entidade, o objeto de acesso aos dados (DAO) e o banco de dados propriamente dito. 

A entidade está na pasta *useCases -> models*, no arquivo *topRatedResponse.dart*, na classe Result. Veja o uso da anotação *@entity*. Também, a anotação *@JsonSerializable*, porque estamos usando o pacote *json_serializable* documentação está disponível [aqui](https://pub.dev/packages/json_serializable).

```
part 'topRatedResponse.g.dart';

@entity
@JsonSerializable()
class Result {
 bool adult;
 String backdrop_path;

 @primaryKey
 int id;

 String original_language;
 String original_title;
 String overview;
 double popularity;
 String poster_path;
 String release_date;
 String title;
 bool video;
 double vote_average;
 int vote_count;

 Result({
   this.adult,
   this.backdrop_path,
   this.id,
   this.original_language,
   this.original_title,
   this.overview,
   this.popularity,
   this.poster_path,
   this.release_date,
   this.title,
   this.video,
   this.vote_average,
   this.vote_count
 });

 factory Result.fromJson(Map<String, dynamic> json) => _$ResultFromJson(json);
 Map<String, dynamic> toJson() => _$ResultToJson(this);
}

@JsonSerializable()
class ResultGroup {
 int page;
 int total_pages;
 int total_results;
 List<Result> results;

 ResultGroup({this.page, this.total_pages, this.total_results, this.results});

 factory ResultGroup.fromJson(Map<String, dynamic> json) =>
     _$ResultGroupFromJson(json);
 Map<String, dynamic> toJson() => _$ResultGroupToJson(this);
}
```

O *DAO (Data Access Object)* está na pasta *lib -> dataSources*, no arquivo *topRatedDAO.dart*. No Floor usamos exaustivamente as anotações, prova disso é o uso do *@dao*, *@Query* e *@insert*. Além disso, todas são auto-expicativas. 

```
@dao
abstract class TopRatedDao {
 @Query('SELECT * FROM Result')
 Future<List<Result>> findAllResults();

 @insert
 Future<List<int>> insertResults(List<Result> results);
}
```

Por fim, na mesma pasta referida anteriormente, encontramos o *appDatabase.dart*.  


```
part 'appDatabase.g.dart'; 

@Database(version: 1, entities: [Result])
abstract class AppDatabase extends FloorDatabase {
 TopRatedDao get topRatedDAO;
}
```
<br/><br/>
##Repositórios

Acima da camada de dados, temos os repositórios. Como nosso aplicativo ainda é muito simples, temos apenas o arquivo *moviesRepository.dart*, na pasta *repositories*. Nesta classes temos dois métodos, uma para chamadas remotas, ou seja, para o web service, nominado como *getRemoteTopRated*. E, um método que busca as informações salvas no banco local, no método nomeado como *getLocalTopRated*.

```
class MoviesRepository {

 void getRemoteTopRated() {
   Get.find<RestClient>().getTopRated().then((response) {
     ListScreenController controller = Get.find();
     Get.find<AppDatabase>().topRatedDAO.insertResults(response.results);
     controller.setResultGroup(response.results);
   });
 }

 void getLocalTopRated() {
   Get.find<AppDatabase>().topRatedDAO.findAllResults().then((response) {
       ListScreenController controller = Get.find();
       controller.setResultGroup(response);
     }
   );
 }

}
```

Aqui usamos o pacote Get para injeção de dependências. Sendo assim, apenas usamos o método estático find da classe Get para requisitar as instâncias das classes desejadas. Em ambos os casos teremos um mesmo retorno de dados, que é passado para o controller da aplicação, que veremos logo na sequência. 

Mas e aonde essas dependências são configuradas? Já vimos isso parcialmente, mas é um conceito importante que vale a pena ser revisto. Quanto ao *RestClient*, isso foi feito no *CoreBinding*. 

```
class CoreBinding extends Bindings{

 @override
 void dependencies() {
   Get.lazyPut<RestClient>(() => RestClient(Dio()));
 }

}
```

Quem assumir a dependência de *binding* com esta classe recebe o método *dependencies* por herança. Sendo assim, quando a página ligada ao *CoreBinding* for chamada, teremos o *lazyPut* do *Get* chamado. E isso podemos ver no *routesDefinitions*:

```
final routes = [
 GetPage(
     name: loginPageName,
     page: () => LoginScreen(),
     binding: CoreBinding()
 ),
 GetPage(
   name: listPageName,
   page: () => ListScreen.initialize(),
 )
];
```

Em relação ao *AppDatabase*, sua instância foi criada no *main.dart*:

```
...

class MyApp extends StatelessWidget {

 @override
 Widget build(BuildContext context) {
   $FloorAppDatabase.databaseBuilder('app_database.db').build().then((appDb) => Get.lazyPut<AppDatabase>(() => appDb));

   ...
 }


}
```

###Casos de Uso
Neste aplicativo de exemplo, temos apenas um caso de uso. Ele está no arquivo *moviesUseCase.dart*, dentro da pasta *useCases*. Devido ao projeto ainda ser simples, o use cases também está simples. Ele é apenas uma camada de ligação para o repositório e a chamada ao método local ou remoto de busca de dados.

```
class MoviesUseCase {

 final MoviesRepository repository = MoviesRepository();

 void getRemoteTopRated() {
   return repository.getRemoteTopRated();
 }

 void getLocalTopRated() {
   return repository.getLocalTopRated();
 }

}
```

###Camada LogicHolders
Acima da camada de casos de uso, temos a *logicHolders*. Dentro dela temos os bindings, já discutidos anteriormente e, os *controllers*. Dentro do mundo Flutter, os controllers geralmente tem uma relação forte com pacotes de gerenciamento de estados, como *Provider*, *BLoC* ou *MobX*, por exemplo. Aqui, vamos usar o pacote Get também para esta função.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c60xr21lwqcqwn5gr9qq.png)

Dentro da pasta *logicHolders -> controllers*, encontramos o arquivo *ListScreenControllers.dart*. O principal ponto aqui é a variável *resultGroup*, que é apenas um observável de uma lista comum. Desta forma, ao atualizar essa lista, como acontece no método *setResultGroup*, chamamos o update do *GetxController* para avisar os observadores que a lista foi alterada. Por fim, o método *getTopRated* faz uma chamada direta ao repositório. Perceba que no momento não estamos tratando a conectividade para chamar o remote ou o local.

```
class ListScreenController extends GetxController{

 final moviesRepositories = MoviesRepository();

 var resultGroup = List<Result>().obs;  //observável.. programação reativa

 setResultGroup(List<Result> results) {
   resultGroup.assignAll(results);
   update();
 }

 getTopRated() => moviesRepositories.getRemoteTopRated();

}
```

###Camada de Widgets
E a última camada na nossa arquitetura é a de *widgets*. Na pasta screens temos uma sub-pasta *customWidgets*, onde ficam os widgets customizados no projeto. No nosso caso temos um *TextFormField* próprio. 

No *listScreen* temos a definição do widget que mostra o formulário de login. Alguns detalhes merecem destaque:

> A aquisição da instância de AppLocalizations logo no início do método build. A variável é usada em diversos momentos onde precisamos dos textos internacionalizados. 
> O uso do componente customizado em diversos momentos: customTextFormField.
> A mudança para uma nova tela (na verdade outro Widget) foi feita também fazendo uso do Get. Veja o na linha *Get.offNamed(listPageName)*. O nome da rota está no arquivo de definição de rotas: *routesDefinitions.dart*.

```
class LoginScreen extends StatelessWidget {

 TextEditingController _userController = TextEditingController();
 TextEditingController _passwordController = TextEditingController();

 final _formKey = GlobalKey<FormState>();

 @override
 Widget build(BuildContext context) {
   AppLocalizations appLocalizations = AppLocalizations.of(context);

   return Scaffold(
     appBar: AppBar(
       title: Text(appLocalizations.login),
     ),
     body: Padding(
       padding: EdgeInsets.all(12),
       child: Form(
         key: _formKey,
         child: Column(
           children: <Widget>[
             customTextFormField(
               appLocalizations.enterUser,
               appLocalizations.user,
               appLocalizations.pleaseEnterUser,
               _userController
             ),
             customTextFormField(
                 appLocalizations.enterPassword,
                 appLocalizations.password,
                 appLocalizations.pleaseEnterPassword,
                 _passwordController
             ),
             Padding(
               padding: EdgeInsets.symmetric(vertical: 16.0),
               child: ElevatedButton(
                 onPressed: () {
                   if (_formKey.currentState.validate()) {
                     Get.offNamed(listPageName);
                   }
                 },
                 child: Text(appLocalizations.login),
               ),
             ),
           ],
         ),
       ),
     ),
   );
 }
}
```
 
Já no *listScreen* usamos o Get de forma mais ampla. Temos um método de inicialização, apenas para inserir a instância do controlador nesta tela. Já no build, esta mesma instância singleton é recuperada. 

No corpo deste Widget usamos o *GetBuilder*. Passamos a chamada ao *getTopRated* no *initState*. No *dispose* também chamamos o *dispose* do *controller*, para não usar recursos de forma desnecessária. E, por fim, no builder chamamos o método *buildBodyWithController*. Este método vai mostrar um loading circular ou, uma lista com os filmes mais bem ranqueados, dependendo do retorno das camadas inferiores ao widget dentro da nossa arquitetura.

```
class ListScreen extends StatelessWidget{

 ListScreen.initialize() {
   Get.put(ListScreenController(), permanent: true);
 }

 @override
 Widget build(BuildContext context) {
   ListScreenController controller = Get.find<ListScreenController>();

   return Scaffold(
     appBar: AppBar(
       title: Text("Top Rated"),
     ),
     body: GetBuilder<ListScreenController>(
       initState: (_) => controller.getTopRated(),
       dispose: (_) => controller.dispose(),
       builder: (controller) => buildBodyWithController(controller),
     ),
     //Obx(() => buildBody())
   );
 }

 Widget buildBodyWithController(ListScreenController controller){
   return controller.resultGroup.value == null ?
       CircularProgressIndicator() :
       ListView.builder(
           itemCount: controller.resultGroup.value.length,
           itemBuilder: (context, index) => buildItemList(controller.resultGroup.value[index])
       );
 }

 Widget buildItemList(Result result) => Card(
     child: ListTile(
       title: Text(result.title),
       subtitle: Text(result.overview),
     ),
 );

}
```