架构设计模式:Clean Architecture 实践
一、Clean Architecture 概述
1.1 什么是 Clean Architecture
Clean Architecture(简称 CA)是由 Robert C. Martin(Uncle Bob)提出的一种软件架构模式,旨在创建一个独立于框架、UI、数据库和任何外部代理的系统。它通过分离关注点来实现高度可测试、可维护和可扩展的代码库。
在 Flutter 应用开发中,Clean Architecture 的核心价值在于:
Clean Architecture 架构模式在 Flutter 开发中的应用。阐述了其核心原则如依赖规则、关注点分离及依赖倒置。详细划分了表示层、领域层和数据层的职责与依赖关系。通过项目结构组织、实体、用例、仓库接口及数据模型的具体实现示例,展示了如何在 Flutter 中落地该架构。结合 BLoC 状态管理与 GetIt 依赖注入,构建了一个新闻阅读应用的完整案例。最后解析了常见面试题,强调了分层架构对可测试性、可维护性及适应变化的优势,建议根据项目规模灵活调整。
Clean Architecture(简称 CA)是由 Robert C. Martin(Uncle Bob)提出的一种软件架构模式,旨在创建一个独立于框架、UI、数据库和任何外部代理的系统。它通过分离关注点来实现高度可测试、可维护和可扩展的代码库。
在 Flutter 应用开发中,Clean Architecture 的核心价值在于:
Clean Architecture 基于以下几个核心原则:
在 Flutter 开发中应用 Clean Architecture 有以下几个显著优势:
Clean Architecture 在 Flutter 中通常分为以下几层:
在 Clean Architecture 中,依赖关系是单向的,从外层指向内层:
表示层 → 领域层 ← 数据层
这种依赖关系通过依赖倒置原则(DIP)实现,即高层模块(领域层)定义接口,低层模块(数据层)实现这些接口。
在 Clean Architecture 中,数据流通常遵循以下路径:
一个遵循 Clean Architecture 的 Flutter 项目结构可能如下:
lib/
├── core/ # 核心工具和通用功能
│ ├── error/ # 错误处理
│ ├── network/ # 网络相关
│ └── utils/ # 工具类
├── data/ # 数据层
│ ├── datasources/ # 数据源
│ │ ├── local/ # 本地数据源
│ │ └── remote/ # 远程数据源
│ ├── models/ # 数据模型
│ └── repositories/ # 仓库实现
├── domain/ # 领域层
│ ├── entities/ # 业务实体
│ ├── repositories/ # 仓库接口
│ └── usecases/ # 用例
├── presentation/ # 表示层
│ ├── bloc/ # 状态管理
│ ├── pages/ # 页面
│ └── widgets/ # 可复用组件
├── di/ # 依赖注入
└── main.dart # 应用入口
领域层是 Clean Architecture 的核心,包含业务实体、用例和仓库接口。
实体代表业务对象和业务规则。它们是纯 Dart 类,不依赖于任何框架。
// domain/entities/article.dart
class Article {
final int id;
final String title;
final String content;
final String author;
final DateTime publishDate;
Article({
required this.id,
required this.title,
required this.content,
required this.author,
required this.publishDate,
});
}
仓库接口定义了领域层期望数据层提供的功能。
// domain/repositories/article_repository.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../../core/error/failures.dart';
abstract class ArticleRepository {
Future<Either<Failure, List<Article>>> getArticles();
Future<Either<Failure, Article>> getArticleById(int id);
Future<Either<Failure, void>> saveArticle(Article article);
}
这里使用了 dartz 包的 Either 类型来处理错误,左侧表示失败,右侧表示成功。
用例封装了特定的业务逻辑,每个用例通常只做一件事。
// domain/usecases/get_articles.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
class GetArticles {
final ArticleRepository repository;
GetArticles(this.repository);
Future<Either<Failure, List<Article>>> call() {
return repository.getArticles();
}
}
// domain/usecases/toggle_favorite_article.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
class ToggleFavoriteArticle implements UseCase<void, Article> {
final ArticleRepository repository;
ToggleFavoriteArticle(this.repository);
@override
Future<Either<Failure, void>> call(Article article) {
final updatedArticle = article.copyWith(isFavorite: !article.isFavorite);
return repository.saveArticle(updatedArticle);
}
}
// domain/usecases/get_article_by_id.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
class GetArticleById implements UseCase<Article, int> {
final ArticleRepository repository;
GetArticleById(this.repository);
@override
Future<Either<Failure, Article>> call(int id) {
return repository.getArticleById(id);
}
}
数据层负责从各种来源获取数据并将其转换为领域实体。
数据模型是实体的具体实现,通常包含序列化和反序列化逻辑。
// data/models/article_model.dart
import '../../domain/entities/article.dart';
class ArticleModel extends Article {
ArticleModel({
required int id,
required String title,
required String content,
required String author,
required DateTime publishDate,
}) : super(
id: id,
title: title,
content: content,
author: author,
publishDate: publishDate,
);
factory ArticleModel.fromJson(Map<String, dynamic> json) {
return ArticleModel(
id: json['id'],
title: json['title'],
content: json['content'],
author: json['author'],
publishDate: DateTime.parse(json['publish_date']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'author': author,
'publish_date': publishDate.toIso8601String(),
};
}
}
数据源负责从特定来源(如 API 或本地数据库)获取数据。
// data/datasources/remote/article_remote_data_source.dart
import '../models/article_model.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
abstract class ArticleRemoteDataSource {
Future<List<ArticleModel>> getArticles();
Future<ArticleModel> getArticleById(int id);
}
class ArticleRemoteDataSourceImpl implements ArticleRemoteDataSource {
final http.Client client;
ArticleRemoteDataSourceImpl({required this.client});
@override
Future<List<ArticleModel>> getArticles() async {
final response = await client.get(
Uri.parse('https://api.example.com/articles'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => ArticleModel.fromJson(json)).toList();
} else {
throw ServerException();
}
}
@override
Future<ArticleModel> getArticleById(int id) async {
final response = await client.get(
Uri.parse('https://api.example.com/articles/$id'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return ArticleModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
}
仓库实现负责协调不同的数据源并实现领域层定义的仓库接口。
// data/repositories/article_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../domain/entities/article.dart';
import '../../domain/repositories/article_repository.dart';
import '../datasources/remote/article_remote_data_source.dart';
import '../datasources/local/article_local_data_source.dart';
import '../../core/error/failures.dart';
import '../../core/network/network_info.dart';
class ArticleRepositoryImpl implements ArticleRepository {
final ArticleRemoteDataSource remoteDataSource;
final ArticleLocalDataSource localDataSource;
final NetworkInfo networkInfo;
ArticleRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, List<Article>>> getArticles() async {
if (await networkInfo.isConnected) {
try {
final remoteArticles = await remoteDataSource.getArticles();
localDataSource.cacheArticles(remoteArticles);
return Right(remoteArticles);
} on ServerException {
return Left(ServerFailure());
}
} else {
try {
final localArticles = await localDataSource.getLastArticles();
return Right(localArticles);
} on CacheException {
return Left(CacheFailure());
}
}
}
@override
Future<Either<Failure, Article>> getArticleById(int id) async {
// 类似的实现逻辑
}
@override
Future<Either<Failure, void>> saveArticle(Article article) async {
// 实现保存文章的逻辑
}
}
表示层负责 UI 和用户交互,在 Flutter 中通常使用 BLoC、Provider 或其他状态管理解决方案。
// presentation/bloc/article/article_event.dart
import 'package:equatable/equatable.dart';
abstract class ArticleEvent extends Equatable {
@override
List<Object> get props => [];
}
class GetArticlesEvent extends ArticleEvent {}
class GetArticleByIdEvent extends ArticleEvent {
final int id;
GetArticleByIdEvent(this.id);
@override
List<Object> get props => [id];
}
// presentation/bloc/article/article_state.dart
import 'package:equatable/equatable.dart';
import '../../../domain/entities/article.dart';
abstract class ArticleState extends Equatable {
@override
List<Object> get props => [];
}
class ArticleInitial extends ArticleState {}
class ArticleLoading extends ArticleState {}
class ArticlesLoaded extends ArticleState {
final List<Article> articles;
ArticlesLoaded(this.articles);
@override
List<Object> get props => [articles];
}
class ArticleLoaded extends ArticleState {
final Article article;
ArticleLoaded(this.article);
@override
List<Object> get props => [article];
}
class ArticleError extends ArticleState {
final String message;
ArticleError(this.message);
@override
List<Object> get props => [message];
}
// presentation/bloc/article/article_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_articles.dart';
import '../../../domain/usecases/get_article_by_id.dart';
import 'article_event.dart';
import 'article_state.dart';
class ArticleBloc extends Bloc<ArticleEvent, ArticleState> {
final GetArticles getArticles;
final GetArticleById getArticleById;
ArticleBloc({
required this.getArticles,
required this.getArticleById,
}) : super(ArticleInitial()) {
on<GetArticlesEvent>(_onGetArticles);
on<GetArticleByIdEvent>(_onGetArticleById);
}
Future<void> _onGetArticles(GetArticlesEvent event, Emitter<ArticleState> emit) async {
emit(ArticleLoading());
final result = await getArticles();
result.fold(
(failure) => emit(ArticleError(_mapFailureToMessage(failure))),
(articles) => emit(ArticlesLoaded(articles)),
);
}
Future<void> _onGetArticleById(GetArticleByIdEvent event, Emitter<ArticleState> emit) async {
emit(ArticleLoading());
final result = await getArticleById(event.id);
result.fold(
(failure) => emit(ArticleError(_mapFailureToMessage(failure))),
(article) => emit(ArticleLoaded(article)),
);
}
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return '服务器错误';
case CacheFailure:
return '缓存错误';
default:
return '未知错误';
}
}
}
// presentation/pages/articles_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/article/article_bloc.dart';
import '../bloc/article/article_event.dart';
import '../bloc/article/article_state.dart';
import '../widgets/article_list_item.dart';
class ArticlesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('文章列表')),
body: BlocBuilder<ArticleBloc, ArticleState>(
builder: (context, state) {
if (state is ArticleInitial) {
BlocProvider.of<ArticleBloc>(context).add(GetArticlesEvent());
return Center(child: Text('加载中...'));
} else if (state is ArticleLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ArticlesLoaded) {
return ListView.builder(
itemCount: state.articles.length,
itemBuilder: (context, index) {
return ArticleListItem(article: state.articles[index]);
},
);
} else if (state is ArticleError) {
return Center(child: Text(state.message));
} else {
return Center(child: Text('未知状态'));
}
},
),
);
}
}
依赖注入是 Clean Architecture 的重要组成部分,它使各层之间的依赖关系更加清晰和可测试。在 Flutter 中,可以使用 get_it 包实现依赖注入。
// di/injection_container.dart
import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http;
import 'package:internet_connection_checker/internet_connection_checker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../data/datasources/local/article_local_data_source.dart';
import '../data/datasources/remote/article_remote_data_source.dart';
import '../data/repositories/article_repository_impl.dart';
import '../domain/repositories/article_repository.dart';
import '../domain/usecases/get_articles.dart';
import '../domain/usecases/get_article_by_id.dart';
import '../presentation/bloc/article/article_bloc.dart';
import '../core/network/network_info.dart';
final sl = GetIt.instance;
Future<void> init() async {
// Bloc
sl.registerFactory(() => ArticleBloc(
getArticles: sl(),
getArticleById: sl(),
));
// Use cases
sl.registerLazySingleton(() => GetArticles(sl()));
sl.registerLazySingleton(() => GetArticleById(sl()));
// Repository
sl.registerLazySingleton<ArticleRepository>(() => ArticleRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
));
// Data sources
sl.registerLazySingleton<ArticleRemoteDataSource>(() => ArticleRemoteDataSourceImpl(client: sl()));
sl.registerLazySingleton<ArticleLocalDataSource>(() => ArticleLocalDataSourceImpl(sharedPreferences: sl()));
// Core
sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl(sl()));
// External
final sharedPreferences = await SharedPreferences.getInstance();
sl.registerLazySingleton(() => sharedPreferences);
sl.registerLazySingleton(() => http.Client());
sl.registerLazySingleton(() => InternetConnectionChecker());
}
我们将构建一个简单的新闻阅读应用,具有以下功能:
首先,我们需要定义领域实体和用例:
// domain/entities/article.dart
class Article {
final int id;
final String title;
final String content;
final String author;
final DateTime publishDate;
final String imageUrl;
final bool isFavorite;
Article({
required this.id,
required this.title,
required this.content,
required this.author,
required this.publishDate,
required this.imageUrl,
this.isFavorite = false,
});
Article copyWith({
int? id,
String? title,
String? content,
String? author,
DateTime? publishDate,
String? imageUrl,
bool? isFavorite,
}) {
return Article(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
author: author ?? this.author,
publishDate: publishDate ?? this.publishDate,
imageUrl: imageUrl ?? this.imageUrl,
isFavorite: isFavorite ?? this.isFavorite,
);
}
}
// domain/usecases/get_articles.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
class GetArticles implements UseCase<List<Article>, NoParams> {
final ArticleRepository repository;
GetArticles(this.repository);
@override
Future<Either<Failure, List<Article>>> call(NoParams params) {
return repository.getArticles();
}
}
// domain/usecases/toggle_favorite_article.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
class ToggleFavoriteArticle implements UseCase<void, Article> {
final ArticleRepository repository;
ToggleFavoriteArticle(this.repository);
@override
Future<Either<Failure, void>> call(Article article) {
final updatedArticle = article.copyWith(isFavorite: !article.isFavorite);
return repository.saveArticle(updatedArticle);
}
}
// domain/usecases/get_article_by_id.dart
import 'package:dartz/dartz.dart';
import '../entities/article.dart';
import '../repositories/article_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecases/usecase.dart';
class GetArticleById implements UseCase<Article, int> {
final ArticleRepository repository;
GetArticleById(this.repository);
@override
Future<Either<Failure, Article>> call(int id) {
return repository.getArticleById(id);
}
}
// data/models/article_model.dart
import '../../domain/entities/article.dart';
class ArticleModel extends Article {
ArticleModel({
required int id,
required String title,
required String content,
required String author,
required DateTime publishDate,
required String imageUrl,
bool isFavorite = false,
}) : super(
id: id,
title: title,
content: content,
author: author,
publishDate: publishDate,
imageUrl: imageUrl,
isFavorite: isFavorite,
);
factory ArticleModel.fromJson(Map<String, dynamic> json) {
return ArticleModel(
id: json['id'],
title: json['title'],
content: json['content'],
author: json['author'],
publishDate: DateTime.parse(json['publish_date']),
imageUrl: json['image_url'],
isFavorite: json['is_favorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'author': author,
'publish_date': publishDate.toIso8601String(),
'image_url': imageUrl,
'is_favorite': isFavorite,
};
}
}
// data/repositories/article_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../domain/entities/article.dart';
import '../../domain/repositories/article_repository.dart';
import '../../core/error/failures.dart';
class ArticleRepositoryImpl implements ArticleRepository {
// 模拟数据
final List<Article> _articles = [
Article(
id: 1,
title: 'Flutter Clean Architecture',
content: 'Clean Architecture is a software design philosophy...',
author: 'Robert C. Martin',
publishDate: DateTime.now(),
imageUrl: 'https://example.com/clean_arch.png',
),
Article(
id: 2,
title: 'Dart 3.0 Features',
content: 'Dart 3.0 introduces patterns, records, and class modifiers...',
author: 'Dart Team',
publishDate: DateTime.now().subtract(Duration(days: 2)),
imageUrl: 'https://example.com/dart.png',
),
];
@override
Future<Either<Failure, List<Article>>> getArticles() async {
// 模拟网络延迟
await Future.delayed(Duration(milliseconds: 800));
return Right(_articles);
}
@override
Future<Either<Failure, Article>> getArticleById(int id) async {
await Future.delayed(Duration(milliseconds: 500));
try {
final article = _articles.firstWhere((element) => element.id == id);
return Right(article);
} catch (e) {
return Left(ServerFailure());
}
}
@override
Future<Either<Failure, void>> saveArticle(Article article) async {
await Future.delayed(Duration(milliseconds: 500));
final index = _articles.indexWhere((element) => element.id == article.id);
if (index != -1) {
_articles[index] = article;
return Right(null);
} else {
return Left(ServerFailure());
}
}
}
// presentation/bloc/article_list/article_list_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_articles.dart';
import '../../../core/usecases/usecase.dart';
import 'article_list_event.dart';
import 'article_list_state.dart';
class ArticleListBloc extends Bloc<ArticleListEvent, ArticleListState> {
final GetArticles getArticles;
ArticleListBloc({required this.getArticles}) : super(ArticleListInitial()) {
on<GetArticlesEvent>(_onGetArticles);
}
Future<void> _onGetArticles(GetArticlesEvent event, Emitter<ArticleListState> emit) async {
emit(ArticleListLoading());
final result = await getArticles(NoParams());
result.fold(
(failure) => emit(ArticleListError('加载失败')),
(articles) => emit(ArticleListLoaded(articles)),
);
}
}
// presentation/bloc/article_detail/article_detail_event.dart
abstract class ArticleDetailEvent {}
class GetArticleDetailEvent extends ArticleDetailEvent {
final int id;
GetArticleDetailEvent(this.id);
}
class ToggleFavoriteEvent extends ArticleDetailEvent {
final Article article;
ToggleFavoriteEvent(this.article);
}
// presentation/bloc/article_detail/article_detail_state.dart
abstract class ArticleDetailState {}
class ArticleDetailInitial extends ArticleDetailState {}
class ArticleDetailLoading extends ArticleDetailState {}
class ArticleDetailLoaded extends ArticleDetailState {
final Article article;
ArticleDetailLoaded(this.article);
}
class ArticleDetailError extends ArticleDetailState {
final String message;
ArticleDetailError(this.message);
}
// presentation/bloc/article_detail/article_detail_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/usecases/get_article_by_id.dart';
import '../../../domain/usecases/toggle_favorite_article.dart';
import 'article_detail_event.dart';
import 'article_detail_state.dart';
class ArticleDetailBloc extends Bloc<ArticleDetailEvent, ArticleDetailState> {
final GetArticleById getArticleById;
final ToggleFavoriteArticle toggleFavoriteArticle;
ArticleDetailBloc({
required this.getArticleById,
required this.toggleFavoriteArticle,
}) : super(ArticleDetailInitial()) {
on<GetArticleDetailEvent>(_onGetArticleDetail);
on<ToggleFavoriteEvent>(_onToggleFavorite);
}
Future<void> _onGetArticleDetail(
GetArticleDetailEvent event,
Emitter<ArticleDetailState> emit,
) async {
emit(ArticleDetailLoading());
final result = await getArticleById(event.id);
result.fold(
(failure) => emit(ArticleDetailError('加载失败')),
(article) => emit(ArticleDetailLoaded(article)),
);
}
Future<void> _onToggleFavorite(
ToggleFavoriteEvent event,
Emitter<ArticleDetailState> emit,
) async {
if (state is ArticleDetailLoaded) {
final result = await toggleFavoriteArticle(event.article);
result.fold(
(failure) => emit(ArticleDetailError('操作失败')),
(_) {
final updatedArticle = event.article.copyWith(
isFavorite: !event.article.isFavorite,
);
emit(ArticleDetailLoaded(updatedArticle));
},
);
}
}
}
// presentation/pages/article_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/article_list/article_list_bloc.dart';
import '../bloc/article_list/article_list_event.dart';
import '../bloc/article_list/article_list_state.dart';
import 'article_detail_page.dart';
class ArticleListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('每日新闻')),
body: BlocProvider(
create: (_) => sl<ArticleListBloc>()..add(GetArticlesEvent()),
child: BlocBuilder<ArticleListBloc, ArticleListState>(
builder: (context, state) {
if (state is ArticleListLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ArticleListLoaded) {
return ListView.builder(
itemCount: state.articles.length,
itemBuilder: (context, index) {
final article = state.articles[index];
return ListTile(
leading: CircleAvatar(child: Text(article.author[0])),
title: Text(article.title),
subtitle: Text(article.publishDate.toString().substring(0, 10)),
trailing: Icon(
article.isFavorite ? Icons.favorite : Icons.favorite_border,
color: article.isFavorite ? Colors.red : null,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ArticleDetailPage(articleId: article.id),
),
).then((_) {
// 返回时刷新列表
context.read<ArticleListBloc>().add(GetArticlesEvent());
});
},
);
},
);
} else if (state is ArticleListError) {
return Center(child: Text(state.message));
}
return Container();
},
),
),
);
}
}
// presentation/pages/article_detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../di/injection_container.dart';
import '../bloc/article_detail/article_detail_bloc.dart';
import '../bloc/article_detail/article_detail_event.dart';
import '../bloc/article_detail/article_detail_state.dart';
class ArticleDetailPage extends StatelessWidget {
final int articleId;
const ArticleDetailPage({Key? key, required this.articleId}) : super(key: key);
@override
Widget build(BuildContext context) {
// 这里的 sl 是 GetIt 实例
return BlocProvider(
create: (context) => sl<ArticleDetailBloc>()..add(GetArticleDetailEvent(articleId)),
child: Scaffold(
appBar: AppBar(title: Text('文章详情')),
body: BlocBuilder<ArticleDetailBloc, ArticleDetailState>(
builder: (context, state) {
if (state is ArticleDetailLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is ArticleDetailLoaded) {
final article = state.article;
return SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 200,
color: Colors.grey[300],
child: Center(child: Text('Image: ${article.imageUrl}')),
),
SizedBox(height: 16),
Text(
article.title,
style: Theme.of(context).textTheme.headline5,
),
SizedBox(height: 8),
Row(
children: [
Text('作者:${article.author}'),
Spacer(),
Text('发布日期:${article.publishDate.toString().substring(0, 10)}'),
],
),
SizedBox(height: 16),
Text(article.content),
],
),
);
} else if (state is ArticleDetailError) {
return Center(child: Text(state.message));
}
return Container();
},
),
floatingActionButton: BlocBuilder<ArticleDetailBloc, ArticleDetailState>(
builder: (context, state) {
if (state is ArticleDetailLoaded) {
return FloatingActionButton(
onPressed: () {
context.read<ArticleDetailBloc>().add(ToggleFavoriteEvent(state.article));
},
child: Icon(
state.article.isFavorite ? Icons.favorite : Icons.favorite_border,
),
);
}
return SizedBox.shrink();
},
),
),
);
}
}
// di/injection_container.dart
import 'package:get_it/get_it.dart';
import '../data/repositories/article_repository_impl.dart';
import '../domain/repositories/article_repository.dart';
import '../domain/usecases/get_articles.dart';
import '../domain/usecases/get_article_by_id.dart';
import '../domain/usecases/toggle_favorite_article.dart';
import '../presentation/bloc/article_list/article_list_bloc.dart';
import '../presentation/bloc/article_detail/article_detail_bloc.dart';
final sl = GetIt.instance;
Future<void> init() async {
// BLoC
sl.registerFactory(() => ArticleListBloc(getArticles: sl()));
sl.registerFactory(() => ArticleDetailBloc(
getArticleById: sl(),
toggleFavoriteArticle: sl(),
));
// Use Cases
sl.registerLazySingleton(() => GetArticles(sl()));
sl.registerLazySingleton(() => GetArticleById(sl()));
sl.registerLazySingleton(() => ToggleFavoriteArticle(sl()));
// Repository
sl.registerLazySingleton<ArticleRepository>(() => ArticleRepositoryImpl());
}
// main.dart
import 'package:flutter/material.dart';
import 'di/injection_container.dart' as di;
import 'presentation/pages/article_list_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await di.init();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Clean Architecture Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ArticleListPage(),
);
}
}
答:
答:这体现了依赖倒置原则(Dependency Inversion Principle)。
答:
答:UseCase(也称为 Interactor)位于 Domain 层,它封装了特定的业务逻辑场景。
Clean Architecture 虽然在初期会增加一些代码量和复杂性,但它为大型 Flutter 应用提供了坚实的架构基础。通过将应用分层,我们实现了关注点分离,使得代码更加清晰、可测试和可维护。
在实际开发中,不必教条式地遵守每一条规则,而应根据项目规模和团队情况灵活调整。对于小型项目,可能不需要如此严格的分层;但对于长期维护的中大型项目,Clean Architecture 绝对是一个值得投资的选择。
通过本文的实践,我们不仅学习了 Clean Architecture 的理论知识,还通过一个完整的案例掌握了其落地实施的方法。希望这些内容能帮助你在未来的 Flutter 开发中构建出更高质量的应用。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online