Как стать автором
Обновить
83.85
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Реализуем чистую архитектуру на Flutter с cubit

Время на прочтение8 мин
Количество просмотров20K

Соблюдать принципы чистой архитектуры – значит обеспечить удобство тестирования, поддержки и модернизации приложения. Понимание архитектуры и state management – это база, необходимая начинающему специалисту для успешной командной работы. В этой статье мы расскажем, как с помощью Cubit реализовать чистую архитектуру на примере стартового приложения Flutter – счетчика нажатий на кнопку. 

Подробнее о работе с фреймворком Flutter мы рассказывали в одной из прошлых статей. На данный момент на Flutter реализуют приложения для мобильных, веб-, настольных и встроенных устройств. 

Библиотека Cubit предназначена для управления состоянием экрана и позволяет реализовать шаблон проектирования BLoC. С ее помощью можно упростить отделение презентации от бизнес-логики, тестирование и переиспользование кода. 

Для начала отметим, что концепция чистой архитектуры, созданная Робертом Мартином, основана на выделении независимых слоев приложения:

Обычно приложение состоит из четырех слоев:

  • Internal  – слой приложения, в котором происходит внедрение зависимостей;

  • Presenters  – слой, в котором описывается визуальная составляющая окна и управление его состоянием;

  • Domain – слой бизнес-логики;

  • Data  – слой, в котором описывается работа с источниками данных (интернет-запрос или база данных).

Также сами слои подразделяются на элементы:

  • data  – элемент слоя data для работы с данными. На этом уровне, например, описываем работу с внешним API;

  • repository – элемент слоя data, который создает и возвращает данные из Data-слоя в виде Entity-объекта;

  • use case – элемент слоя domain, отвечающий за детализацию, описание действия, которое может совершить пользователь системы;

  • presenter – элемент слоя presentation, на этом уровне описывается state management;

  • UI – элемент слоя presentation, на этом уровне описываются визуальные элементы окна.

Эту схему не стоит воспринимать буквально: в отдельных проектах может отсутствовать Use Case, также state managеment может переходить из presenter в Use Case. Однако, слои остаются независимыми, что помогает упростить работу программиста. 

Потоком данных на схеме является набор информации, перетекающий из одной части приложения в другую.

Мы преобразуем одно из простых приложений на Flutter с использованием чистой архитектуры и с возможностью сохранения данных счетчика. Добавим функционал сохранения данных для того, чтобы наглядно показать реализацию data слоя чистой архитектуры.

Создание проекта

Если вы еще не работали с Flutter, вы можете воспользоваться инструкцией и создать проект с помощью IDE или командной строки:

flutter create myapp

Вы создаете проект с примером счетчика нажатий. После удаления комментариев и переноса части кода в home_page.screen.dart получаете проект с примерно такой структурой:

main.dart
import 'package:clean_arch_example_cubit/home_page_screen.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

home_page_screen.dart
import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Теперь необходимо написать кубит для управления состоянием home_page_screen, который будет лежать в директории domain

Перенесем _counter из home_page_screen.dart в home_page_state.dart, а функцию _incrementCounter() в home_page_cubit

Распределим файлы по директориям, и в результате наш проект будет выглядеть следующим образом:

home_page_screen.dart
import 'package:clean_arch_example_cubit/presentation/bloc/home_page/home_page_cubit.dart';
import 'package:clean_arch_example_cubit/presentation/bloc/home_page/home_page_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final HomePageCubit cubit =  HomePageCubit();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: BlocBuilder<HomePageCubit, HomePageState>(
        bloc: cubit,
        builder: (context, state) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${state.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          );
        },
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: cubit.incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

home_page_cubit.dart
import 'package:clean_arch_example_cubit/presentation/bloc/home_page/home_page_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomePageCubit extends Cubit<HomePageState> {
  HomePageCubit() : super(HomePageState(count: 0));

  void incrementCounter() {
    emit(HomePageState(count: state.count+1));
  }
}

home_page_state.dart
class HomePageState {
  final int count;

  const HomePageState({required this.count});
}

В home_page_state необходимо сделать все объекты final, для того чтобы не было возможности редактировать существующий стейт. В противном случае при попытке выполнить emit() с измененным стейтом в кубите не будет изменений на экране.

Теперь создадим слой для работы с данными.

Для этого необходимо создать директорию data с поддиректориями repository, который будет хранить как абстрактный класс репозитория, так и его имплементацию.

От репозитория требуется 2 действия: получить последнее сохраненное значение и записать значение в базу для последующего извлечения.

Для этого создадим 2 метода:

int getLastCount();

Future<void> saveCount(int count);

Чтобы в приложении появилась возможность сохранять количество нажатий на кнопку, воспользуемся библиотекой hive. Добавим 2 библиотеки для работы с Hive: hive и path_provider

Напишем реализацию для CounterRepositoryImpl.dart
import 'package:clean_arch_example_cubit/data/repository/interface/counter_repo.dart';
import 'package:hive/hive.dart';

class CounterRepositoryImpl extends CounterRepository {
  static const boxKey = 'counter';

  final Box box;

  CounterRepositoryImpl(this.box);

  @override
  int getLastCount() => box.get(boxKey, defaultValue: 0);

  @override
  Future<void> saveCount(int count) => box.put(boxKey, count);
}

Теперь нужно создать слой Domain с Use Case.

Для этого необходимо создать папку domain с use_cases, в которой мы выполним абстрактную часть и ее реализацию. Архитектура domain-слоя будет выглядеть следующим образом:

counter_case.dart содержит в себе абстрактную часть use case, в котором будет 2 метода для получения и сохранения значения счетчика

abstract class CounterCase{
  int getLastCount();

  Future<int> saveCount(int count);
}
Реализация (counter_case_imple.dart) будет выглядеть следующим образом:
import 'package:clean_arch_example_cubit/data/repository/interface/counter_repo.dart';
import 'package:clean_arch_example_cubit/domain/use_cases/interfaces/counter_case.dart';

class CounterCaseImpl extends CounterCase {
  final CounterRepository _counterRepository;

  CounterCaseImpl(this._counterRepository);

  @override
  int getLastCount() => _counterRepository.getLastCount();

  @override
  Future<int> saveCount(int count) => _counterRepository.saveCount(count);
}

Теперь добавим внедрение зависимостей. Для этого создадим класс-синглтон DI, в котором метод init будет реализовывать counterRepository, там же и сделаем инициализацию hive.

В итоге DI будет выглядеть следующим образом:
import 'package:clean_arch_example_cubit/data/repository/impl/counter_repo_impl.dart';
import 'package:clean_arch_example_cubit/data/repository/interface/counter_repo.dart';
import 'package:clean_arch_example_cubit/domain/use_cases/impl/counter_case_impl.dart';
import 'package:clean_arch_example_cubit/domain/use_cases/interfaces/counter_case.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';

class DI {
  static DI? instance;

  late CounterRepository counterRepository;
  late CounterCase counterCase;

  DI._();

  static DI getInstance() {
    return instance ?? (instance = DI._());
  }

  Future<void> init() async {
    final directory = await getApplicationSupportDirectory();
    Hive.init(directory.path);
    counterRepository = CounterRepositoryImpl(await Hive.openBox('counter'));
    counterCase = CounterCaseImpl(counterRepository);
  }
}

Инициализацию DI можно сделать через FutureBuilder при открытии приложения.

В итоге файл main.dart будет выглядеть следующим образом:
import 'package:clean_arch_example_cubit/di.dart';
import 'package:clean_arch_example_cubit/presentation/screen/home_page_screen.dart';
import 'package:flutter/material.dart';

void main() async {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FutureBuilder(
        future: DI.getInstance().init(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            return MyHomePage(title: 'Flutter Demo Home Page');
          }else{
            return const CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

Теперь необходимо дописать функционал инициализации home_page_cubit и обработку нажатия на кнопку прибавления счетчика

Код
import 'package:clean_arch_example_cubit/di.dart';
import 'package:clean_arch_example_cubit/presentation/bloc/home_page/home_page_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class HomePageCubit extends Cubit<HomePageState> {
  final counterCases = DI.getInstance().counterCase;

  HomePageCubit() : super(HomePageState(count: 0)) {
    emit(HomePageState(count: counterCases.getLastCount()));
  }

  Future<void> incrementCounter() async {
    final _savedValue = await counterCases.saveCount(state.count + 1);
    emit(HomePageState(count: _savedValue));
  }
}

Всё!

Теперь при запуске приложения происходит инициализация DI, в котором создается CounterRepository, CounterCase. После открывается home_page_screen, который инициализирует home_page_cubit, и тот загружает последнее сохраненное значение счетчика и показывает его на экране.

Логику работы кнопки увеличения счетчика можно представить на графике ниже:

Нажатие на кнопку вызывает incrementCounter у кубита, что приводит в действие метод saveCount у use case. Последний, в свою очередь, запускает метод saveCount у репозитория. Репозиторий сохранит в Hive значение, вернет в виде объекта Entity в home_page_cubit, который обновит стейт у home_page_screen. Так как метод put у Hive не возвращает никаких данных, поэтому в графике отсутствует стрелка от hive к counter_repo. Если бы, например, у нас был интернет-запрос, то от блока hive была бы стрелочка к counter_repo с результатом интернет-запроса. Познакомиться с проектом подробнее можно на GitHub

Спасибо за внимание! Надеемся, что этот пример был вам полезен.  

Авторские материалы для mobile-разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии15

Публикации

Информация

Сайт
www.simbirsoft.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия