0%

开始整活儿了,实战音乐播放器项目

前面谈到了怎么用 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 22G120 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_playerworkspace,添加 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' # Remove this line if you wish to publish to pub.dev
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/

介绍一下上面配置文件中用到的第三方依赖库:

  • injectableinjectable_generator :依赖注入。
  • freezed_annotationfreezed :代码生成库,用来生成不可变(immutable)数据类和可复制(copyable)数据类,通常和 flutter_bloc 配合做状态更新时使用。
  • flutter_screenutil:屏幕适配工具
  • auto_routeauto_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 做跳转。
  • 常见对象的销毁,如:StreamSubscriptionStreamControllerChangeNotifier 等等对象,在自定义的 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
// 自定义的 State 基类
abstract class BasePageState<T extends StatefulWidget,
B extends BaseBloc> extends State<T> with LogMixin {
// 导航器对象
late final AppNavigator navigator = GetIt.instance.get<AppNavigator>();

// 入口的 widget 的 bloc,也是全局的 bloc,负责全局的状态刷新,如切换主题、语言等。
late final AppBloc appBloc = GetIt.instance.get<AppBloc>();

// 通用的 bloc,主要负责如处理异常、全局的loading的显示和隐藏等等
late final CommonBloc commonBloc = GetIt.instance.get<CommonBloc>()
..navigator = navigator
..disposeBag = disposeBag
..appBloc = appBloc;

// 返回当前的页面的 bloc 对象。同时将导航器、全局状态、
// 通用的 bloc 等传给当前的页面的 bloc 对象
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: [
// 将 bloc,commonBloc 注册到当前页面
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(
// 这里的 isAppWidget 的意思是当入口的widget,也就是 MyApp 加载完进入程序主页,
// 在全局加上 Loading,根据需要来显示或隐藏
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;

// 全局的 loading
Widget buildPageLoading() => const Center(
child: CircularProgressIndicator(
color: Color(0xFF333333),
strokeWidth: 2,
),
);

// 子类重写改方法返回当前页面
Widget buildPage(BuildContext context);

@override
void dispose() {
super.dispose();
// 销毁常见对象,如StreamSubscription、StreamController、ChangeNotifier等
disposeBag.dispose();
}
}

自定义 Bloc 父类

上面代码中的泛型类型 B 继承自 BaseBloc,而 BaseBloc 的父类是 BlocBaseBloc 是项目中所有用到的 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> {
// 构造方法中传一个泛型为S类型的对象
BaseBlocDelegate(S initialState) : super(initialState);
// 导航器,在 base_page_state 赋值其子类
late final AppNavigator navigator;
// 全局状态管理对象
late final AppBloc appBloc;
// 常见对象销毁管理器,在 base_page_state 赋值其子类
late final DisposeBag disposeBag;
// 通用的状态管理对象,在 base_page_state 赋值其子类
late final CommonBloc _commonBloc;

set commonBloc(CommonBloc commonBloc) {
_commonBloc = commonBloc;
}

CommonBloc get commonBloc =>
this is CommonBloc ? this as CommonBloc : _commonBloc;

// 重写 Bloc 的 add 函数,只有没有 closed 的情况下才添加 event。
@override
void add(E event) {
if (!isClosed) {
super.add(event);
} else {
Log.e('Cannot add new event $event because $runtimeType was closed');
}
}

// 添加异常对象,会触发 base_page_state 中异常监听器来处理异常。
Future<void> addException(AppExceptionWrapper appExceptionWrapper) async {
commonBloc.add(ExceptionEmitted(
appExceptionWrapper: appExceptionWrapper,
));
return appExceptionWrapper.exceptionCompleter?.future;
}

// loading 显示
void showLoading() {
commonBloc.add(const LoadingVisibilityEmitted(isLoading: true));
}

// loading 隐藏
void hideLoading() {
commonBloc.add(const LoadingVisibilityEmitted(isLoading: false));
}
}

这里的自定义的父类 BaseBloc 的构造函数需要传入泛型类型 SS 继承自抽象类 BaseBlocStateBaseBlocState 是作为整个项目的 BlocState 的状态基类。

这里的 EventTransformerMixin 是事件处理转换器,通常用于在业务逻辑组件中对事件进行一些预处理或后处理操作,其常用操作有 distinct: 如果当前 event 等于前一个 event ,就会直接跳过当前 eventexhaustMap:如果前一个 event 没有完成,新的 event 就会被忽略;throttleTime 在一段时间内忽略后续事件,然后重复此过程 等等。

项目入口的 Widget

这里可以看到 _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();
// 添加初始化 event 对象
bloc.add(const AppInitEvent());
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
}

@override
Widget buildPage(BuildContext context) {
return ScreenUtilInit(
// 设置当前 App 的设计尺寸。
designSize: const Size(DeviceWindowConstants.designWebWidth,
DeviceWindowConstants.designWebHeight),
builder: (context, _) => BlocBuilder<AppBloc, AppState>(
// 当语言或者主题发生变化时重绘整个 App
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(),
);
},
// 设置路由委托对象,负责管理 App 的路由状态
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;

此时 appBlocbloc 难道不是同一个东西吗?会不会有什么问题?它们的确是同一个东西,因为类 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());
}

// app 初始化 event
FutureOr<void> _onAppInitEvent(
AppInitEvent event, Emitter<AppState> emit) async {}

// 主题改变的 event
Future<void> _onAppThemeChanged(
AppThemeChanged event, Emitter<AppState> emit) async {}

// 语言切换的 event
Future<void> _onAppLanguageChanged(
AppLanguageChanged event, Emitter<AppState> emit) async {}
}

上面的代码看到 AppBloc 继承自 BaseBloc,同时指定了 eventstate 类,调用父类的构造函数时将 AppState 的对象作为参数传入,而 AppState 是继承自 BaseBlocState

小结

是不是感觉讲了半天到现在还没开始写页面,别着急哈,这次音乐播放器项目重点讲的是以大型项目架构的角度来实现的,前期的各种准备工作如父类、全局的状态管理还有后面的路由管理都是需要在项目开始的时候做好基本的规划设计,而不只是调接口画页面。这里是挑重要的几个类讲的,有的不够全面,建议大家看源码,关注公众号回复 简悦 将源码链接发给您,今天就分享到这里,后续还会有更多更新,您的关注是我更新下去最大的动力。

Flutter技术实践