最近对手头上的项目进行重构,总结了有以下几个痛点:

  1. 1.状况办理紊乱 尽管用了provider来做状况办理,但一些代码如:异步恳求、事情呼应等仍是会掺杂在UI页面的代码里面,一旦页面的各种Widget多了起来之后,显得十分严重,并且对事务逻辑的测验也不方便,多个组件或许需求同享相同的数据或状况,需求在每个组件中分别创立Provider实例,容易导致代码冗余,如果只需求更新页面的部分Widget运用Provider还会导致嵌套过深,也或许导致性能问题、状况不一致以及难以追踪的错误。

  2. 2.代码分层紊乱,一切的代码都在一个工程里面,包含UI页面、ViewModelUtil、网络恳求和通用的Widget等,模块划分不清晰,模块依靠和调用紊乱,并且各个模块没发独自开发和测验,没有明确的分层或许导致事务逻辑和 UI 代码稠浊在一起,并且多个项目之间很难做到代码复用。

  3. 3.事务迭代导致API接口改动频频,API接口返回的字段一旦呈现类型解析问题就会导致呈现崩溃,而现在担任Json映射的数据模型Model类在很多的 UIWidgetBloc中有运用到,修复这个问题就会导致很多依靠于此类的改动。而为了安全起见,数据模型类中的一切字段有必要始终是这样的:int?string?等,但是,这又或许会导致因为NullPoInterException而呈现使用程序崩溃的危险。

  4. 4.路由办理问题,fluro作为路由办理有必要依靠于context,如果在逻辑层需求处理页面跳转的话就会比较费事。并且参数传递只能作为转成字符串放在path中,然后再在注册路由的Handler中将字段解分出或者转成Model,运用起来多少有些不方便。

接下来系列文章来聊一聊怎处理上面的痛点,本篇文章先说一下状况办理办法,现有的项目需求一个能很好地敷衍大型项目的状况办理机制。

什么是状况办理

Flutter 状况办理是指在 Flutter 使用中有效地办理使用的数据和状况,以保证用户界面(UI)与数据之间的一致性和交互性。在杂乱的使用中,数据通常会在不同的部分之间流动和改动,而状况办理的目标是协助开发者更好地安排、更新和同享这些数据。

下面是运用比较多的状况办理办法及优缺点:

转存失败,主张直接上传图片文件

状况办理办法优缺点

上面能够看出如果是小型项目用setState()InheritedWidget,原生自带,简略快捷。大型项目能够挑选更杂乱的办法,如ProviderBLoCRedux。特别是对于杂乱的使用逻辑,BLoCRedux供给了更强大的状况办理和事务别离,将逻辑事务从UI层中剥离,方便保护和拓宽功用,但一起学习的杂乱度也高一些。在项目初期挑选适宜的状况办理办法仍是很重要的,提升功率的一起也避免了后边频频更换引起很多代码的改动。

本系列文章主要是针对大型项目的架构来打开的,所以挑选了BLoC作为状况办理办法,那为什么是BLoC?

Why Bloc?

Bloc makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable.

引用了 bloclibrary.dev 中一句话:Bloc 能够轻松地将展现层与事务逻辑别离,使您的代码快速、易于测验且可重用。

Flutter大型项目架构:状态管理篇

Flutter BLoC架构中,使用的事务逻辑被安排为一个个的BLoC。每个BLoC是一个独立的单元,一情况下对应一个页面(Page),担任办理这个页面特定的状况和逻辑,如网络恳求、恳求返回的数据解析处理、点击事情等等。BLoC经过Streams(流)和Sink(数据源)与 UI 组件通信。这种别离的架构使得 UI 组件只需重视显现数据,而将数据获取、处理和改动交给了相应的BLoC

Bloc主要由以下几个部分组成:

StateBloc包含一个状况目标,它存储了当前的使用状况。这个状况能够是任何数据类型,从简略的布尔值到杂乱的目标都能够。

classHomeState{
boolisShimmerLoading;
intselectedIndex;
dynamicresponseData;
}

Event: 事情是触发状况改动的操作或用户动作。当用户与使用交互时,事情会被触发,然后 BLoC 依据事情来决定如何改动状况。

abstractclassHomeEvent{
constHomeEvent();
}
classHomePageInitiatedextendsHomeEvent{
constHomePageInitiated(this.status);
finalAuthenticationStatusstatus;
}
classHomePageRefreshedextendsHomeEvent{
constHomePageRefreshed(this.completer);
Completer<void>completer;
}

BlocBloc中包含了实践的事务逻辑。依据接收到的事情,Bloc能够对状况进行更新、核算、处理异步操作等。

classHomeBlocextendsBloc<HomeEvent,HomeState>{
HomeBloc():super(HomeState()){
on<HomePageInitiated>(
_onHomePageInitiated
);
on<HomePageRefreshed>(
_onHomePageRefreshed
);
}

FutureOr<void>_onHomePageInitiated(
HomePageInitiatedevent,Emitter<HomeState>emit){
//宣布一个新状况,通知页面更新。
emit(state.copyWith(isShimmerLoading:false));
}

FutureOr<void>_onHomePageRefreshed(
HomePageRefreshedevent,Emitter<HomeState>emit){
}
}

UI Page:Bloc运用Streams将状况传递给 UI 组件,UI 组件能够订阅BlocStream来监听状况改动,一起经过SinkBloc发送事情,触发状况改动。

@override
Widgetbuild(BuildContextcontext){
returnScaffold(
appBar:AppBar(title:constText('Home')),
body:Padding(
padding:constEdgeInsets.all(12),
child:BlocProvider(
create:(context){
returnHomeBloc();
},
child:BlocBuilder<HomeBloc,HomeState>(
buildWhen:(previous,current)=>previous.isShimmerLoading!=current.isShimmerLoading,
builder:(context,state){
returnGestureDetector(
onTap:(){
bloc.add(constHomePageInitiated());
},
child:......,
);
},
)
),
),
);
}

这样做的优势也很明显:

  • •事务逻辑别离:Bloc架构将事务逻辑从 UI 中别离,使代码更具可读性和可保护性,一起便于单元测验。

  • •可测验性: 因为Bloc的状况和逻辑都是独立的,能够轻松地进行单元测验,保证代码质量。

  • •状况可预测: 运用Streams将状况传递给 UI 组件,保证状况的一致性和可预测性。

Bloc 实践使用

接下来完成Bloc中发送网络恳求,获取数据后再刷新页面,实践使用中会导入freezedinjectableget_it等依靠库,其中freezed是数据体代码生成器,injectable调配get_it处理依靠注入的问题。还有其它依靠如下:

name:app
description:AnewFlutterproject.
publish_to:'none'#Removethislineifyouwishtopublishtopub.dev
version:1.0.0+1
environment:
sdk:">=2.15.0<3.0.0"
dependency_overrides:
analyzer:5.2.0
build_resolvers:2.1.0
dart_style:2.2.4
ffi:^1.2.0
dependencies:
flutter_localizations:
sdk:flutter
flutter:
sdk:flutter
cupertino_icons:^1.0.2

#以下这几个package后边会有文章独自来说
widgets:
path:../widgets
shared:
path:../shared
domain:
path:../domain
resources:
path:../resources
initializer:
path:../initializer

injectable:
get_it:
flutter_bloc:8.0.1
freezed_annotation:2.2.0
flutter_screenutil:5.5.3+2
auto_route:5.0.4
loading_animation_widget:^1.2.0+4
pull_to_refresh:^2.0.0
url_launcher:
dev_dependencies:
flutter_test:
sdk:flutter
flutter_lints:^1.0.0
injectable_generator:^2.1.4
build_runner:2.3.3
freezed:2.3.2
auto_route_generator:5.0.3
flutter_gen_runner:4.1.6
flutter_gen:
output:lib/resource/generated
line_lenght:160
null_safety:true
integrations:
flutter_svg:true
assets:
enabled:true
fonts:
enabled:true
flutter:
uses-material-design:true
generate:false
assets:
-assets/images/

以 Home 页面为例,文件创立好后目录结构大概是这样的:

Flutter大型项目架构:状态管理篇

image.png

其中home_event.freezed.darthome_state.freezed.dart需求手动创立好,内容是增加@freezed注解后主动生成的,不需求手动更改内容。

home_event.dart

import'dart:async';
import'package:freezed_annotation/freezed_annotation.dart';
import'../../../base/bloc/base_bloc_event.dart';
part'home_event.freezed.dart';
abstractclassHomeEventextendsBaseBlocEvent{
constHomeEvent();
}
@freezed
classHomePageInitiatedextendsHomeEventwith_$HomePageInitiated{
constfactoryHomePageInitiated()=_HomePageInitiated;
}

home_state.dart

import'package:domain/domain.dart';
import'package:freezed_annotation/freezed_annotation.dart';
import'package:shared/shared.dart';
import'../../../base/bloc/base_bloc_state.dart';
part'home_state.freezed.dart';
@freezed
classHomeStateextendsBaseBlocStatewith_$HomeState{
factoryHomeState({
@Default([])List<PlayListItemData>playList,
})=_HomeState;
}

home_bloc.dart

import'dart:async';
import'package:app/base/bloc/base_bloc.dart';
import'package:domain/domain.dart';
import'package:flutter_bloc/flutter_bloc.dart';
import'package:injectable/injectable.dart';
import'package:app/ui/home/bloc/home_event.dart';
import'package:app/ui/home/bloc/home_state.dart';
import'package:shared/util/log_utils.dart';
@Injectable()
classHomeBlocextendsBaseBloc<HomeEvent,HomeState>{
HomeBloc(this._repository):super(HomeState()){
on<HomePageInitiated>(
_onHomePageInitiated,
transformer:log(),
);
on<UserLoadMore>(
_onUserLoadMore,
transformer:log(),
);
on<HomePageRefreshed>(
_onHomePageRefreshed,
transformer:log(),
);
}
finalRepository_repository;

FutureOr<void>_onHomePageInitiated(
HomePageInitiatedevent,Emitter<HomeState>emit)async{
try{
List<PlayListItemData>playList=await_repository.playlist("");
//获取数据后触发页面更新
emit(state.copyWith(playList:playList));
}catch(err){
Log.e(err);
}
}
}

Repository是数据恳求的接口,其完成类是RepositoryImpl,经过注解@LazySingleton(as: Repository)将完成类RepositoryImpl注入到di中,这一块儿将在网络层的重构会说到。

HomePage_HomePageState承继自BasePageState,而BasePageState承继BasePageStateDelegate并在BasePageStateDelegate注册Bloc

base_page_state.dart

abstractclassBasePageState<TextendsStatefulWidget,BextendsBaseBloc>
extendsBasePageStateDelegate<T,B>withLogMixin{}
abstractclassBasePageStateDelegate<TextendsStatefulWidget,
BextendsBaseBloc>extendsState<T>implementsExceptionHandlerListener{
latefinalAppNavigatornavigator=GetIt.instance.get<AppNavigator>();
latefinalBbloc=GetIt.instance.get<B>()
..navigator=navigator;
@override
Widgetbuild(BuildContextcontext){
returnProvider(
create:(context){},
child:MultiBlocProvider(
providers:[
BlocProvider(create:(_)=>bloc),
],
child:buildPageListeners(
child:isAppWidget
?buildPage(context)
:Stack(
children:[
buildPage(context),
BlocBuilder<CommonBloc,CommonState>(
buildWhen:(previous,current)=>
previous.isLoading!=current.isLoading,
builder:(context,state)=>Visibility(
visible:state.isLoading,
child:buildPageLoading(),
),
),
],
),
)
),
);
}
WidgetbuildPageListeners({requiredWidgetchild})=>child;
WidgetbuildPageLoading()=>constCenter(
child:CircularProgressIndicator(
color:Color(0xFF666666),
strokeWidth:2,
),
);
WidgetbuildPage(BuildContextcontext);
@override
voiddispose(){
super.dispose();
}
}

home_page.dart

import'package:app/ui/home/bloc/home_state.dart';
import'package:flutter/material.dart';
import'package:app/base/base_page_state.dart';
import'package:app/common_view/common_scaffold.dart';
import'package:flutter_bloc/flutter_bloc.dart';
import'bloc/home_bloc.dart';
import'bloc/home_event.dart';
classHomePageextendsStatefulWidget{
constHomePage({Key?key}):super(key:key);
@override
_HomePageStatecreateState()=>_HomePageState();
}
class_HomePageStateextendsBasePageState<HomePage,HomeBloc>{
@override
initState(){
super.initState();
//向bloc增加HomePageInitiatedevent
bloc.add(constHomePageInitiated());
}
@override
WidgetbuildPage(BuildContextcontext){
returnCommonScaffold(
backgroundColor:Colors.white,
hideKeyboardWhenTouchOutside:true,
body:BlocBuilder<HomeBloc,HomeState>(
buildWhen:(previous,current)=>
previous.playList!=current.playList,
builder:(ctx,state){
returnListView.separated(
itemBuilder:(BuildContextcontext,intindex){
returnSizedBox(
height:120,
child:Text(state.playList[index].name??""),
);
},
separatorBuilder:(BuildContextcontext,intindex){
returnContainer();
},
itemCount:state.playList.length);
},
));
}
}

buildWhen中比照前后数据不一样的时分就会触发更新ListView的显现,并且只需求判断监听playList这一个字段的数据改动,如果需求监听其它字段来更新其它的Widget时也互相不搅扰,,整个没有剩余的逻辑处理,UI 层干净清新多了。以事情为驱动,事情一旦被触发,然后BLoC依据事情来决定如何改动状况,好处是这儿的状况和逻辑都是独立的,能够轻松地进行单元测验来保证代码质量。

欢迎重视我的大众号“flutter技术实践”,原创技术文章第一时间推送。