架构设计模式:Clean Architecture实践

架构设计模式:Clean Architecture实践

一、Clean Architecture概述

1.1 什么是Clean Architecture

Clean Architecture(简称CA)是由Robert C. Martin(Uncle Bob)提出的一种软件架构模式,旨在创建一个独立于框架、UI、数据库和任何外部代理的系统。它通过分离关注点来实现高度可测试、可维护和可扩展的代码库。

在Flutter应用开发中,Clean Architecture的核心价值在于:

  • 独立于框架:核心业务逻辑不依赖于Flutter框架,使代码更易于迁移和重用
  • 可测试性:业务规则可以在没有UI、数据库或任何外部元素的情况下进行测试
  • 独立于UI:UI可以轻松更改,而不影响系统的其余部分
  • 独立于数据库:业务规则不绑定到特定的数据库实现
  • 独立于任何外部代理:业务规则不知道外部世界的任何信息

1.2 Clean Architecture的核心原则

Clean Architecture基于以下几个核心原则:

  1. 依赖规则:源代码依赖只能指向内层,内层不应该知道任何关于外层的信息
  2. 关注点分离:将应用程序分为不同的责任层
  3. 依赖倒置原则:高层模块不应该依赖于低层模块,两者都应该依赖于抽象
  4. 实体封装业务规则:实体封装企业范围的业务规则
  5. 用例封装应用程序特定的业务规则:用例编排实体之间的数据流

1.3 Clean Architecture在Flutter中的应用价值

在Flutter开发中应用Clean Architecture有以下几个显著优势:

  • 代码组织清晰:明确的层次结构使团队成员更容易理解和导航代码库
  • 可维护性提高:由于关注点分离,修改一个部分不会影响其他部分
  • 测试更加容易:业务逻辑与UI和外部依赖分离,使单元测试变得简单
  • 适应变化:当需求变化时,可以更容易地修改代码而不影响整个系统
  • 团队协作效率提升:不同团队成员可以同时在不同层上工作,减少冲突

二、Clean Architecture的层次结构

2.1 典型的Clean Architecture层次

Clean Architecture在Flutter中通常分为以下几层:

  1. 表示层(Presentation Layer)
    • 包含所有UI组件(Widget、Screen、Page)
    • 包含状态管理(Bloc、Provider等)
    • 负责用户交互和数据展示
  2. 领域层(Domain Layer)
    • 包含业务实体(Entities)
    • 包含用例(Use Cases/Interactors)
    • 包含抽象的仓库接口(Repository Interfaces)
    • 是整个架构的核心,不依赖于任何外部层
  3. 数据层(Data Layer)
    • 包含仓库实现(Repository Implementations)
    • 包含数据源(Data Sources):本地数据源和远程数据源
    • 包含数据模型(Data Models)
    • 负责数据的获取和存储

2.2 各层之间的依赖关系

在Clean Architecture中,依赖关系是单向的,从外层指向内层:

表示层 → 领域层 ← 数据层 
  • 表示层依赖于领域层,但不知道数据层的存在
  • 领域层不依赖于任何其他层,它是独立的
  • 数据层依赖于领域层(为了实现其接口),但不知道表示层的存在

这种依赖关系通过依赖倒置原则(DIP)实现,即高层模块(领域层)定义接口,低层模块(数据层)实现这些接口。

2.3 数据流向

在Clean Architecture中,数据流通常遵循以下路径:

  1. 用户交互:用户在UI上执行操作
  2. 表示层处理:表示层(如Bloc)接收用户操作并调用相应的用例
  3. 用例执行:用例协调实体和仓库接口来执行业务逻辑
  4. 数据获取:仓库实现从数据源获取数据
  5. 数据转换:数据从数据模型转换为领域实体
  6. 结果返回:结果沿着相同的路径返回到表示层
  7. UI更新:表示层更新UI以反映新状态

三、在Flutter中实现Clean Architecture

3.1 项目结构组织

一个遵循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 # 应用入口 

3.2 领域层实现

领域层是Clean Architecture的核心,包含业务实体、用例和仓库接口。

3.2.1 实体(Entities)

实体代表业务对象和业务规则。它们是纯Dart类,不依赖于任何框架。

// domain/entities/article.dartclassArticle{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,});}
3.2.2 仓库接口(Repository Interfaces)

仓库接口定义了领域层期望数据层提供的功能。

// domain/repositories/article_repository.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../../core/error/failures.dart';abstractclassArticleRepository{ Future<Either<Failure, List<Article>>>getArticles(); Future<Either<Failure, Article>>getArticleById(int id); Future<Either<Failure,void>>saveArticle(Article article);}

这里使用了dartz包的Either类型来处理错误,左侧表示失败,右侧表示成功。

3.2.3 用例(Use Cases)

用例封装了特定的业务逻辑,每个用例通常只做一件事。

// domain/usecases/get_articles.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';classGetArticles{final ArticleRepository repository;GetArticles(this.repository); Future<Either<Failure, List<Article>>>call(){return repository.getArticles();}}// domain/usecases/toggle_favorite_article.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';import'../../core/usecases/usecase.dart';classToggleFavoriteArticleimplementsUseCase<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.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';import'../../core/usecases/usecase.dart';classGetArticleByIdimplementsUseCase<Article, int>{final ArticleRepository repository;GetArticleById(this.repository);@override Future<Either<Failure, Article>>call(int id){return repository.getArticleById(id);}}

3.3 数据层实现

数据层负责从各种来源获取数据并将其转换为领域实体。

3.3.1 数据模型(Data Models)

数据模型是实体的具体实现,通常包含序列化和反序列化逻辑。

// data/models/article_model.dartimport'../../domain/entities/article.dart';classArticleModelextendsArticle{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){returnArticleModel( 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(),};}}
3.3.2 数据源(Data Sources)

数据源负责从特定来源(如API或本地数据库)获取数据。

// data/datasources/remote/article_remote_data_source.dartimport'../models/article_model.dart';import'package:http/http.dart'as http;abstractclassArticleRemoteDataSource{ Future<List<ArticleModel>>getArticles(); Future<ArticleModel>getArticleById(int id);}classArticleRemoteDataSourceImplimplementsArticleRemoteDataSource{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{throwServerException();}}@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{throwServerException();}}}
3.3.3 仓库实现(Repository Implementations)

仓库实现负责协调不同的数据源并实现领域层定义的仓库接口。

// data/repositories/article_repository_impl.dartimport'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';classArticleRepositoryImplimplementsArticleRepository{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);returnRight(remoteArticles);}on ServerException {returnLeft(ServerFailure());}}else{try{final localArticles =await localDataSource.getLastArticles();returnRight(localArticles);}on CacheException {returnLeft(CacheFailure());}}}@override Future<Either<Failure, Article>>getArticleById(int id)async{// 类似的实现逻辑}@override Future<Either<Failure,void>>saveArticle(Article article)async{// 实现保存文章的逻辑}}

3.4 表示层实现

表示层负责UI和用户交互,在Flutter中通常使用BLoC、Provider或其他状态管理解决方案。

3.4.1 使用BLoC进行状态管理
// presentation/bloc/article/article_event.dartimport'package:equatable/equatable.dart';abstractclassArticleEventextendsEquatable{@override List<Object>get props =>[];}classGetArticlesEventextendsArticleEvent{}classGetArticleByIdEventextendsArticleEvent{final int id;GetArticleByIdEvent(this.id);@override List<Object>get props =>[id];}
// presentation/bloc/article/article_state.dartimport'package:equatable/equatable.dart';import'../../../domain/entities/article.dart';abstractclassArticleStateextendsEquatable{@override List<Object>get props =>[];}classArticleInitialextendsArticleState{}classArticleLoadingextendsArticleState{}classArticlesLoadedextendsArticleState{final List<Article> articles;ArticlesLoaded(this.articles);@override List<Object>get props =>[articles];}classArticleLoadedextendsArticleState{final Article article;ArticleLoaded(this.article);@override List<Object>get props =>[article];}classArticleErrorextendsArticleState{final String message;ArticleError(this.message);@override List<Object>get props =>[message];}
// presentation/bloc/article/article_bloc.dartimport'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';classArticleBlocextendsBloc<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 =awaitgetArticles(); result.fold((failure)=>emit(ArticleError(_mapFailureToMessage(failure))),(articles)=>emit(ArticlesLoaded(articles)),);} Future<void>_onGetArticleById(GetArticleByIdEvent event, Emitter<ArticleState> emit)async{emit(ArticleLoading());final result =awaitgetArticleById(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'未知错误';}}}
3.4.2 UI实现
// presentation/pages/articles_page.dartimport'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';classArticlesPageextendsStatelessWidget{@override Widget build(BuildContext context){returnScaffold( appBar:AppBar(title:Text('文章列表')), body: BlocBuilder<ArticleBloc, ArticleState>( builder:(context, state){if(state is ArticleInitial){ BlocProvider.of<ArticleBloc>(context).add(GetArticlesEvent());returnCenter(child:Text('加载中...'));}elseif(state is ArticleLoading){returnCenter(child:CircularProgressIndicator());}elseif(state is ArticlesLoaded){return ListView.builder( itemCount: state.articles.length, itemBuilder:(context, index){returnArticleListItem(article: state.articles[index]);},);}elseif(state is ArticleError){returnCenter(child:Text(state.message));}else{returnCenter(child:Text('未知状态'));}},),);}}

3.5 依赖注入

依赖注入是Clean Architecture的重要组成部分,它使各层之间的依赖关系更加清晰和可测试。在Flutter中,可以使用get_it包实现依赖注入。

// di/injection_container.dartimport'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()),);// Externalfinal sharedPreferences =await SharedPreferences.getInstance(); sl.registerLazySingleton(()=> sharedPreferences); sl.registerLazySingleton(()=> http.Client()); sl.registerLazySingleton(()=>InternetConnectionChecker());}

四、实战案例:新闻阅读应用

4.1 需求分析

我们将构建一个简单的新闻阅读应用,具有以下功能:

  1. 显示新闻文章列表
  2. 查看文章详情
  3. 保存文章到收藏夹
  4. 离线阅读功能

4.2 领域建模

首先,我们需要定义领域实体和用例:

// domain/entities/article.dartclassArticle{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,}){returnArticle( 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,);}}

4.3 用例实现

// domain/usecases/get_articles.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';import'../../core/usecases/usecase.dart';classGetArticlesimplementsUseCase<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.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';import'../../core/usecases/usecase.dart';classToggleFavoriteArticleimplementsUseCase<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.dartimport'package:dartz/dartz.dart';import'../entities/article.dart';import'../repositories/article_repository.dart';import'../../core/error/failures.dart';import'../../core/usecases/usecase.dart';classGetArticleByIdimplementsUseCase<Article, int>{final ArticleRepository repository;GetArticleById(this.repository);@override Future<Either<Failure, Article>>call(int id){return repository.getArticleById(id);}}

4.4 数据层实现

// data/models/article_model.dartimport'../../domain/entities/article.dart';classArticleModelextendsArticle{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){returnArticleModel( 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,};}}
4.4.2 仓库实现
// data/repositories/article_repository_impl.dartimport'package:dartz/dartz.dart';import'../../domain/entities/article.dart';import'../../domain/repositories/article_repository.dart';import'../../core/error/failures.dart';classArticleRepositoryImplimplementsArticleRepository{// 模拟数据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));returnRight(_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);returnRight(article);}catch(e){returnLeft(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;returnRight(null);}else{returnLeft(ServerFailure());}}}

4.5 表示层实现

4.5.1 定义BLoC
// presentation/bloc/article_list/article_list_bloc.dartimport'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';classArticleListBlocextendsBloc<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 =awaitgetArticles(NoParams()); result.fold((failure)=>emit(ArticleListError('加载失败')),(articles)=>emit(ArticleListLoaded(articles)),);}}// presentation/bloc/article_detail/article_detail_event.dartabstractclassArticleDetailEvent{}classGetArticleDetailEventextendsArticleDetailEvent{final int id;GetArticleDetailEvent(this.id);}classToggleFavoriteEventextendsArticleDetailEvent{final Article article;ToggleFavoriteEvent(this.article);}// presentation/bloc/article_detail/article_detail_state.dartabstractclassArticleDetailState{}classArticleDetailInitialextendsArticleDetailState{}classArticleDetailLoadingextendsArticleDetailState{}classArticleDetailLoadedextendsArticleDetailState{final Article article;ArticleDetailLoaded(this.article);}classArticleDetailErrorextendsArticleDetailState{final String message;ArticleDetailError(this.message);}// presentation/bloc/article_detail/article_detail_bloc.dartimport'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';classArticleDetailBlocextendsBloc<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 =awaitgetArticleById(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 =awaittoggleFavoriteArticle(event.article); result.fold((failure)=>emit(ArticleDetailError('操作失败')),(_){final updatedArticle = event.article.copyWith( isFavorite:!event.article.isFavorite,);emit(ArticleDetailLoaded(updatedArticle));},);}}}
4.5.2 文章列表页面
// presentation/pages/article_list_page.dartimport'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';classArticleListPageextendsStatelessWidget{@override Widget build(BuildContext context){returnScaffold( appBar:AppBar(title:Text('每日新闻')), body:BlocProvider( create:(_)=> sl<ArticleListBloc>()..add(GetArticlesEvent()), child: BlocBuilder<ArticleListBloc, ArticleListState>( builder:(context, state){if(state is ArticleListLoading){returnCenter(child:CircularProgressIndicator());}elseif(state is ArticleListLoaded){return ListView.builder( itemCount: state.articles.length, itemBuilder:(context, index){final article = state.articles[index];returnListTile( 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());});},);},);}elseif(state is ArticleListError){returnCenter(child:Text(state.message));}returnContainer();},),),);}}
4.5.3 文章详情页面
// presentation/pages/article_detail_page.dartimport'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';classArticleDetailPageextendsStatelessWidget{final int articleId;constArticleDetailPage({Key? key, required this.articleId}):super(key: key);@override Widget build(BuildContext context){// 这里的sl是GetIt实例returnBlocProvider( create:(context)=> sl<ArticleDetailBloc>()..add(GetArticleDetailEvent(articleId)), child:Scaffold( appBar:AppBar(title:Text('文章详情')), body: BlocBuilder<ArticleDetailBloc, ArticleDetailState>( builder:(context, state){if(state is ArticleDetailLoading){returnCenter(child:CircularProgressIndicator());}elseif(state is ArticleDetailLoaded){final article = state.article;returnSingleChildScrollView( 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),],),);}elseif(state is ArticleDetailError){returnCenter(child:Text(state.message));}returnContainer();},), floatingActionButton: BlocBuilder<ArticleDetailBloc, ArticleDetailState>( builder:(context, state){if(state is ArticleDetailLoaded){returnFloatingActionButton( onPressed:(){ context.read<ArticleDetailBloc>().add(ToggleFavoriteEvent(state.article));}, child:Icon( state.article.isFavorite ? Icons.favorite : Icons.favorite_border,),);}return SizedBox.shrink();},),),);}}

4.6 依赖注入配置

// di/injection_container.dartimport'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(),);}

4.7 应用入口

// main.dartimport'package:flutter/material.dart';import'di/injection_container.dart'as di;import'presentation/pages/article_list_page.dart';voidmain()async{ WidgetsFlutterBinding.ensureInitialized();await di.init();runApp(MyApp());}classMyAppextendsStatelessWidget{@override Widget build(BuildContext context){returnMaterialApp( title:'Clean Architecture Demo', theme:ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity,), home:ArticleListPage(),);}}

五、常见面试题解析

5.1 Clean Architecture有哪些主要优点?

  1. 独立于框架:核心业务逻辑不依赖于UI框架,使得框架升级或更换更加容易。
  2. 可测试性:由于业务逻辑与UI和外部依赖(如数据库、网络)分离,可以轻松编写单元测试。
  3. 独立于UI:UI的改变不会影响业务逻辑。
  4. 独立于数据库:业务规则不绑定到特定的数据库实现。
  5. 关注点分离:明确的层次结构使得代码更易于维护和理解。

5.2 为什么在Domain层定义Repository接口,而在Data层实现它?

:这体现了依赖倒置原则(Dependency Inversion Principle)

  • Domain层是高层模块,包含核心业务逻辑,不应该依赖于低层模块(如Data层)。
  • 通过在Domain层定义接口,我们让Data层依赖于Domain层(实现接口),从而反转了依赖关系。
  • 这样,Domain层就保持了独立性,不依赖于任何外部实现细节。

5.3 实体(Entity)和数据模型(Model)有什么区别?

  • 实体(Entity):位于Domain层,是纯粹的业务对象,只包含业务数据和业务规则,不依赖于任何框架或序列化机制(如JSON转换)。
  • 数据模型(Model):位于Data层,通常是实体的子类或包装类。它负责处理数据传输(DTO)和数据转换(如JSON序列化/反序列化、数据库映射)。
  • 这种分离确保了外部数据格式的变化不会影响核心业务逻辑。

5.4 什么是UseCase(用例)?它有什么作用?

:UseCase(也称为Interactor)位于Domain层,它封装了特定的业务逻辑场景。

  • 每个UseCase通常只负责一个单一的任务(如"获取文章列表"、“登录”)。
  • 它协调Repository和Entity来完成业务目标。
  • UseCase使得业务逻辑更加清晰、可复用,并且符合单一职责原则。

六、总结

Clean Architecture虽然在初期会增加一些代码量和复杂性,但它为大型Flutter应用提供了坚实的架构基础。通过将应用分层,我们实现了关注点分离,使得代码更加清晰、可测试和可维护。

在实际开发中,不必教条式地遵守每一条规则,而应根据项目规模和团队情况灵活调整。对于小型项目,可能不需要如此严格的分层;但对于长期维护的中大型项目,Clean Architecture绝对是一个值得投资的选择。

通过本文的实践,我们不仅学习了Clean Architecture的理论知识,还通过一个完整的案例掌握了其落地实施的方法。希望这些内容能帮助你在未来的Flutter开发中构建出更高质量的应用。

Read more

Stable-Diffusion-v1-5-archive部署指南:GPU加速+7860端口一键启用

Stable-Diffusion-v1-5-archive部署指南:GPU加速+7860端口一键启用 想快速体验经典AI绘画的魅力吗?Stable Diffusion v1.5 Archive(简称SD1.5)作为文生图领域的“常青树”,以其稳定的表现和广泛的社区支持,依然是许多创作者和开发者的首选。今天,我们就来手把手教你,如何在几分钟内完成这个经典模型的部署,并立即通过浏览器开始你的AI绘画创作。 1. 环境准备与快速部署 部署过程非常简单,几乎不需要任何复杂的配置。你只需要一个支持GPU的云服务器实例,然后按照以下步骤操作即可。 1.1 系统要求与准备工作 在开始之前,请确保你的环境满足以下基本要求: * 操作系统:推荐使用Ubuntu 20.04或更高版本,其他Linux发行版也可。 * GPU支持:需要NVIDIA GPU,并已安装好相应的CUDA驱动(建议CUDA 11.8及以上)。 * 网络:能够正常访问互联网,用于下载模型文件。 * 存储空间:至少预留10GB的可用磁盘空间。 如果你使用的是云服务商提供的GPU实例,通常这些环境都已经预配置好了,

DAMODEL平台|Llama 3.1 开源模型快速部署:从零到上线

DAMODEL平台|Llama 3.1 开源模型快速部署:从零到上线

文章目录 * 一、Llama 3.1 系列的演进与发展历程 * 二、大型语言模型的力量:Llama 3.1 405B * 三、Llama 3.1 405B 部署教程 * 四、Llama 3.1在客户服务中的运用 一、Llama 3.1 系列的演进与发展历程 自开源LLM(大语言模型)兴起以来,Meta公司凭借其Llama系列逐步在全球AI领域占据重要地位。2024年7月23日,Meta发布了Llama 3.1系列,标志着该系列在技术上的一次重要飞跃。 Llama 3.1的发布不仅在算法优化和性能提升方面做出了突破,还在数据处理和模型架构上进行了革新。随着版本的不断迭代,Llama系列逐步从最初的研究原型发展为一个功能强大、易于扩展的工具,深刻影响了开源AI生态的进步。 本篇文章将详细回顾Llama 3.1系列的演进历程,探讨其在开源领域的重要贡献以及未来发展的潜力。 这一成就的背后,是超过15万亿的Tokens和超过1.

知网AIGC检测不通过?三步搞定降AI率

知网AIGC检测不通过?三步搞定降AI率

知网AIGC检测不通过?三步搞定降AI率 “我论文在知网AIGC检测里被判了52%的AI率,学校要求低于30%才能过,我该怎么办?” 最近几个月,这类求助在毕业生群里几乎天天都能看到。2026年的知网AIGC检测系统已经升级了好几轮,检测精度比去年高了不少,很多以前能蒙混过关的方法现在都不管用了。 但这不意味着没有办法。这篇文章,我把降知网AI率的方法浓缩成三个步骤,每一步都讲清楚具体该怎么操作。不绕弯子,直接上干货。 开始之前:了解知网AIGC检测的特点 要打败对手,先要了解对手。知网的AIGC检测与其他平台相比,有几个显著的特点: 检测颗粒度细:知网不仅给出全文的AI率,还会对每个段落甚至每个句子进行逐一判定。它的检测报告会用颜色标注每一段的AI概率——红色(高概率AI生成)、橙色(疑似AI生成)、绿色(人类写作)。 对学术文本更敏感:知网的训练数据包含大量学术论文,所以它对学术写作风格的AI特征识别得更准。那种一看就是AI写的"学术腔"文字,在知网面前特别容易露馅。 更新频率快:知网的检测模型会定期更新。上个月能过的文本,这个月不一定能过。所以不要依赖"据说有用

llamafactory微调qwen3-vl详细流程

llamafactory微调qwen3-vl详细流程

llamafactory微调qwen3-vl详细流程 目标:本文讲详细介绍多模态大模型使用llama-factory进行多模态模型微调(sft)的全部流程,以及微调后合并和工业落地部署方案。具体包括: 1. 环境安装部署 2. 数据集准备 3. 启动微调 4. 模型合并 5. 模型部署和请求方式(vllm部署) 示例模型: qwen2.5-vl-instruct qwen3-vl-instruct 环境安装 llama-factory环境准备 方式1 git直接下载 git clone --depth https://github.com/hiyouga/LLaMA-Factory.git 方式2 下载项目压缩包再解压 python环境安装 1. python虚拟环境创建 * conda create --name llama_env python=3.12 (默认已安装好anaconda或者minianaconda) * conda