Pular para conteúdo

Documento de Referência Arquitetural

Introdução

Este documento descreve a arquitetura proposta para o desenvolvimento de um aplicativo móvel utilizando flutter.

O código está disponível em: http://git.magnasistemas.com.br/mith/labs/projeto-referencia-flutter.git

Este código implementa um aplicativo para retornar usuários de uma equipe, onde você pode consultar detalhes de um usuário, ou fazer um CRUD completo de tarefas para a equipe.

Arquitetura Geral

A arquitetura do aplicativo seguirá os princípios de separação de responsabilidades, modularidade e escalabilidade. Utilizaremos uma abordagem MVVM (Model-View-ViewModel) para organizar o código em camadas distintas.

Componentes Principais

  1. View: Responsável pela apresentação da interface do usuário.
  2. ViewModel: Responsável pela lógica de negócios e interação com os dados.
  3. Model: Representa os dados e a lógica de negócios.

Bibliotecas e Frameworks

  • Material Design 3: Para uma interface do usuário moderna e consistente.
  • Design Pattern Repository: Para separação clara entre a fonte de dados e a lógica de negócios.
  • GetX: Para gerenciamento de estado e navegação entre páginas.
  • SQLite: Para armazenamento local de dados.
  • Dio: Para requisições REST.

Iniciando

  1. Para iniciar a estrutura inicial do projeto, após a instalação correta do flutter, basta executar flutter install <nome_do_projeto>, respeitando o padrão de nomenclatura em snake case.
  2. Neste exemplo o comando flutter install poc_flutter foi executado.
  3. A seguinte estrutura será gerada:
- 📁 poc_flutter
    - 📁 android
    - 📁 ios
    - 📁 lib
      - 📄 main.dart
    - 📁 linux
    - 📁 macos
    - 📁 test
    - 📁 web
    - 📁 windows
    - 📄 analysis_options.yaml
    - 📄 devtools_options.yaml
    - 📄 pubspec.lock
    - 📄 pubspec.yaml
  • O desenvolvimento acontece dentro da pasta lib, sendo o arquivo main a porta de entrada da aplicação.
  • O arquivo pubspec.yaml guarda a configuração do projeto, no que diz respeito a compilação, as suas dependencias e estruturas de testes.
  • As demais pastas tratam dos arquivos de configuração necessários a compilação para a plataforma específica, não necessitando interação exceto se haja necessidade de especialização.

A configuração

Estrutura básica de dependencias gerado pelo sdk flutter contendo:

  • O nome do projeto, que será o nome final do arquivo compilado, uma configuração de publicação e sua versão atual.
  • A definição da versão do SDK Flutter.
  • A declaração das dependencias utilizadas pelo projeto final e as dependencias utilizadas apenas durante o desenvolvimento, como os testes.
name: poc_flutter
description: "A new Flutter project."
publish_to: 'none' 

version: 1.0.0+1

environment:
  sdk: '>=3.3.3 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.6
  get: ^4.6.6
  shared_preferences: ^2.2.3
  shared_preferences_ios: ^2.1.1
  shared_preferences_android: ^2.2.2
  hive: ^2.2.3
  path_provider: ^2.1.3
  sqflite: ^2.3.3
  dio: ^5.4.3+1
  intl: ^0.19.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^3.0.0

flutter:

  uses-material-design: true

Porta de entrada

O método main com retorno void é a porta de entrada para qualquer aplicação dart, e no flutter não poderia ser diferente. A porta de entrada mais básica de uma aplicação flutter seria:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}
  1. O método ensureInitialized do WidgetsFlutterBinding é muito importante na nossa aplicação, e provavelmente da grande maioria delas, pois a função dele é chamar o código nativo (caso precise) antes de rodar o runApp
  2. O método runApp é mantido, pois ele é quem amarra a estrutura inicial da aplicação flutter.
  3. A estrutura visual básica MyApp() é um Widget, que representa aquilo que é visível na tela.

Primeiro Widget

MyApp é um Widget, do tipo StatelessWidget, que configura tudo o que precisaremos a respeito estilização. Visto que o flutter é organizado por meio de composição, MyApp tem um papel fundamental, pois é a raiz dos widgets.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        colorScheme: ColorScheme.fromSeed(
          surface: Colors.blue,
          primary: Colors.blueGrey,
          seedColor: Colors.grey,
          secondary: Colors.grey,
        ),
        useMaterial3: true,
      ),
      home: const GetxHomeScreen(),
    );
  }
}
1. O convencional MaterialApp é substituído por GetMaterialApp que é uma implementação do Getx, responsável pela navegação e injeção de dependências em toda a aplicação. A utilização desta dependencia facilita a navegação e injeção de dependências, entretanto é necessário configurar toda a aplicação desde o início para ativar. Em breve um exemplo da utilização do potencial do Getx. 2. GetMaterialApp assim como MaterialApp recebe a configuração inicial do Material e pode ser feita de várias formas. Uma forma otimizada de utilizar o material é com Temas, objetos ThemData que encapsulam o estilo e podem ser replicados e utilizados por toda a aplicação. 3. O home page recebe o Widget que de fato representa a sua tela inicial, neste exemplo utilizamos GetxHomeScreen com uma injeção de dependencia.

Pages, controllers e repositories

Dentro da pasta lib estarão todos os arquivos dart com a implementação do aplicativo. Portanto, é conveniente organizar os arquivos dart em diretórios que identifiquem os seus objetivos.

Uma estrutura otimizada contendo o GetxHomePage seria:

    - 📁 lib
        - 📁 core
        - 📁 getx
            - 📁 git_users
                - 📁 model
                - 📁 view
                    - 📁 pages
                        - 📄 getx_home_screen.dart <-----
                - 📁 view_model
        - 📄 main.dart

Essa estrutura, que se assemelha ao MVC, no flutter é conhecido como Model-View-ViewModel. O MVVM separa a lógica de apresentação (View) da lógica de negócios (Model) e da lógica de visualização (ViewModel).

  • Para fíns de familiaridade e experiencia de desenvolvimento, separamos a camada de model em entities, repositories e services.
  • Outra parte importante seria a organização de pastas por feature, onde cada pasta teria o nome da feature, com a estrutura MVVM

A Home Page

O widget GetxHomePage, do tipo StatefulWidget, vai oferecer as funcionalidades iniciais do aplicativo. Recebe o GitUsersViewModel como dependencia para orquestrar a execução das lógicas de negócio.

class GetxHomeScreen extends StatefulWidget {
  const GetxHomeScreen({super.key});

  @override
  State<GetxHomeScreen> createState() => _GetxHomeScreenState();
}

class _GetxHomeScreenState extends State<GetxHomeScreen> {
  ///Aqui ocorre a primeira injeção de dependências, onde a gente chama a ViewModel, pra fazer o controle da Tela
  final GitUsersViewModel gitUsersViewModel = Get.put(GitUsersViewModel());

  @override
  void initState() {
    gitUsersViewModel.fetchListGitUsers();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final apiResponse = gitUsersViewModel.apiResponse;
    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          actions: [
            InkWell(
              onTap: () {
                Get.to(
                      () => const GitUserTasksPage(),
                );
              },
              child: const Padding(
                padding: EdgeInsets.only(right: 16),
                child: Icon(Icons.label, color: Colors.white,),
              ),
            )
          ],

          title: const Center(
            child: Text(
              'Time AlmaViva',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
        body: Obx(
              () {
            switch (apiResponse.value.status) {
              case ApiStatus.loading:
                return const Center(
                  child: CircularProgressIndicator(),
                );
              case ApiStatus.loaded:
                return ListView.builder(
                  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
                  itemCount: apiResponse.value.data?.gitUsersList?.length,
                  itemBuilder: (BuildContext context, int index) {
                    //Text('${apiResponse.value.data?.gitUsersList?[index].login}')
                    return Padding(
                      padding: const EdgeInsets.symmetric(vertical: 4),
                      child: Card(
                        elevation: 2,
                        child: ListTile(
                          onTap: () {
                            Get.to(
                              GitUserDetailPage(
                                model: apiResponse.value.data!.gitUsersList![index],
                              ),
                            );
                          },
                          leading: CircleAvatar(
                            backgroundImage: NetworkImage(
                              apiResponse.value.data!.gitUsersList![index].avatarUrl.toString(),
                            ),
                          ),
                          title: Text('${apiResponse.value.data?.gitUsersList?[index].id}'),
                          subtitle: Text('${apiResponse.value.data?.gitUsersList?[index].login}'),
                          trailing: const Icon(Icons.arrow_forward_ios),
                        ),
                      ),
                    );
                  },
                );
              case ApiStatus.error:
                return const Center(
                  child: Text('Por favor, tente novamente'),
                );
              case ApiStatus.initial:
              default:
                return const Center(
                  child: Text('Estado Inicial'),
                );
            }
          },
        ),
      ),
    );
  }
}
  • Como um StatefulWidget, possui a classe interna que representa o estado, extendendo a classe State.
  • É utilizada uma estrutura básica do MaterialDesign que é o Scaffold, que possui estruturas visuais amigáveis aos dispositivos modernos como o AppBar e o Body.
  • Body é o maior espaço visível no dispositivo recebe também um Widget, de qualquer tipo.
  • Por meio da dependencia GetX, utilizando a implementação Obx, injetamos a dependencia que foi gerada. Dentro do escopo do Obx utilizamos o ApiResponse, que irá fazer o controle da tela.
  • Nesta pagina estão exibidos os usuários de uma equipe, trazendo o nome deles, e sua foto de perfil
  • Para isso, a lógica de consultas e inserções estão centralizadas na classe GitUsersViewModel.

A View Model

  • A classe GitUsersViewModel possui a dependencia da classe GitUsersRepository.
  • A responsabilidade da View Model é guardar os objetos que serão utilizados pelo estado e avisá-los quando houver mudança.
  • A View Model só funcionará corretamente se aplicarmos o conceito de herança, herdando a classe GetxController.
  • Para que todos os widgets que observam os atributos e métodos de GitUsersViewModel é necessário chamar o método obs ao final das variaveis.
class GitUsersViewModel extends GetxController{

  ///A apiResponse é quem vai ficar responsavel por lidar com as respostas da API / Banco de Dados como listado abaixo
  final Rx<ApiResponse<GitUsersResponseModel>> apiResponse = ApiResponse<GitUsersResponseModel>.initial('Sem dados').obs;


  ///Aqui pegamos uma lista de usuários, ela vem do Repository
  Future<void> fetchListGitUsers() async {
    apiResponse.value = ApiResponse.loading('Carregando Data');

    try{
      GitUsersResponseModel gitUsersList = await GitUsersRepository().fetchListGitUsers();
      print(gitUsersList);
      apiResponse.value = ApiResponse.loaded(gitUsersList);
    } catch (e){
      apiResponse.value = ApiResponse.error(e.toString());
      print(e);
    }
  }
}
  • Nota-se que toda a Aplicação da View Model, tem como base a classe ApiResponse, ela é nossa base para fazer o controle da View

O Repository

  • O MarcaRepository é uma classe vanilla, sem atributos especiais nem acumulo de estado.
  • Ela é quem faz a comunicação entre a Service e a View Model.
class GitUsersRepository {

  GitUsersServiceImpl gitUsersService = GitUsersServiceImpl();

  Future<GitUsersResponseModel> fetchListGitUsers() async{
    GitUsersResponseModel response = await gitUsersService.fetchListGitUsers();
    return response;
  }
}

A Service

  • A GitUsersServiceImpl é quem fica responsável por fazer as requisições REST, através dá biblioteca DIO, chamamos a requisição que nos retornará uma entidade.
class GitUsersService{
  final Dio dio = Dio();

  Future<GitUsersResponseModel> fetchListGitUsers() async {
    try{
      final response = await dio.get('https://api.github.com/users');
      print(response.data);

      return GitUsersResponseModel.fromJson(response.data);
    } on DioException catch (e){
      if(e.error != null){
        throw e.error!;
      }
      throw UnknownException();
    }
  }
}
  • Como podemos ver nessa classe, tentamos fazer o get de https://api.github.com/users ⧉.
  • Caso seja uma resposta positiva, chamamos a classe GitUsersResponseModel, passando a resposta da requisição, que no nosso caso é uma lista de usuários
  • Caso seja uma resposta negativa, fazemos a tratativa de erro.

A Entidade

  • A GitUsersResponseModel e a GitUsersModel é quem fica responsável por ser nossas entidades

Exemplo Entidade de Objeto

class GitUsersModel {
  final int? id;
  final String? login;
  final String? avatarUrl;
  final String? reposUrl;

  GitUsersModel({
    this.id,
    this.login,
    this.avatarUrl,
    this.reposUrl,
  });

  factory GitUsersModel.fromJson(Map<String, dynamic> json) {
    return GitUsersModel(
      id: json['id'],
      login: json['login'],
      avatarUrl: json['avatar_url'],
      reposUrl: json['repos_url'],
    );
  }
}

Exemplo Entidade de Lista

class GitUsersResponseModel {
  final List<GitUsersModel>? gitUsersList;

  GitUsersResponseModel({
    required this.gitUsersList,
  });

  factory GitUsersResponseModel.fromJson(List<dynamic> json) {
    return GitUsersResponseModel(
      gitUsersList: json != null
          ? List<GitUsersModel>.from(
        json.map(
              (i) => GitUsersModel.fromJson(i),
        ),
      )
          : null,
    );
  }
}
- Essas são entidades que vão ser quem vão servir de modelo pra resposta da requisição - E é no método fromJson que fazemos a conversão do Json para um Objeto Dart

No body da GetxHomeScreen temos a lista de usuários e para cada usuário o método onTap está declarado fornecendo o comportamento de navegação por meio do Getx.

  • Com o GetX podemos fazer uma navegação entre páginas respeitando o padrão do Material de forma organizada e simplificada. Esta navegação, por exemplo, mantém o estado, oferece comportamentos de retorno e breadcrumb se habilitado.
  • Para completar a navegação é necessário fornecer o outro widget para onde se destina e suas dependencias necessárias.
  • No exemplo é fornecido a nossa classe Entidade, que vai servir de modelo para a pagina de Detalhes.
onTap: () {
  Get.to(
    GitUserDetailPage(
      model: apiResponse.value.data!.gitUsersList![index],
    ),
  );
}

A GitUserDetailPage

  • GitUserDetailPage possui o atributo model, que é recebido na navegação da home page para a de detalhes
  • Podemos perceber um padrão, começamos instanciando a View Model
  • No metódo initState, chamamos a instância da View Model para receber uma lista de repositorios do usuário, passando alguns Paramêtros como reposUrl
  • Após isso, continuamos construindo nossa classe, e usamos o Obx quando formos mexer com os estados de uma Pagina
class GitUserDetailPage extends StatefulWidget {
  final GitUsersModel model;

  const GitUserDetailPage({super.key, required this.model});

  @override
  State<GitUserDetailPage> createState() => _GitUserDetailPageState();
}

class _GitUserDetailPageState extends State<GitUserDetailPage> {
  final gitUserReposViewModel = Get.put(GitUserDetailReposViewModel());

  @override
  void initState() {
    gitUserReposViewModel.fetchListGitUserRepos(
      GitUserDatailReposParams(reposUrl: widget.model.reposUrl),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final apiResponse = gitUserReposViewModel.apiResponse;

    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          backgroundColor: Colors.white,
        ),
        body: Column(
          children: [
            HeaderUserDetailWidget(
              model: widget.model,
            ),
            const Padding(
              padding:  EdgeInsets.only(bottom: 24),
              child:  Text(
                'Trabalhos do Usuário',
                style: TextStyle(fontSize: 18),
              ),
            ),
            Obx(
              () {
                switch (apiResponse.value.status) {
                  case ApiStatus.loading:
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  case ApiStatus.loaded:
                    return Flexible(
                      child: ListView.builder(
                        itemCount: apiResponse.value.data!.gitReposList!.length,
                        itemBuilder: (ctx, i) {
                          return Padding(
                            padding: const EdgeInsets.only(left: 20, right: 20, bottom: 5),
                            child: Card(
                              color: Colors.white,
                              elevation: 2,
                              child: ListTile(
                                contentPadding: const EdgeInsets.only(left: 16, bottom: 4, top: 4),
                                title: Text(
                                  apiResponse.value.data!.gitReposList![i].name!,
                                ),
                                subtitle: Row(
                                  children: [
                                    const Padding(
                                      padding: EdgeInsets.only(right: 8),
                                      child: Icon(
                                        Icons.remove_red_eye,
                                      ),
                                    ),
                                    Text(
                                      apiResponse.value.data!.gitReposList![i].watchersCount.toString(),
                                    ),
                                    const SizedBox(
                                      width: 20,
                                    ),
                                    const Padding(
                                      padding: EdgeInsets.only(right: 8),
                                      child: Icon(
                                        Icons.alt_route,
                                      ),
                                    ),
                                    Text(
                                      apiResponse.value.data!.gitReposList![i].watchersCount.toString(),
                                    ),
                                  ],
                                ),
                              ),
                            ),
                          );
                        },
                      ),
                    );
                  case ApiStatus.error:
                    return const Center(
                      child: Text('Por favor, tente novamente'),
                    );
                  case ApiStatus.initial:
                  default:
                    return const Center(
                      child: Text('Estado Inicial'),
                    );
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

A GitUserTasksPage

  • Podemos perceber novamente um padrão, começamos instanciando a View Model, porém, essa tela não é uma tela que recebe requisição de API
  • Diferente das outras Pages, essa faz uma consulta com o banco de dados, todas as informações são alteradas e observadas usando um banco e não a API
  • No metódo initState, chamamos a instância da View Model para receber uma lista de Tasks do banco de dados
  • Após isso, continuamos construindo nossa classe, e usamos o Obx quando formos mexer com os estados de uma Pagina
class GitUserTasksPage extends StatefulWidget {
  const GitUserTasksPage({super.key});

  @override
  State<GitUserTasksPage> createState() => _GitUserTasksPageState();
}

class _GitUserTasksPageState extends State<GitUserTasksPage> {
  final tasksViewModel = Get.put(GitUserTasksViewModel());

  @override
  void initState() {
    tasksViewModel.fetchTasks();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final apiResponse = tasksViewModel.apiResponse;

    return SafeArea(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Tasks Equipe', style: TextStyle(color: Colors.white),),
        ),
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            showDialog(
              context: context,
              builder: (_) => TaskWidget(
                onSubmit: (List<String> values) async {
                  await tasksViewModel.createTask(title: values.first, description: values.last);
                  Get.back();
                },
              ),
            );
          },
        ),
        body: Obx(() {
          switch (apiResponse.value.status) {
            case ApiStatus.loading:
              return const Center(
                child: CircularProgressIndicator(),
              );
            case ApiStatus.loaded:
              return ListView.separated(
                itemBuilder: (ctx, index) {
                  final createdDate = DateFormat('dd/MM/yyyy').format(DateTime.parse(apiResponse.value.data![index].createdAt!));
                  return ListTile(
                    onTap: () {
                      showDialog(
                        context: context,
                        builder: (context) => TaskWidget(
                          task: apiResponse.value.data?[index],
                          onSubmit: (List<String> values) async {
                            await tasksViewModel.updateTask(
                              title: values.first,
                              description: values.last,
                              id: apiResponse.value.data![index].id!,
                            );
                            Get.back();
                          },
                        ),
                      );
                    },
                    title: Text(
                      '${apiResponse.value.data?[index].title}',
                    ),
                    subtitle: Text(
                      createdDate,
                    ),
                    trailing: InkWell(
                      onTap: () async {
                        await tasksViewModel.deleteTask(
                          id: apiResponse.value.data![index].id!,
                        );
                        tasksViewModel.fetchTasks();
                      },
                      child: const Icon(
                        Icons.delete,
                        color: Colors.red,
                      ),
                    ),
                  );
                },
                separatorBuilder: (context, index) => const SizedBox(
                  height: 10,
                ),
                itemCount: apiResponse.value.data!.length,
              );
            case ApiStatus.error:
              return const Center(
                child: Text('Por favor, tente novamente'),
              );
            case ApiStatus.initial:
            default:
              return const Center(
                child: Text('Estado Inicial'),
              );
          }
        }),
      ),
    );
  }
}

O banco de dados

Essas classes singleton centraliza a complexidade de lidar com o banco de dados SqLite contendo:

  • Uma classe contendo a inicialização do banco de dados que seria a classe base, e outra que trabalha com os scripts do banco
  • Todo método que bloqueia a thread é retornado comoFuture<>e deve ser tratado corretamente com o await para se obter oResultadodoFuture`.
  • O Sqlite segue a sintaxe sql e oferece métodos amigáveis a sinxate dart para interagir com os dados.

Exemplo Classe Base

class GitUserReposTasksDbService {
  Database? _database;

  Future<Database> get database async {
    if (_database != null) {
      return _database!;
    } else {
      _database = await _initialize();
      return _database!;
    }
  }

  Future<String> get fullPath async {
    const name = 'tasks_db';

    final path = await getDatabasesPath();

    return join(path, name);
  }

  Future<Database> _initialize() async {
    final path = await fullPath;

    var database = await openDatabase(
        path,
        version: 1,
        onCreate: create,
        singleInstance: true
    );
    return database;
  }

  Future<void> create(Database database, int version) async => await GitUserReposTasksDb().createTable(database);
}

Exemplo Classe dos Scripts

class GitUserReposTasksDb {
  final tableName = 'tasks';

  Future<void> createTable(Database database) async {
    await database.execute("""CREATE TABLE IF NOT EXISTS $tableName (
    "id" INTEGER NOT NULL,
    "title" TEXT NOT NULL,
    "description" TEXT NOT NULL,
    "created_at" INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)),
    PRIMARY KEY("id" AUTOINCREMENT)
    );""");
  }

  Future<int> create({String? title, String? description}) async {
    final database = await GitUserReposTasksDbService().database;

    return await database.rawInsert(
      """INSERT INTO $tableName (title,description,created_at) VALUES (?,?,?)""",
      [title, description, DateTime.now().millisecondsSinceEpoch],
    );
  }

  Future<List<GitTasksModel>> fetchAllTasks() async {
    final database = await GitUserReposTasksDbService().database;
    final gitUsersList = await database.rawQuery(
      """SELECT * from $tableName ORDER BY (created_at)""",
    );
    return gitUsersList.map((gitUser) => GitTasksModel.fromJson(gitUser)).toList();
  }

  Future<GitTasksModel> fetchById({required int id}) async {
    final database = await GitUserReposTasksDbService().database;
    final gitUser = await database.rawQuery(
      """SELECT * from $tableName WHERE id = ?""",
      [id],
    );

    return GitTasksModel.fromJson(gitUser.first);
  }

  Future<int> update({required int id, String? title, String? description}) async {
    final database = await GitUserReposTasksDbService().database;
    return await database.update(
      tableName,
      {
        if (title != null) 'title': title,
        if (description != null) 'description': description,
      },
      where: 'id = ?',
      conflictAlgorithm: ConflictAlgorithm.rollback,
      whereArgs: [id],
    );
  }

  Future<void> delete({required int id}) async {
    final database = await GitUserReposTasksDbService().database;
    await database.rawDelete(
      """DELETE FROM $tableName WHERE id = ?""",
      [id],
    );
  }

  Future<void> deleteAll() async {
    final database = await GitUserReposTasksDbService().database;
    await database.delete(
      tableName,
    );
  }
}

Extra

Propriedades

Em uma aplicação mobile existe muita aplicabilidade para configurações editáveis, ou seja, uma persistencia local. O Hive é uma implementação que abstrai a plataforma e oferece uma api para interagir com propriedades.

Para oferecer este comportamento por toda a aplicação, independente do escopo, utilizamos o Getx, que assim como o Provider, oferece uma forma de injeção de dependencia. A injeção do getx oferece uma forma mais global de injeção de dependencia, bastando extender GetxController.

  • Para interagir com o Hive é necessário fornecer um diretório dentro do sistema operacional.
  • A dependencia path_provider auxilia com o método getApplicationDocumentsDirectory() a obter o diretório com as caracteristicas ideais para guardar as propriedads.
  • Neste exemplo o Hive é utilizado para guardar a configuração de Material ThemeMode, para fornecer a funcionalidade de tema dark e light na aplicação.
  • O código abaixo demonstra como inicializar o hive, inputar uma propriedade e recuperar uma propriedade.
class ThemeController extends GetxController {

  var isDark = false.obs;
  Map<String, ThemeMode> themeModes = {
    'light': ThemeMode.light,
    'dark': ThemeMode.dark
  };

  late SharedPreferences prefs; 

  static ThemeController get to =>
      Get.find();

  loadThemeMode() async {
    Directory dir = await getApplicationDocumentsDirectory();
    var box = await Hive.openBox('preferencias', path: dir.path);
    String themeText = box.get('theme') ?? 'light';
    isDark.value = themeText == 'dark';
    setMode(themeText);
  }

  Future setMode(String themeText) async {
    ThemeMode themeMode = themeModes[themeText]!;
    Get.changeThemeMode(themeMode);
    var box = await Hive.openBox('preferencias');
    await box.put('theme', themeText);
  }

  changeTheme() {
    setMode(isDark.value ? 'light' : 'dark');
    isDark.value = !isDark.value;
  }
}

Conclusão

A arquitetura proposta combina as melhores práticas e tecnologias atuais para o desenvolvimento de aplicativos móveis. A utilização do Material Design 3 garante uma interface do usuário moderna e intuitiva, enquanto as demais tecnologias oferecem uma base sólida para o desenvolvimento de um aplicativo robusto e escalável.

Com esta arquitetura, esperamos alcançar um código bem organizado, fácil de manter e expandir, proporcionando uma excelente experiência para os usuários do aplicativo.