Соблюдать принципы чистой архитектуры – значит обеспечить удобство тестирования, поддержки и модернизации приложения. Понимание архитектуры и state management – это база, необходимая начинающему специалисту для успешной командной работы. В этой статье мы расскажем, как с помощью Cubit реализовать чистую архитектуру на примере стартового приложения Flutter – счетчика нажатий на кнопку.
![](https://habrastorage.org/getpro/habr/upload_files/b69/b59/3cf/b69b593cfca88690a8486ed4eeb307dc.png)
Подробнее о работе с фреймворком Flutter мы рассказывали в одной из прошлых статей. На данный момент на Flutter реализуют приложения для мобильных, веб-, настольных и встроенных устройств.
Библиотека Cubit предназначена для управления состоянием экрана и позволяет реализовать шаблон проектирования BLoC. С ее помощью можно упростить отделение презентации от бизнес-логики, тестирование и переиспользование кода.
Для начала отметим, что концепция чистой архитектуры, созданная Робертом Мартином, основана на выделении независимых слоев приложения:
![](https://habrastorage.org/getpro/habr/upload_files/af0/d66/15f/af0d6615f1dc82fd6ac6045863618d02.png)
Обычно приложение состоит из четырех слоев:
-
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 получаете проект с примерно такой структурой:
![](https://habrastorage.org/getpro/habr/upload_files/e78/64a/399/e7864a39973c45dfcc054c4ba4451387.png)
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
Распределим файлы по директориям, и в результате наш проект будет выглядеть следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/2cb/e46/e7a/2cbe46e7aa1af7234b95d34fceda5ee9.png)
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
![](https://habrastorage.org/getpro/habr/upload_files/714/10b/dcf/71410bdcfa69d7672acc97d8fb9e3036.png)
Напишем реализацию для 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-слоя будет выглядеть следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/713/074/337/71307433797a2652622a3d1abab2f6a3.png)
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, и тот загружает последнее сохраненное значение счетчика и показывает его на экране.
Логику работы кнопки увеличения счетчика можно представить на графике ниже:
![](https://habrastorage.org/getpro/habr/upload_files/cc0/116/d18/cc0116d1800d101436aeaec009aa57a6.png)
Нажатие на кнопку вызывает incrementCounter у кубита, что приводит в действие метод saveCount у use case. Последний, в свою очередь, запускает метод saveCount у репозитория. Репозиторий сохранит в Hive значение, вернет в виде объекта Entity в home_page_cubit, который обновит стейт у home_page_screen. Так как метод put у Hive не возвращает никаких данных, поэтому в графике отсутствует стрелка от hive к counter_repo. Если бы, например, у нас был интернет-запрос, то от блока hive была бы стрелочка к counter_repo с результатом интернет-запроса. Познакомиться с проектом подробнее можно на GitHub.
Спасибо за внимание! Надеемся, что этот пример был вам полезен.
Авторские материалы для mobile-разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.
ссылка на оригинал статьи https://habr.com/ru/articles/573848/
Добавить комментарий