TOC
What is the purpose of layering the architecture in a Flutter project? How should it be structured?
1.The purpose of layering the architecture in a Flutter project
The purpose of layering the architecture in a Flutter project is to enhance code maintainability, promote a clear separation of concerns, promote reusability, improve testability, and enhance scalability.
2.What is maintainability?
3.Why does layering the archetecture encourage maintainability?
-
Separation of Concerns (SoC): By separating the codebase into distinct layers, each layer has a specific responsibility and focus. The presentation layer handles UI concerns, the business logic layer encapsulates the core functionality, and the data layer manages data access. This separation makes it easier to understand, modify, and test each layer independently, reducing the risk of unintended side effects when making changes.
-
Code Organization: A layered architecture provides a well-defined structure for organizing the codebase. With clear boundaries and responsibilities for each layer, developers can easily navigate and understand the codebase, even in large projects. This organization makes it easier to locate and modify specific parts of the application without impacting other areas.
-
Code Reusability: By separating concerns into layers, reusable components and modules can be identified and shared across different parts of the application or even across multiple projects. For example, the business logic layer can be reused with different presentation layers (e.g., mobile and web), and the data layer can be shared among different features or modules.
-
Scalability: As the application grows in complexity, a layered architecture ensures that new features or modules can be added without significantly impacting existing code. New features can be developed within their respective layers, adhering to the established boundaries and interfaces, minimizing the risk of introducing bugs or breaking existing functionality.
-
Testability: A layered architecture promotes testability by allowing each layer to be tested independently. Unit tests can be written for the business logic layer without involving the presentation or data layers, and integration tests can be created to verify the interactions between layers. This focused testing approach makes it easier to identify and fix issues, reducing the risk of regressions and improving overall code quality.
-
Team Collaboration: A well-structured layered architecture facilitates collaboration among team members. Developers can work on different layers or modules concurrently, reducing conflicts and merge issues. Clear boundaries and interfaces between layers also make it easier for team members to understand and contribute to specific parts of the codebase.
By promoting these qualities, a layered architecture in a Flutter project enhances maintainability, making it easier to understand, modify, and extend the codebase over time, ultimately reducing the long-term costs of development and maintenance.
4.How to layer the project?
Here’s a common way to structure a layered architecture in a Flutter project:
-
Presentation Layer:
- This layer contains the UI components, widgets, screens, and their associated logic.
- It handles user interactions, renders the UI, and communicates with the business logic layer.
- This layer should be UI-centric and avoid containing complex business logic.
-
Business Logic Layer:
- This layer encapsulates the core business rules and use cases of the application.
- It acts as an intermediary between the presentation layer and the data layer.
- It receives requests from the presentation layer, processes the data, and returns the results.
-
Data Layer:
- This layer is responsible for handling data operations, such as fetching data from APIs, databases, or other sources.
- It abstracts the data access logic and provides a consistent interface to the business logic layer.
- It can include repositories, data sources, and data models.
-
Core Layer (optional):
- This layer contains utilities, constants, shared preferences, and other reusable modules that can be used across different layers.
- It provides a central place for shared functionality, reducing code duplication.
Here’s a typical directory structure for a layered Flutter project:
lib/
├── core/
│ ├── constants/
│ ├── utils/
│ ├── preferences/
│ └── ...
├── data/
│ ├── models/
│ ├── repositories/
│ ├── data_sources/
│ └── ...
├── domain/
│ ├── entities/
│ ├── repositories/
│ ├── use_cases/
│ └── ...
├── presentation/
│ ├── pages/
│ ├── widgets/
│ ├── blocs/
│ ├── view_models/
│ └── ...
├── main.dart
└── ...
This structure promotes separation of concerns, code reusability, and testability. The presentation layer handles the UI, the business logic layer encapsulates the core functionality, and the data layer manages data access. The core layer (if included) contains shared utilities and modules.
It’s important to note that the specific implementation and naming conventions may vary based on the architectural pattern you choose (e.g., BLoC, MVC, or MVVM) and the project’s requirements. Additionally, you can further organize the layers into smaller modules or features for better scalability and maintainability in larger projects.
4.1 Why does presentation have so many sub folders?
The presentation
folder represents the user interface layer in a layered architecture for a Flutter project. It contains the components responsible for rendering the UI and handling user interactions. Here’s an explanation of the subfolders within the presentation
folder:
-
pages:
- This folder contains the individual screens or pages of the application.
- Each page is typically represented as a separate widget or a stateful/stateless widget class.
- Pages are responsible for composing the UI, managing the layout, and handling user interactions specific to that screen.
- Examples of pages might include
HomeScreen
,ProductDetailScreen
, orCheckoutScreen
.
-
widgets:
- This folder contains reusable UI components or widgets that can be shared across multiple pages or screens.
- Widgets can range from simple UI elements like buttons or inputs to more complex components like lists or carousels.
- By separating widgets into their own folder, you promote code reusability and maintainability.
- Examples of widgets might include
ProductCard
,LoadingIndicator
, orCustomAppBar
.
-
blocs (or view_models):
- This folder contains the presentation logic components that manage the state and business logic for the UI.
- In the BLoC (Business Logic Component) pattern, this folder would contain the BLoC classes responsible for handling events, updating state, and providing data to the UI.
- In other architectural patterns like MVVM (Model-View-ViewModel), this folder might be named
view_models
and contain the ViewModel classes that encapsulate the presentation logic. - These components act as intermediaries between the UI and the domain layer, exposing data and functionality to the UI components.
-
view_models (or blocs):
- Depending on the architectural pattern you choose, you might have a separate folder for view models or BLoC components.
- In the MVVM pattern, this folder would contain the ViewModel classes that encapsulate the presentation logic and provide data and commands to the UI components.
- In the BLoC pattern, this folder might be named
blocs
and contain the BLoC classes responsible for managing state and business logic.
The separation of concerns within the presentation
folder allows for better organization, testability, and reusability of UI components. Pages encapsulate the screen-specific logic, widgets are reusable UI building blocks, and the BLoC/ViewModel components handle the presentation logic and state management.
By structuring the presentation layer in this way, you can maintain a clear separation between the UI components and the underlying business logic, promoting code maintainability and making it easier to develop, test, and modify the user interface independently from other layers of the application.
4.2 What differences about data
and domain
folders?
The data
and domain
folders serve different purposes in a layered architecture, reflecting the separation of concerns between the data access layer and the business logic layer. Here are the key differences between them:
Domain Layer:
-
Entities: The
entities
folder in the domain layer contains the pure business models or entities that represent the core data structures and logic of the application. These entities are independent of any specific data source or persistence mechanism. -
Repositories (Interfaces): The
repositories
folder in the domain layer defines the interfaces or abstract classes for data access operations. These repositories act as abstractions that decouple the business logic from the data access layer. -
Use Cases: The
use_cases
folder contains the implementation of the business rules and application logic. Use cases orchestrate the flow of data between entities and repositories, encapsulating the core functionality of the application.
The domain layer is concerned with the pure business logic and is completely independent of any implementation details related to data access or presentation.
Data Layer:
-
Models: The
models
folder in the data layer contains the data models or data transfer objects (DTOs) that represent the structure of data received from or sent to external data sources, such as APIs or databases. These models are typically different from the domain entities and may contain additional data source-specific information. -
Repositories (Implementations): The
repositories
folder in the data layer contains the concrete implementations of the repository interfaces defined in the domain layer. These implementations handle the actual data access logic, such as making API calls, interacting with databases, or performing file operations. -
Data Sources: The
data_sources
folder contains classes or modules responsible for fetching and persisting data from various sources, such as APIs, databases, or local storage. These data sources are typically used by the repository implementations in the data layer.
The data layer is responsible for managing the communication with external data sources, mapping the data between the data models and domain entities, and providing concrete implementations of the repository interfaces defined in the domain layer.
The main difference between the data
and domain
layers lies in their responsibilities:
- The domain layer encapsulates the core business logic, entities, and use cases, and remains independent of any data access or presentation concerns.
- The data layer handles the data access and persistence logic, providing implementations of the repository interfaces defined in the domain layer, and managing the mapping between data models and domain entities.
This separation of concerns promotes modularity, testability, and maintainability of the codebase. It allows the business logic to be decoupled from the data access and presentation layers, enabling easier testing, reusability, and future modifications or extensions of each layer independently.
4.3 An example in a Flutter app using a layered architecture.
- Domain Layer:
Entities:
// lib/domain/entities/product.dart
class Product {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
Repositories:
// lib/domain/repositories/product_repository.dart
import '../entities/product.dart';
abstract class ProductRepository {
Future<List<Product>> getProducts();
}
Use Cases:
// lib/domain/use_cases/get_products.dart
import '../entities/product.dart';
import '../repositories/product_repository.dart';
class GetProducts {
final ProductRepository _repository;
GetProducts(this._repository);
Future<List<Product>> call() async {
return await _repository.getProducts();
}
}
- Data Layer:
Models:
// lib/data/models/product_model.dart
class ProductModel {
final int id;
final String name;
final double price;
ProductModel({required this.id, required this.name, required this.price});
factory ProductModel.fromJson(Map<String, dynamic> json) {
return ProductModel(
id: json['id'],
name: json['name'],
price: json['price'].toDouble(),
);
}
}
Repositories:
// lib/data/repositories/product_repository_impl.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../models/product_model.dart';
class ProductRepositoryImpl implements ProductRepository {
final String _baseUrl = 'https://api.example.com/products';
@override
Future<List<Product>> getProducts() async {
final response = await http.get(Uri.parse(_baseUrl));
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
final products = jsonList.map((json) => ProductModel.fromJson(json)).toList();
return products.map((model) => Product(id: model.id, name: model.name, price: model.price)).toList();
} else {
throw Exception('Failed to load products');
}
}
}
- Presentation Layer:
// lib/presentation/pages/product_list_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/product.dart';
import '../view_models/product_list_view_model.dart';
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ProductListViewModel(),
child: Scaffold(
appBar: AppBar(
title: Text('Product List'),
),
body: Consumer<ProductListViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return Center(child: CircularProgressIndicator());
} else if (viewModel.errorMessage.isNotEmpty) {
return Center(child: Text(viewModel.errorMessage));
} else {
return ListView.builder(
itemCount: viewModel.products.length,
itemBuilder: (context, index) {
final product = viewModel.products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
);
},
);
}
},
),
),
);
}
}
// lib/presentation/view_models/product_list_view_model.dart
import 'package:flutter/material.dart';
import '../../domain/entities/product.dart';
import '../../domain/use_cases/get_products.dart';
import '../../data/repositories/product_repository_impl.dart';
class ProductListViewModel extends ChangeNotifier {
final GetProducts _getProducts;
ProductListViewModel() : _getProducts = GetProducts(ProductRepositoryImpl());
List<Product> _products = [];
List<Product> get products => _products;
bool _isLoading = false;
bool get isLoading => _isLoading;
String _errorMessage = '';
String get errorMessage => _errorMessage;
Future<void> fetchProducts() async {
_isLoading = true;
notifyListeners();
try {
_products = await _getProducts();
} catch (e) {
_errorMessage = 'Failed to fetch products: $e';
}
_isLoading = false;
notifyListeners();
}
}
In this example, we have separated the concerns into different layers:
- Domain Layer: Defines the
Product
entity, theProductRepository
interface, and theGetProducts
use case. - Data Layer: Implements the
ProductRepositoryImpl
class, which fetches product data from an API and maps it to theProductModel
data model. - Presentation Layer: Contains the
ProductListPage
widget and theProductListViewModel
class, which fetches the products using theGetProducts
use case and exposes the data to the UI.
The ProductListPage
uses the ChangeNotifierProvider
from the provider
package to provide the ProductListViewModel
to the widget tree. The ProductListViewModel
class fetches the products by calling the GetProducts
use case, which in turn uses the ProductRepositoryImpl
implementation to fetch the data from the API.
The ProductListPage
widget then displays a ListView
with the fetched products, showing the name and price of each product. It also handles loading and error states based on the values exposed by the ProductListViewModel
.
This example demonstrates how the layered architecture separates concerns, promotes code reusability, and facilitates testing by isolating the business logic, data access, and presentation logic into distinct layers.
「点个赞」
点个赞
使用微信扫描二维码完成支付
