前面谈到了怎么用 flutter_bloc
来做状态管理的、Flutter
项目架构是怎样分层的、各个分层之间又是如何依赖的,以及写界面用到的 widget
是如何设计封装实现的。这些是从一个成熟 Flutter
大型项目的不同角度来讨论的,很多人会觉得偏向于理论片面,或者说是很多实现的细节没有讲到,本篇将从零开始实现一个音乐播放器,也是将前面讲到的落地到真实的项目实践中去检验,看看会不会遇到什么问题,以及怎样去解决这些问题。
为什么要选择一个音乐播放器项目?音乐播放器项目大概是我在2年前开源放在 github
的一个项目,当时的代码实现也比较简单,没有用到前面讲到的大型项目架构用到的技术,本次开发完全使用最新的架构去实现,复杂度也有所上升,开发过程中为了照顾初学者,尽量把实现的细节说清楚。
功能列表 市面上的音乐播放器项目功能非常复杂,这里的音乐播放器项目参照了它们的一些常用的功能来开发,目的是仅供学习 ,所以在功能上会有所舍弃。下面图中是本次项目大概需要完成的功能,当然在实现的过程中下面列出来的这些功能也会有所变动。
UI 设计稿 下图是旧版本在 Mac
上运行的首页界面,页面风格是借鉴了 dribbble
上一位设计师大神的图片,具体的图片来源现在也忘记了。剩下其它页面和首页差不多也比较简单,这里就不放那么多图片了。本次实现的界面效果和旧版本会有些细微差别。
运行环境 命令行运行 dart --version
查看 dart SDK
版本。
1 2 yunxi@joe workspace % dart --version Dart SDK version: 2 .19 .1 (stable) (Tue Jan 31 12 :25 :35 2023 +0000 ) on "macos_arm64"
命令行运行 flutter doctor
查看 Flutter
版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 yunxi@joe flutter % flutter doctor Doctor summary (to see all details, run flutter doctor -v): [!] Flutter (Channel unknown, 3.7 .1 , on macOS 13.6 22 G120 darwin-arm64 (Rosetta), locale zh-Hans-CN) ! Flutter version 3.7 .1 on channel unknown at /Users/yunxi/flutter Currently on an unknown channel. Run `flutter channel` to switch to an official channel. If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/docs/get-started/install. ! Unknown upstream repository. Reinstall Flutter by following instructions at https://flutter.dev/docs/get-started/install. [✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0-rc1) [✓] Xcode - develop for iOS and macOS (Xcode 15.2) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.1) [✓] VS Code (version 1.88.1) [✓] Connected device (2 available) [✓] HTTP Host Availability
这里 Flutter
版本用的是 3.7.1
,而不是 stable
分支,因为需要兼容电脑里其它的老项目,所以没做升级更新。后面大家把代码下载下来后可以自己本地更新一下,有的依赖库也需要一起更新。开发工具用的是 Android Studio
, 版本是 version: 11.0.11
,也有段时间没更新了,不过不影响。
初始化项目 第一步:先建一个名为 jianyue_music_player
的 workspace
,添加 melos
配置文件 melos.yaml
,这里需要提前安装好 melos
,安装命令也很简单,在命令行中运行命令: dart pub global activate melos
。
第二步:在 jianyue_music_player
目录下新建主工程名为 app
,并在配置文件 melos.yaml
添加如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 name: jianyue_music_player packages: - app/** command: bootstrap: usePubspecOverrides: true scripts: analyze: run: dart pub global run melos exec --flutter "flutter analyze --no-pub --suppress-analytics" description: Run analyze. pub_get: run: dart pub global run melos exec --flutter "flutter pub get" description: pub get build_all: run: dart pub global run melos exec --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build all modules. build_app: run: dart pub global run melos exec --fail-fast --scope="*app*" --depends-on="build_runner" "flutter packages pub run build_runner build --delete-conflicting-outputs" description: build_runner build app module.
第三步:在当前目录下运行指令:melos bootstrap
。 第四步:来到主工程 app
的配置文件 pubspec.yaml
添加一下依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 name: jianyue_music_player description: music player project for learning flutter publish_to: 'none' version: 1.0 .0 +1 environment: sdk: '>=2.19.1 <3.0.0' dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 injectable: get_it: flutter_bloc: 8.0 .1 freezed_annotation: 2.2 .0 flutter_screenutil: 5.5 .3 +2 auto_route: 7.8 .0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 injectable_generator: ^2.1.4 build_runner: 2.3 .3 freezed: 2.3 .2 auto_route_generator: 7.3 .1 flutter: uses-material-design: true assets: - images/
介绍一下上面配置文件中用到的第三方依赖库:
injectable
和 injectable_generator
:依赖注入。
freezed_annotation
和 freezed
:代码生成库,用来生成不可变(immutable
)数据类和可复制(copyable
)数据类,通常和 flutter_bloc
配合做状态更新时使用。
flutter_screenutil
:屏幕适配工具
auto_route
和 auto_route_generator
:路由管理工具。
get_it
:依赖管理。
flutter_bloc
:状态管理。
build_runner
:用于执行代码生成器,通过读取注解并生成相应的代码文件。如 auto_route
:可以根据路由配置文件自动生成路由代码。
关于 melos
的使用可以看看我之前的文章《Flutter大型项目架构:分层设计篇》,那里有更加详细的介绍。
通用父类设计 一个大型的项目在设计类似 state
这种全局父类的时候,往往考虑的情况是非常多的,但在实际编写代码的时候却要很谨慎克制,特别是在设计通用功能的时候,设计之初是为了给它的其子类提供一些通用的功能或属性,但如果没处理好会造成继承关系的混乱,代码之间的依赖关系更加复杂,代码的结构变得不清晰。
自定义 State
父类 在 Flutter
项目中,State
是表示用户界面的可变状态的对象,它可以随着用户交互、数据更新或其他事件的发生而变化。State
对象是与特定的 Widget
实例相关联的,且每个 StatefulWidget
都有一个对应的 State
对象。所以在自定义 State
父类需要考虑一下几个方面:
状态管理对象,比如说这个项目里用的是 bloc
作为状态管理,如果在每个页面的 state
中都去手动创建这个 bloc
状态管理对象是不是很麻烦。而且还有所有的页面对应的 bloc
都需要一个导航器,因为很多时候我们需要在 bloc
中处理完业务逻辑之后做页面跳转。还有全局的状态管理对象,页面对应的 bloc
中也会读取或者更新全局的状态。
统一的导航器,这个就很好理解了,因为大多数情况下都需在页面的 state
做跳转。
常见对象的销毁,如:StreamSubscription
、StreamController
、ChangeNotifier
等等对象,在自定义的 State
父类中重写的父类的 dispose
函数中统一销毁,避免忘记在子页面 dispose
这些对象而造成内存泄露。
下面是自定义 State
父类的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 abstract class BasePageState <T extends StatefulWidget , B extends BaseBloc > extends State <T > with LogMixin { late final AppNavigator navigator = GetIt.instance.get <AppNavigator>(); late final AppBloc appBloc = GetIt.instance.get <AppBloc>(); late final CommonBloc commonBloc = GetIt.instance.get <CommonBloc>() ..navigator = navigator ..disposeBag = disposeBag ..appBloc = appBloc; late final B bloc = GetIt.instance.get <B>() ..navigator = navigator ..disposeBag = disposeBag ..appBloc = appBloc ..commonBloc = commonBloc; late final DisposeBag disposeBag = DisposeBag(); bool get isAppWidget => false ; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider(create: (_) => bloc), BlocProvider(create: (_) => commonBloc), ], child: BlocListener<CommonBloc, CommonState>( listenWhen: (previous, current) => (previous.appExceptionWrapper != current.appExceptionWrapper && current.appExceptionWrapper != null ), listener: (context, state) {}, child: buildPageListeners( child: isAppWidget ? buildPage(context) : Stack( children: [ buildPage(context), BlocBuilder<CommonBloc, CommonState>( buildWhen: (previous, current) => previous.isLoading != current.isLoading, builder: (context, state) { return Visibility( visible: state.isLoading, child: buildPageLoading(), ); }, ), ], ), ), ), ); } Widget buildPageListeners({required Widget child}) => child; Widget buildPageLoading() => const Center( child: CircularProgressIndicator( color: Color(0xFF333333 ), strokeWidth: 2 , ), ); Widget buildPage(BuildContext context); @override void dispose() { super .dispose(); disposeBag.dispose(); } }
自定义 Bloc
父类 上面代码中的泛型类型 B
继承自 BaseBloc
,而 BaseBloc
的父类是 Bloc
,BaseBloc
是项目中所有用到的 bloc
的父类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 abstract class BaseBloc <E extends BaseBlocEvent , S extends BaseBlocState > extends BaseBlocDelegate <E , S > with EventTransformerMixin , LogMixin { BaseBloc(S initialState) : super (initialState); } abstract class BaseBlocDelegate <E extends BaseBlocEvent , S extends BaseBlocState > extends Bloc <E , S > { BaseBlocDelegate(S initialState) : super (initialState); late final AppNavigator navigator; late final AppBloc appBloc; late final DisposeBag disposeBag; late final CommonBloc _commonBloc; set commonBloc(CommonBloc commonBloc) { _commonBloc = commonBloc; } CommonBloc get commonBloc => this is CommonBloc ? this as CommonBloc : _commonBloc; @override void add(E event) { if (!isClosed) { super .add(event); } else { Log.e('Cannot add new event $event because $runtimeType was closed' ); } } Future<void > addException(AppExceptionWrapper appExceptionWrapper) async { commonBloc.add(ExceptionEmitted( appExceptionWrapper: appExceptionWrapper, )); return appExceptionWrapper.exceptionCompleter?.future; } void showLoading() { commonBloc.add(const LoadingVisibilityEmitted(isLoading: true )); } void hideLoading() { commonBloc.add(const LoadingVisibilityEmitted(isLoading: false )); } }
这里的自定义的父类 BaseBloc
的构造函数需要传入泛型类型 S
,S
继承自抽象类 BaseBlocState
,BaseBlocState
是作为整个项目的 BlocState
的状态基类。
这里的 EventTransformerMixin
是事件处理转换器,通常用于在业务逻辑组件中对事件进行一些预处理或后处理操作,其常用操作有 distinct
: 如果当前 event
等于前一个 event
,就会直接跳过当前 event
;exhaustMap
:如果前一个 event
没有完成,新的 event
就会被忽略;throttleTime
在一段时间内忽略后续事件,然后重复此过程 等等。
这里可以看到 _MyAppState
继承自上面代码中的 BasePageState
类,并将泛型 T
指定为 MyApp
,将泛型 B
指定为 AppBloc
,而 AppBloc
的对象赋值过程是在父类 BasePageState
完成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 class MyApp extends StatefulWidget { const MyApp({Key? key}) : super (key: key); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends BasePageState <MyApp , AppBloc > { final _appRouter = GetIt.instance.get <AppRouter>(); @override bool get isAppWidget => true ; @override void initState() { super .initState(); bloc.add(const AppInitEvent()); } @override void didChangeDependencies() { super .didChangeDependencies(); } @override Widget buildPage(BuildContext context) { return ScreenUtilInit( designSize: const Size(DeviceWindowConstants.designWebWidth, DeviceWindowConstants.designWebHeight), builder: (context, _) => BlocBuilder<AppBloc, AppState>( buildWhen: (previous, current) => previous.isDarkTheme != current.isDarkTheme || previous.languageCode != current.languageCode, builder: (context, state) { return MaterialApp.router( useInheritedMediaQuery: true , builder: (context, child) { final MediaQueryData data = MediaQuery.of(context); return MediaQuery( data: data.copyWith(textScaleFactor: 1.0 ), child: child ?? const SizedBox.shrink(), ); }, routerDelegate: _appRouter.delegate( deepLinkBuilder: (deepLink) { return DeepLink.defaultPath; }, navigatorObservers: () => [AppNavigatorObserver()], ), routeInformationParser: _appRouter.defaultRouteParser(), title: "简悦" , themeMode: state.isDarkTheme ? ThemeMode.dark : ThemeMode.light, debugShowCheckedModeBanner: false , ); }, ), ); } }
到这里也许会有同学觉得很奇怪,在父类 BasePageState
已经有了 appBloc
,这里通过继承 BasePageState
又创建一个 bloc
。
1 2 3 4 5 6 7 late final AppBloc appBloc = GetIt.instance.get <AppBloc>();late final B bloc = GetIt.instance.get <B>() ..navigator = navigator ..disposeBag = disposeBag ..appBloc = appBloc ..commonBloc = commonBloc;
此时 appBloc
和 bloc
难道不是同一个东西吗?会不会有什么问题?它们的确是同一个东西,因为类 AppBloc
在注入的时候是 @LazySingleton()
延迟加载的单例对象。appBloc
是作为全局的状态管理对象,负责全局的状态更新,其实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @LazySingleton ()class AppBloc extends BaseBloc <AppEvent , AppState > { AppBloc() : super (const AppState()) { on <AppThemeChanged>( _onAppThemeChanged, transformer: throttleTime(), ); on <AppLanguageChanged>( _onAppLanguageChanged, transformer: log(), ); on <AppInitEvent>(_onAppInitEvent, transformer: log()); } FutureOr<void > _onAppInitEvent( AppInitEvent event, Emitter<AppState> emit) async {} Future<void > _onAppThemeChanged( AppThemeChanged event, Emitter<AppState> emit) async {} Future<void > _onAppLanguageChanged( AppLanguageChanged event, Emitter<AppState> emit) async {} }
上面的代码看到 AppBloc
继承自 BaseBloc
,同时指定了 event
和 state
类,调用父类的构造函数时将 AppState
的对象作为参数传入,而 AppState
是继承自 BaseBlocState
。
小结 是不是感觉讲了半天到现在还没开始写页面,别着急哈,这次音乐播放器项目重点讲的是以大型项目架构的角度来实现的,前期的各种准备工作如父类、全局的状态管理还有后面的路由管理都是需要在项目开始的时候做好基本的规划设计,而不只是调接口画页面。这里是挑重要的几个类讲的,有的不够全面,建议大家看源码,关注公众号回复 简悦 将源码链接发给您,今天就分享到这里,后续还会有更多更新,您的关注是我更新下去最大的动力。