Разбираемся с Clean Architecture в Flutter.

  • 25 сентября 2024
  • 42 просмотра
  • 0 комментариев

Clean Architecture, предложенная Робертом Мартином (Uncle Bob), — это подход к проектированию программного обеспечения, который акцентирует внимание на разделении обязанностей, тестируемости и поддерживаемости. При применении к разработке мобильных приложений на Flutter, цель заключается в том, чтобы разделить код на независимые слои, что упрощает его поддержку, масштабирование и тестирование.

Основные концепции Clean Architecture:

  1. Разделение обязанностей: Каждый слой имеет четко определённые функции, что снижает взаимозависимость между частями системы и улучшает поддерживаемость кода.

  2. Принцип инверсии зависимостей: Внутренние слои не должны зависеть от внешних. Напротив, внешние слои зависят от абстракций, определённых во внутренних слоях. Это правило обеспечивает изоляцию основной бизнес-логики от изменений в пользовательском интерфейсе или API.

  3. Тестируемость: Поскольку бизнес-логика отделена от пользовательского интерфейса и источников данных, её можно легко тестировать с помощью модульных тестов, что повышает общую надёжность приложения.

Слои Clean Architecture в Flutter:

  1. Entities (Сущности, доменный слой): Сущности — это основные бизнес-объекты приложения, которые содержат фундаментальные бизнес-правила. Эти сущности независимы от любых фреймворков, пользовательских интерфейсов или внешних источников данных.

  2. Use Cases (Приложение логики): Сценарии использования (Use Cases) инкапсулируют конкретные бизнес-правила и координируют поток данных между сущностями и репозиториями. Они определяют логику того, как система должна вести себя на основе взаимодействий с пользователем или внешних событий.

  3. Repositories (Абстракция для работы с данными): Репозитории предоставляют интерфейсы для получения и сохранения данных, абстрагируя реальный источник данных (например, сеть или локальное хранилище). Это позволяет использовать сценарии использования без необходимости знать, как или где хранятся данные.

  4. Data Layer (Слой данных): Слой данных содержит конкретные реализации репозиториев. Здесь происходит взаимодействие с сетью, базами данных и другими источниками данных. Этот слой управляет запросами к API, базам данных или даже с фальшивыми данными для тестирования.

  5. 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-разработчика можно тут!

    Оставьте отзыв
    (минимум 60 знаков)
    Оценка5/5
    Нужно авторизоваться