Clean Architecture, предложенная Робертом Мартином (Uncle Bob), — это подход к проектированию программного обеспечения, который акцентирует внимание на разделении обязанностей, тестируемости и поддерживаемости. При применении к разработке мобильных приложений на Flutter, цель заключается в том, чтобы разделить код на независимые слои, что упрощает его поддержку, масштабирование и тестирование.
Основные концепции Clean Architecture:
-
Разделение обязанностей: Каждый слой имеет четко определённые функции, что снижает взаимозависимость между частями системы и улучшает поддерживаемость кода.
-
Принцип инверсии зависимостей: Внутренние слои не должны зависеть от внешних. Напротив, внешние слои зависят от абстракций, определённых во внутренних слоях. Это правило обеспечивает изоляцию основной бизнес-логики от изменений в пользовательском интерфейсе или API.
-
Тестируемость: Поскольку бизнес-логика отделена от пользовательского интерфейса и источников данных, её можно легко тестировать с помощью модульных тестов, что повышает общую надёжность приложения.
Слои Clean Architecture в Flutter:
-
Entities (Сущности, доменный слой): Сущности — это основные бизнес-объекты приложения, которые содержат фундаментальные бизнес-правила. Эти сущности независимы от любых фреймворков, пользовательских интерфейсов или внешних источников данных.
-
Use Cases (Приложение логики): Сценарии использования (Use Cases) инкапсулируют конкретные бизнес-правила и координируют поток данных между сущностями и репозиториями. Они определяют логику того, как система должна вести себя на основе взаимодействий с пользователем или внешних событий.
-
Repositories (Абстракция для работы с данными): Репозитории предоставляют интерфейсы для получения и сохранения данных, абстрагируя реальный источник данных (например, сеть или локальное хранилище). Это позволяет использовать сценарии использования без необходимости знать, как или где хранятся данные.
-
Data Layer (Слой данных): Слой данных содержит конкретные реализации репозиториев. Здесь происходит взаимодействие с сетью, базами данных и другими источниками данных. Этот слой управляет запросами к API, базам данных или даже с фальшивыми данными для тестирования.
-
Presentation Layer (Слой представления): Этот слой включает пользовательский интерфейс (виджеты в Flutter) и логику управления состоянием (с использованием инструментов вроде
Bloc
,Provider
илиRiverpod
). Он взаимодействует со сценариями использования для отображения данных и обработки пользовательских действий.
Пример проекта на тему "Книжный Магазин" на Flutter с использованием Clean Architecture:
Структура проекта:
lib/
├── core/
│ ├── error/
│ └── usecases/
├── features/
│ ├── book_store/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ ├── data/
│ │ │ ├── models/
│ │ │ ├── repositories/
│ │ │ └── datasources/
│ │ └── presentation/
│ │ ├── bloc/ (или provider)
│ │ └── pages/
└── main.dart
1. Domain Layer (Слой доменной логики)
Entity (Сущность Book
)
Файл: lib/features/book_store/domain/entities/book.dart
class Book {
final String id;
final String title;
final String author;
final String description;
Book({
required this.id,
required this.title,
required this.author,
required this.description,
});
}
Use Case (Получение списка книг)
Файл: lib/features/book_store/domain/usecases/get_books.dart
import '../entities/book.dart';
import '../repositories/book_repository.dart';
class GetBooks {
final BookRepository repository;
GetBooks(this.repository);
Future<List<Book>> call() async {
return await repository.getBooks();
}
}
Repository Interface (Интерфейс репозитория)
Файл: lib/features/book_store/domain/repositories/book_repository.dart
import '../entities/book.dart';
abstract class BookRepository {
Future<List<Book>> getBooks();
Future<Book> getBookDetails(String id);
}
2. Data Layer (Слой данных)
Model (Модель книги)
Файл: lib/features/book_store/data/models/book_model.dart
import '../../domain/entities/book.dart';
class BookModel extends Book {
BookModel({
required String id,
required String title,
required String author,
required String description,
}) : super(
id: id,
title: title,
author: author,
description: description,
);
factory BookModel.fromJson(Map<String, dynamic> json) {
return BookModel(
id: json['id'],
title: json['title'],
author: json['author'],
description: json['description'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'author': author,
'description': description,
};
}
}
Repository Implementation (Реализация репозитория)
Файл: lib/features/book_store/data/repositories/book_repository_impl.dart
import 'dart:convert';
import 'package:flutter/services.dart';
import '../../domain/entities/book.dart';
import '../../domain/repositories/book_repository.dart';
import '../models/book_model.dart';
class BookRepositoryImpl implements BookRepository {
@override
Future<List<Book>> getBooks() async {
final jsonString = await rootBundle.loadString('assets/books.json');
final List<dynamic> jsonResponse = json.decode(jsonString);
return jsonResponse.map((book) => BookModel.fromJson(book)).toList();
}
@override
Future<Book> getBookDetails(String id) async {
final books = await getBooks();
return books.firstWhere((book) => book.id == id);
}
}
3. Presentation Layer (Слой представления)
Book Bloc (с использованием flutter_bloc
)
Файл: lib/features/book_store/presentation/bloc/book_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/book.dart';
import '../../../domain/usecases/get_books.dart';
abstract class BookEvent {}
class LoadBooks extends BookEvent {}
class BookState {
final List<Book>? books;
final bool isLoading;
final String? error;
BookState({this.books, this.isLoading = false, this.error});
factory BookState.loading() => BookState(isLoading: true);
factory BookState.loaded(List<Book> books) => BookState(books: books);
factory BookState.error(String error) => BookState(error: error);
}
class BookBloc extends Bloc<BookEvent, BookState> {
final GetBooks getBooks;
BookBloc(this.getBooks) : super(BookState.loading()) {
on<LoadBooks>((event, emit) async {
emit(BookState.loading());
try {
final books = await getBooks();
emit(BookState.loaded(books));
} catch (e) {
emit(BookState.error(e.toString()));
}
});
}
}
UI (Экран списка книг)
Файл: lib/features/book_store/presentation/pages/book_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/book_bloc.dart';
class BookListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Book Store'),
),
body: BlocBuilder<BookBloc, BookState>(
builder: (context, state) {
if (state.isLoading) {
return Center(child: CircularProgressIndicator());
}
if (state.error != null) {
return Center(child: Text('Error: ${state.error}'));
}
return ListView.builder(
itemCount: state.books?.length ?? 0,
itemBuilder: (context, index) {
final book = state.books![index];
return ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () {
// переход к деталям книги
},
);
},
);
},
),
);
}
}
4. Настройка main.dart
Файл: lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'features/book_store/presentation/bloc/book_bloc.dart';
import 'features/book_store/data/repositories/book_repository_impl.dart';
import 'features/book_store/domain/usecases/get_books.dart';
import 'features/book_store/presentation/pages/book_list_page.dart';
void main() {
final bookRepository = BookRepositoryImpl();
runApp(BookStoreApp(bookRepository: bookRepository));
}
class BookStoreApp extends StatelessWidget {
final BookRepositoryImpl bookRepository;
BookStoreApp({required this.bookRepository});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (context) => BookBloc(GetBooks(bookRepository))..add(LoadBooks()),
child: BookListPage(),
),
);
}
}
Пример данных books.json
Файл: assets/books.json
[
{
"id": "1",
"title": "Clean Code",
"author": "Robert C. Martin",
"description": "A Handbook of Agile Software Craftsmanship."
},
{
"id": "2",
"title": "The Pragmatic Programmer",
"author": "Andrew Hunt, David Thomas",
"description": "Your journey to mastery."
}
]
Основные моменты:
- Слой данных: Реализует хранение данных (в данном примере через локальный JSON).
- Use Case: Инкапсулирует логику получения данных (списка книг) и используется в блоке.
- Presentation Layer: Отвечает за отображение списка книг с помощью
Bloc
.
Этот пример демонстрирует, как можно использовать Clean Architecture в приложении "Книжный магазин" на Flutter.
Что в итоге?
Преимущества Clean Architecture:
- Масштабируемость: Новые функции можно добавлять, не влияя на существующий код.
- Тестируемость: Каждый слой может быть протестирован независимо, особенно бизнес-логика, которая отделена от пользовательского интерфейса и источников данных.
- Поддерживаемость: Чёткое разделение обязанностей ведёт к улучшению поддерживаемости кода.
- Модульность: Легче заменять компоненты или слои (например, сменить базу данных или API) без влияния на остальную часть системы.
Недостатки:
- Сложность: Для небольших приложений подход с многослойной архитектурой может показаться избыточным.
- Избыточные накладные расходы: Дополнительные уровни абстракции могут увеличить время разработки для простых проектов.
Заключение:
Clean Architecture в Flutter — это надёжный способ структурирования сложных приложений. Она позволяет независимо разрабатывать различные части приложения, способствует повторному использованию кода и гарантирует, что изменения в пользовательском интерфейсе, источниках данных или фреймворках не затронут основную бизнес-логику. Однако для небольших проектов такая архитектура может показаться слишком сложной.
Посмотреть подходящие вакансии для Flutter-разработчика можно тут!