最近对手头上的项目进行重构,总结了有以下几个痛点:
-
1.状况办理紊乱 尽管用了
provider
来做状况办理,但一些代码如:异步恳求、事情呼应等仍是会掺杂在UI页面的代码里面,一旦页面的各种Widget
多了起来之后,显得十分严重,并且对事务逻辑的测验也不方便,多个组件或许需求同享相同的数据或状况,需求在每个组件中分别创立Provider
实例,容易导致代码冗余,如果只需求更新页面的部分Widget
运用Provider
还会导致嵌套过深,也或许导致性能问题、状况不一致以及难以追踪的错误。 -
2.代码分层紊乱,一切的代码都在一个工程里面,包含UI页面、
ViewModel
、Util
、网络恳求和通用的Widget
等,模块划分不清晰,模块依靠和调用紊乱,并且各个模块没发独自开发和测验,没有明确的分层或许导致事务逻辑和 UI 代码稠浊在一起,并且多个项目之间很难做到代码复用。 -
3.事务迭代导致API接口改动频频,API接口返回的字段一旦呈现类型解析问题就会导致呈现崩溃,而现在担任
Json
映射的数据模型Model
类在很多的 UIWidget
和Bloc
中有运用到,修复这个问题就会导致很多依靠于此类的改动。而为了安全起见,数据模型类中的一切字段有必要始终是这样的:int?
、string?
等,但是,这又或许会导致因为NullPoInterException
而呈现使用程序崩溃的危险。 -
4.路由办理问题,
fluro
作为路由办理有必要依靠于context
,如果在逻辑层需求处理页面跳转的话就会比较费事。并且参数传递只能作为转成字符串放在path
中,然后再在注册路由的Handler
中将字段解分出或者转成Model
,运用起来多少有些不方便。
接下来系列文章来聊一聊怎处理上面的痛点,本篇文章先说一下状况办理办法,现有的项目需求一个能很好地敷衍大型项目的状况办理机制。
什么是状况办理
Flutter 状况办理是指在 Flutter 使用中有效地办理使用的数据和状况,以保证用户界面(UI)与数据之间的一致性和交互性。在杂乱的使用中,数据通常会在不同的部分之间流动和改动,而状况办理的目标是协助开发者更好地安排、更新和同享这些数据。
下面是运用比较多的状况办理办法及优缺点:
状况办理办法优缺点
上面能够看出如果是小型项目用setState()
或InheritedWidget
,原生自带,简略快捷。大型项目能够挑选更杂乱的办法,如Provider
、BLoC
或Redux
。特别是对于杂乱的使用逻辑,BLoC
和Redux
供给了更强大的状况办理和事务别离,将逻辑事务从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 BLoC
架构中,使用的事务逻辑被安排为一个个的BLoC
。每个BLoC
是一个独立的单元,一情况下对应一个页面(Page),担任办理这个页面特定的状况和逻辑,如网络恳求、恳求返回的数据解析处理、点击事情等等。BLoC
经过Streams
(流)和Sink
(数据源)与 UI 组件通信。这种别离的架构使得 UI 组件只需重视显现数据,而将数据获取、处理和改动交给了相应的BLoC
。
Bloc
主要由以下几个部分组成:
State
:Bloc
包含一个状况目标,它存储了当前的使用状况。这个状况能够是任何数据类型,从简略的布尔值到杂乱的目标都能够。
classHomeState{
boolisShimmerLoading;
intselectedIndex;
dynamicresponseData;
}
Event
: 事情是触发状况改动的操作或用户动作。当用户与使用交互时,事情会被触发,然后 BLoC 依据事情来决定如何改动状况。
abstractclassHomeEvent{
constHomeEvent();
}
classHomePageInitiatedextendsHomeEvent{
constHomePageInitiated(this.status);
finalAuthenticationStatusstatus;
}
classHomePageRefreshedextendsHomeEvent{
constHomePageRefreshed(this.completer);
Completer<void>completer;
}
Bloc
:Bloc
中包含了实践的事务逻辑。依据接收到的事情,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 组件能够订阅Bloc
的Stream
来监听状况改动,一起经过Sink
向Bloc
发送事情,触发状况改动。
@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
中发送网络恳求,获取数据后再刷新页面,实践使用中会导入freezed
、injectable
和get_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 页面为例,文件创立好后目录结构大概是这样的:
image.png
其中home_event.freezed.dart
和home_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技术实践”,原创技术文章第一时间推送。