Commit c272c662bd42c755c8ebf73b721a3a64bac78bed
1 parent
46385ad0
fix: 崩溃修复
Showing
10 changed files
with
199 additions
and
90 deletions
lib/app/splash_page.dart
@@ -140,19 +140,22 @@ class _TransitionViewState extends State<TransitionView> { | @@ -140,19 +140,22 @@ class _TransitionViewState extends State<TransitionView> { | ||
140 | actions: <Widget>[ | 140 | actions: <Widget>[ |
141 | TextButton( | 141 | TextButton( |
142 | child: const Text('退出'), | 142 | child: const Text('退出'), |
143 | - onPressed: () => { | ||
144 | - // 关闭对话框并重试 | ||
145 | - Navigator.of(context).pop(), | ||
146 | - AppConfigHelper.exitApp(), | ||
147 | - completer?.complete(), | ||
148 | - }), | 143 | + onPressed: () { |
144 | + // 关闭对话框并退出应用 | ||
145 | + Navigator.of(context).pop(); | ||
146 | + AppConfigHelper.exitApp(); | ||
147 | + // 只有在 completer 还没有完成时才完成它 | ||
148 | + if (completer != null && !completer.isCompleted) { | ||
149 | + completer.complete(); | ||
150 | + } | ||
151 | + }), | ||
149 | TextButton( | 152 | TextButton( |
150 | child: const Text('重试'), | 153 | child: const Text('重试'), |
151 | onPressed: () async { | 154 | onPressed: () async { |
152 | // 关闭对话框并重试 | 155 | // 关闭对话框并重试 |
153 | Navigator.of(context).pop(); | 156 | Navigator.of(context).pop(); |
154 | await fetchNecessaryData(userToken, completer: completer); | 157 | await fetchNecessaryData(userToken, completer: completer); |
155 | - completer?.complete(); | 158 | + // 不需要再次调用 completer.complete(),因为 fetchNecessaryData 内部已经处理了 |
156 | }, | 159 | }, |
157 | ), | 160 | ), |
158 | ], | 161 | ], |
lib/common/permission/permissionRequestPage.dart
@@ -30,6 +30,7 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | @@ -30,6 +30,7 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | ||
30 | bool _isGoSetting = false; | 30 | bool _isGoSetting = false; |
31 | late final List<String> msgList; | 31 | late final List<String> msgList; |
32 | bool _isDialogShowing = false; | 32 | bool _isDialogShowing = false; |
33 | + bool _isRequestingPermission = false; | ||
33 | 34 | ||
34 | @override | 35 | @override |
35 | void initState() { | 36 | void initState() { |
@@ -107,6 +108,11 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | @@ -107,6 +108,11 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | ||
107 | 108 | ||
108 | /// 校验权限 | 109 | /// 校验权限 |
109 | void _handlePermission(List<Permission> permissions) async { | 110 | void _handlePermission(List<Permission> permissions) async { |
111 | + if (_isRequestingPermission) { | ||
112 | + Log.d('_handlePermission already in progress, skipping'); | ||
113 | + return; | ||
114 | + } | ||
115 | + | ||
110 | ///一个新待申请权限列表 | 116 | ///一个新待申请权限列表 |
111 | List<Permission> intentPermissionList = []; | 117 | List<Permission> intentPermissionList = []; |
112 | 118 | ||
@@ -129,37 +135,51 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | @@ -129,37 +135,51 @@ class _PermissionRequestPageState extends State<PermissionRequestPage> | ||
129 | 135 | ||
130 | ///实际触发请求权限 | 136 | ///实际触发请求权限 |
131 | Future<void> _requestPermission(List<Permission> permissions) async { | 137 | Future<void> _requestPermission(List<Permission> permissions) async { |
132 | - Log.d('_requestPermission permissions=$permissions'); | ||
133 | - MapEntry<Permission, PermissionStatus>? statusEntry = | ||
134 | - await requestPermissionsInner(permissions); | ||
135 | - if (statusEntry == null) { | ||
136 | - ///都手动同意授予了 | ||
137 | - _popPage(true); | 138 | + if (_isRequestingPermission) { |
139 | + Log.d('_requestPermission already in progress, skipping'); | ||
138 | return; | 140 | return; |
139 | } | 141 | } |
142 | + | ||
143 | + _isRequestingPermission = true; | ||
144 | + Log.d('_requestPermission permissions=$permissions'); | ||
145 | + | ||
146 | + try { | ||
147 | + MapEntry<Permission, PermissionStatus>? statusEntry = | ||
148 | + await requestPermissionsInner(permissions); | ||
149 | + if (statusEntry == null) { | ||
150 | + ///都手动同意授予了 | ||
151 | + _popPage(true); | ||
152 | + return; | ||
153 | + } | ||
140 | 154 | ||
141 | - Permission permission = statusEntry.key; | ||
142 | - PermissionStatus status = statusEntry.value; | 155 | + Permission permission = statusEntry.key; |
156 | + PermissionStatus status = statusEntry.value; | ||
143 | 157 | ||
144 | - // 还未申请权限或之前拒绝了权限(在 iOS 上为首次申请权限,拒绝后将变为 `永久拒绝权限`) | ||
145 | - if (status.isDenied) { | ||
146 | - showAlert( | ||
147 | - permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | ||
148 | - } | ||
149 | - // 权限已被永久拒绝 | ||
150 | - /// 在 Android 上:Android 11+ (API 30+):用户是否第二次拒绝权限。低于 Android 11 (API 30):用户是否拒绝访问请求的功能,并选择不再显示请求。 | ||
151 | - /// 在 iOS 上:如果用户拒绝访问所请求的功能。 | ||
152 | - if (status.isPermanentlyDenied) { | ||
153 | - _isGoSetting = true; | ||
154 | - showAlert( | ||
155 | - permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | ||
156 | - } | ||
157 | - // isLimited:拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持) | ||
158 | - // isRestricted:拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS) | ||
159 | - if (status.isLimited || status.isRestricted) { | ||
160 | - if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true; | ||
161 | - showAlert( | ||
162 | - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | 158 | + // 还未申请权限或之前拒绝了权限(在 iOS 上为首次申请权限,拒绝后将变为 `永久拒绝权限`) |
159 | + if (status.isDenied) { | ||
160 | + showAlert( | ||
161 | + permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | ||
162 | + } | ||
163 | + // 权限已被永久拒绝 | ||
164 | + /// 在 Android 上:Android 11+ (API 30+):用户是否第二次拒绝权限。低于 Android 11 (API 30):用户是否拒绝访问请求的功能,并选择不再显示请求。 | ||
165 | + /// 在 iOS 上:如果用户拒绝访问所请求的功能。 | ||
166 | + if (status.isPermanentlyDenied) { | ||
167 | + _isGoSetting = true; | ||
168 | + showAlert( | ||
169 | + permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | ||
170 | + } | ||
171 | + // isLimited:拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持) | ||
172 | + // isRestricted:拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS) | ||
173 | + if (status.isLimited || status.isRestricted) { | ||
174 | + if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true; | ||
175 | + showAlert( | ||
176 | + permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); | ||
177 | + } | ||
178 | + } catch (e) { | ||
179 | + Log.d('_requestPermission error: $e'); | ||
180 | + _popPage(false); | ||
181 | + } finally { | ||
182 | + _isRequestingPermission = false; | ||
163 | } | 183 | } |
164 | } | 184 | } |
165 | 185 |
lib/common/permission/permissionRequester.dart
@@ -35,12 +35,13 @@ Future<bool> requestPermissions( | @@ -35,12 +35,13 @@ Future<bool> requestPermissions( | ||
35 | if (allGranted) { | 35 | if (allGranted) { |
36 | return true; | 36 | return true; |
37 | } else { | 37 | } else { |
38 | - return await Navigator.of(context).push(PageRouteBuilder( | 38 | + final result = await Navigator.of(context).push(PageRouteBuilder( |
39 | opaque: false, | 39 | opaque: false, |
40 | pageBuilder: ((context, animation, secondaryAnimation) { | 40 | pageBuilder: ((context, animation, secondaryAnimation) { |
41 | return PermissionRequestPage(permissions, permissionNames, permissionDesc, | 41 | return PermissionRequestPage(permissions, permissionNames, permissionDesc, |
42 | isRequiredPermission: isRequiredPermission); | 42 | isRequiredPermission: isRequiredPermission); |
43 | }))); | 43 | }))); |
44 | + return result ?? false; | ||
44 | } | 45 | } |
45 | } | 46 | } |
46 | 47 |
lib/common/request/api_response/api_response_entity.dart
1 | import 'dart:convert'; | 1 | import 'dart:convert'; |
2 | 2 | ||
3 | -import 'api_response_entity.g.dart'; | 3 | +import 'package:json_annotation/json_annotation.dart'; |
4 | 4 | ||
5 | +import 'api_response_entity.g.dart'; | ||
5 | 6 | ||
7 | +@JsonSerializable(genericArgumentFactories: true) | ||
6 | class ApiResponse<T> { | 8 | class ApiResponse<T> { |
7 | int? code; | 9 | int? code; |
8 | String? msg; | 10 | String? msg; |
lib/pages/home/widgets/ShakeImage.dart
@@ -20,7 +20,7 @@ class _ShakeImageState extends State<ShakeImage> | @@ -20,7 +20,7 @@ class _ShakeImageState extends State<ShakeImage> | ||
20 | with SingleTickerProviderStateMixin, WidgetsBindingObserver, RouteAware { | 20 | with SingleTickerProviderStateMixin, WidgetsBindingObserver, RouteAware { |
21 | late AnimationController _controller; | 21 | late AnimationController _controller; |
22 | late Animation<double> _animation; | 22 | late Animation<double> _animation; |
23 | - late Timer _timer; | 23 | + Timer? _timer; |
24 | late final AppLifecycleListener appLifecycleListener; | 24 | late final AppLifecycleListener appLifecycleListener; |
25 | static const TAG = 'ShakeImage'; | 25 | static const TAG = 'ShakeImage'; |
26 | 26 | ||
@@ -43,7 +43,11 @@ class _ShakeImageState extends State<ShakeImage> | @@ -43,7 +43,11 @@ class _ShakeImageState extends State<ShakeImage> | ||
43 | ); | 43 | ); |
44 | 44 | ||
45 | super.initState(); | 45 | super.initState(); |
46 | - WidgetsBinding.instance.addObserver(this); | 46 | + try { |
47 | + WidgetsBinding.instance.addObserver(this); | ||
48 | + } catch (e) { | ||
49 | + printLog('Add observer error: $e'); | ||
50 | + } | ||
47 | WidgetsBinding.instance.addPostFrameCallback((_) { | 51 | WidgetsBinding.instance.addPostFrameCallback((_) { |
48 | final route = ModalRoute.of(context); | 52 | final route = ModalRoute.of(context); |
49 | if (route is PageRoute) { | 53 | if (route is PageRoute) { |
@@ -100,16 +104,32 @@ class _ShakeImageState extends State<ShakeImage> | @@ -100,16 +104,32 @@ class _ShakeImageState extends State<ShakeImage> | ||
100 | 104 | ||
101 | void _startAnimation() { | 105 | void _startAnimation() { |
102 | Timer(const Duration(seconds: 1), () { | 106 | Timer(const Duration(seconds: 1), () { |
103 | - _controller.forward(from: 0.0); | ||
104 | - _timer = Timer.periodic(const Duration(seconds: 4), (Timer timer) { | ||
105 | - _controller.forward(from: 0.0); | ||
106 | - }); | 107 | + // 检查widget是否还存在且controller是否有效 |
108 | + if (mounted) { | ||
109 | + try { | ||
110 | + _controller.forward(from: 0.0); | ||
111 | + _timer = Timer.periodic(const Duration(seconds: 4), (Timer timer) { | ||
112 | + // 每次执行前都检查widget状态和controller状态 | ||
113 | + if (mounted) { | ||
114 | + try { | ||
115 | + _controller.forward(from: 0.0); | ||
116 | + } catch (e) { | ||
117 | + printLog('Timer callback error: $e'); | ||
118 | + timer.cancel(); | ||
119 | + } | ||
120 | + } | ||
121 | + }); | ||
122 | + } catch (e) { | ||
123 | + printLog('Initial animation error: $e'); | ||
124 | + } | ||
125 | + } | ||
107 | }); | 126 | }); |
108 | printLog('_startAnimation'); | 127 | printLog('_startAnimation'); |
109 | } | 128 | } |
110 | 129 | ||
111 | void _stopAnimation() { | 130 | void _stopAnimation() { |
112 | - _timer.cancel(); | 131 | + _timer?.cancel(); |
132 | + _timer = null; | ||
113 | printLog('_stopAnimation'); | 133 | printLog('_stopAnimation'); |
114 | } | 134 | } |
115 | 135 | ||
@@ -162,9 +182,21 @@ class _ShakeImageState extends State<ShakeImage> | @@ -162,9 +182,21 @@ class _ShakeImageState extends State<ShakeImage> | ||
162 | @override | 182 | @override |
163 | void didChangeAppLifecycleState(AppLifecycleState state) { | 183 | void didChangeAppLifecycleState(AppLifecycleState state) { |
164 | if (state == AppLifecycleState.paused) { | 184 | if (state == AppLifecycleState.paused) { |
165 | - _controller.stop(); | 185 | + if (mounted) { |
186 | + try { | ||
187 | + _controller.stop(); | ||
188 | + } catch (e) { | ||
189 | + printLog('Stop animation error: $e'); | ||
190 | + } | ||
191 | + } | ||
166 | } else if (state == AppLifecycleState.resumed) { | 192 | } else if (state == AppLifecycleState.resumed) { |
167 | - _controller.forward(); | 193 | + if (mounted) { |
194 | + try { | ||
195 | + _controller.forward(); | ||
196 | + } catch (e) { | ||
197 | + printLog('Resume animation error: $e'); | ||
198 | + } | ||
199 | + } | ||
168 | } | 200 | } |
169 | } | 201 | } |
170 | 202 | ||
@@ -172,9 +204,13 @@ class _ShakeImageState extends State<ShakeImage> | @@ -172,9 +204,13 @@ class _ShakeImageState extends State<ShakeImage> | ||
172 | void dispose() { | 204 | void dispose() { |
173 | printLog('dispose'); | 205 | printLog('dispose'); |
174 | customerRouteObserver.unsubscribe(this); | 206 | customerRouteObserver.unsubscribe(this); |
175 | - WidgetsBinding.instance.removeObserver(this); | 207 | + try { |
208 | + WidgetsBinding.instance.removeObserver(this); | ||
209 | + } catch (e) { | ||
210 | + printLog('Remove observer error: $e'); | ||
211 | + } | ||
176 | _controller.dispose(); | 212 | _controller.dispose(); |
177 | - _timer.cancel(); | 213 | + _timer?.cancel(); |
178 | super.dispose(); | 214 | super.dispose(); |
179 | } | 215 | } |
180 | 216 |
lib/pages/reading/bloc/reading_bloc.dart
@@ -293,9 +293,13 @@ class ReadingPageBloc | @@ -293,9 +293,13 @@ class ReadingPageBloc | ||
293 | } | 293 | } |
294 | 294 | ||
295 | Future<void> _playAudio(String? audioUrl) async { | 295 | Future<void> _playAudio(String? audioUrl) async { |
296 | - if (audioUrl!.isNotEmpty) { | ||
297 | - await audioPlayer.play(UrlSource(audioUrl), | ||
298 | - balance: 0.0, ctx: AudioContext()); | 296 | + if (audioUrl != null && audioUrl.isNotEmpty) { |
297 | + try { | ||
298 | + await audioPlayer.play(UrlSource(audioUrl), | ||
299 | + balance: 0.0, ctx: AudioContext()); | ||
300 | + } catch (e) { | ||
301 | + Log.d('_playAudio error: $e'); | ||
302 | + } | ||
299 | } | 303 | } |
300 | } | 304 | } |
301 | 305 | ||
@@ -321,10 +325,10 @@ class ReadingPageBloc | @@ -321,10 +325,10 @@ class ReadingPageBloc | ||
321 | } | 325 | } |
322 | List<SingsoundResultDetailEntity> resultDetails; | 326 | List<SingsoundResultDetailEntity> resultDetails; |
323 | if (currentPageData()?.resultDetails != null) { | 327 | if (currentPageData()?.resultDetails != null) { |
324 | - resultDetails = currentPageData()!.resultDetails!; | 328 | + resultDetails = currentPageData()?.resultDetails ?? []; |
325 | } else { | 329 | } else { |
326 | List<String>? wordList = currentPageData()?.word?.split(RegExp(r'\s+')); | 330 | List<String>? wordList = currentPageData()?.word?.split(RegExp(r'\s+')); |
327 | - resultDetails = wordList! | 331 | + resultDetails = (wordList ?? []) |
328 | .map( | 332 | .map( |
329 | (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) | 333 | (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0)) |
330 | .toList(); | 334 | .toList(); |
lib/pages/section/section_page.dart
@@ -85,13 +85,17 @@ class _SectionPageView extends StatelessWidget { | @@ -85,13 +85,17 @@ class _SectionPageView extends StatelessWidget { | ||
85 | 'courseLessonId': courseLessonId, | 85 | 'courseLessonId': courseLessonId, |
86 | 'isTopic': true | 86 | 'isTopic': true |
87 | }).then((value) { | 87 | }).then((value) { |
88 | - if (value != null) { | 88 | + if (value != null && !bloc.isClosed) { |
89 | Map<String, dynamic> dataMap = | 89 | Map<String, dynamic> dataMap = |
90 | value as Map<String, dynamic>; | 90 | value as Map<String, dynamic>; |
91 | - bloc.add(RequestEndClassEvent( | ||
92 | - dataMap['courseLessonId']!, dataMap['isCompleted'], | ||
93 | - currentTime: dataMap['currentTime'], | ||
94 | - autoNextSection: dataMap['nextSection'])); | 91 | + try { |
92 | + bloc.add(RequestEndClassEvent( | ||
93 | + dataMap['courseLessonId']!, dataMap['isCompleted'], | ||
94 | + currentTime: dataMap['currentTime'], | ||
95 | + autoNextSection: dataMap['nextSection'])); | ||
96 | + } catch (e) { | ||
97 | + print('BLoC add event error: $e'); | ||
98 | + } | ||
95 | } | 99 | } |
96 | AudioPlayerUtil.getInstance() | 100 | AudioPlayerUtil.getInstance() |
97 | .playAudio(AudioPlayerUtilType.countWithMe); | 101 | .playAudio(AudioPlayerUtilType.countWithMe); |
@@ -111,15 +115,19 @@ class _SectionPageView extends StatelessWidget { | @@ -111,15 +115,19 @@ class _SectionPageView extends StatelessWidget { | ||
111 | 'courseLessonId': courseLessonId, | 115 | 'courseLessonId': courseLessonId, |
112 | 'moduleColor': ModuleCache.instance.getCurrentThemeColor() | 116 | 'moduleColor': ModuleCache.instance.getCurrentThemeColor() |
113 | }).then((value) { | 117 | }).then((value) { |
114 | - if (value != null) { | 118 | + if (value != null && !bloc.isClosed) { |
115 | Map<String, dynamic> dataMap = | 119 | Map<String, dynamic> dataMap = |
116 | value as Map<String, dynamic>; | 120 | value as Map<String, dynamic>; |
117 | - bloc.add(RequestEndClassEvent( | ||
118 | - dataMap['courseLessonId']!, | ||
119 | - dataMap['isCompleted'], | ||
120 | - currentStep: dataMap['currentStep'], | ||
121 | - autoNextSection: dataMap['nextSection'], | ||
122 | - )); | 121 | + try { |
122 | + bloc.add(RequestEndClassEvent( | ||
123 | + dataMap['courseLessonId']!, | ||
124 | + dataMap['isCompleted'], | ||
125 | + currentStep: dataMap['currentStep'], | ||
126 | + autoNextSection: dataMap['nextSection'], | ||
127 | + )); | ||
128 | + } catch (e) { | ||
129 | + print('BLoC add event error: $e'); | ||
130 | + } | ||
123 | AudioPlayerUtil.getInstance() | 131 | AudioPlayerUtil.getInstance() |
124 | .playAudio(AudioPlayerUtilType.countWithMe); | 132 | .playAudio(AudioPlayerUtilType.countWithMe); |
125 | } | 133 | } |
@@ -138,13 +146,17 @@ class _SectionPageView extends StatelessWidget { | @@ -138,13 +146,17 @@ class _SectionPageView extends StatelessWidget { | ||
138 | 'courseLessonId': courseLessonId, | 146 | 'courseLessonId': courseLessonId, |
139 | 'moduleColor': ModuleCache.instance.getCurrentThemeColor() | 147 | 'moduleColor': ModuleCache.instance.getCurrentThemeColor() |
140 | }).then((value) { | 148 | }).then((value) { |
141 | - if (value != null) { | 149 | + if (value != null && !bloc.isClosed) { |
142 | Map<String, dynamic> dataMap = | 150 | Map<String, dynamic> dataMap = |
143 | value as Map<String, dynamic>; | 151 | value as Map<String, dynamic>; |
144 | - bloc.add(RequestEndClassEvent( | ||
145 | - dataMap['courseLessonId']!, dataMap['isCompleted'], | ||
146 | - currentStep: dataMap['currentStep'], | ||
147 | - autoNextSection: dataMap['nextSection'])); | 152 | + try { |
153 | + bloc.add(RequestEndClassEvent( | ||
154 | + dataMap['courseLessonId']!, dataMap['isCompleted'], | ||
155 | + currentStep: dataMap['currentStep'], | ||
156 | + autoNextSection: dataMap['nextSection'])); | ||
157 | + } catch (e) { | ||
158 | + print('BLoC add event error: $e'); | ||
159 | + } | ||
148 | } | 160 | } |
149 | AudioPlayerUtil.getInstance() | 161 | AudioPlayerUtil.getInstance() |
150 | .playAudio(AudioPlayerUtilType.countWithMe); | 162 | .playAudio(AudioPlayerUtilType.countWithMe); |
@@ -183,7 +195,9 @@ class _SectionPageView extends StatelessWidget { | @@ -183,7 +195,9 @@ class _SectionPageView extends StatelessWidget { | ||
183 | itemCount: bloc.unlockPageCount(), | 195 | itemCount: bloc.unlockPageCount(), |
184 | controller: bloc.pageController, | 196 | controller: bloc.pageController, |
185 | onPageChanged: (int index) { | 197 | onPageChanged: (int index) { |
186 | - bloc.add(CurrentUnitIndexChangeEvent(index)); | 198 | + if (!bloc.isClosed) { |
199 | + bloc.add(CurrentUnitIndexChangeEvent(index)); | ||
200 | + } | ||
187 | }, | 201 | }, |
188 | itemBuilder: (context, index) { | 202 | itemBuilder: (context, index) { |
189 | return ScrollConfiguration( | 203 | return ScrollConfiguration( |
lib/pages/unit/view.dart
@@ -78,7 +78,7 @@ class UnitPage extends StatelessWidget { | @@ -78,7 +78,7 @@ class UnitPage extends StatelessWidget { | ||
78 | if (value != null) { | 78 | if (value != null) { |
79 | Map<String, dynamic> dataMap = | 79 | Map<String, dynamic> dataMap = |
80 | value as Map<String, dynamic>; | 80 | value as Map<String, dynamic>; |
81 | - bool needRefresh = dataMap['needRefresh']; | 81 | + bool needRefresh = dataMap['needRefresh'] ?? false; |
82 | if (needRefresh) { | 82 | if (needRefresh) { |
83 | bloc.add(RequestUnitDataEvent( | 83 | bloc.add(RequestUnitDataEvent( |
84 | courseModuleEntity?.id)); | 84 | courseModuleEntity?.id)); |
lib/pages/video/lookvideo/widgets/video_widget.dart
@@ -81,7 +81,9 @@ class _VideoWidgetState extends State<VideoWidget> { | @@ -81,7 +81,9 @@ class _VideoWidgetState extends State<VideoWidget> { | ||
81 | 'nextSection': widget.isTopic | 81 | 'nextSection': widget.isTopic |
82 | }); | 82 | }); |
83 | } as VoidCallback, againSectionTap: (() { | 83 | } as VoidCallback, againSectionTap: (() { |
84 | - lookVideoBloc.add(SectionAgainEvent()); | 84 | + if (mounted && !lookVideoBloc.isClosed) { |
85 | + lookVideoBloc.add(SectionAgainEvent()); | ||
86 | + } | ||
85 | }), context: context); | 87 | }), context: context); |
86 | } | 88 | } |
87 | } | 89 | } |
lib/utils/audio_player_util.dart
@@ -30,7 +30,7 @@ enum AudioPlayerUtilType { | @@ -30,7 +30,7 @@ enum AudioPlayerUtilType { | ||
30 | 30 | ||
31 | class AudioPlayerUtil extends WidgetsBindingObserver { | 31 | class AudioPlayerUtil extends WidgetsBindingObserver { |
32 | static AudioPlayerUtil? _instance; | 32 | static AudioPlayerUtil? _instance; |
33 | - late AudioPlayer _audioPlayer; | 33 | + AudioPlayer? _audioPlayer; |
34 | late AudioPlayerUtilType currentType; | 34 | late AudioPlayerUtilType currentType; |
35 | bool _wasPlaying = false; | 35 | bool _wasPlaying = false; |
36 | static const TAG = "AudioPlayerUtil"; | 36 | static const TAG = "AudioPlayerUtil"; |
@@ -43,7 +43,7 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | @@ -43,7 +43,7 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | ||
43 | if (!BasicConfig.isEnvProd()) { | 43 | if (!BasicConfig.isEnvProd()) { |
44 | AudioLogger.logLevel = AudioLogLevel.info; | 44 | AudioLogger.logLevel = AudioLogLevel.info; |
45 | } | 45 | } |
46 | - _audioPlayer.onPlayerStateChanged.listen((event) async { | 46 | + _audioPlayer?.onPlayerStateChanged.listen((event) async { |
47 | Log.d("$TAG onPlayerStateChanged $event _wasPlaying=$_wasPlaying"); | 47 | Log.d("$TAG onPlayerStateChanged $event _wasPlaying=$_wasPlaying"); |
48 | if (event == PlayerState.completed) { | 48 | if (event == PlayerState.completed) { |
49 | // 播放结束再次播放 | 49 | // 播放结束再次播放 |
@@ -70,42 +70,66 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | @@ -70,42 +70,66 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | ||
70 | // 播放音频 | 70 | // 播放音频 |
71 | Future<void> playAudio(AudioPlayerUtilType type) async { | 71 | Future<void> playAudio(AudioPlayerUtilType type) async { |
72 | Log.d('$TAG playAudio begin $type'); | 72 | Log.d('$TAG playAudio begin $type'); |
73 | + if (_audioPlayer == null) { | ||
74 | + Log.d('$TAG playAudio _audioPlayer is null, creating new one'); | ||
75 | + _audioPlayer = AudioPlayer(); | ||
76 | + } | ||
73 | currentType = type; | 77 | currentType = type; |
74 | String path = type.path; | 78 | String path = type.path; |
75 | - await _audioPlayer.play(AssetSource(path.assetMp3), volume: 0.5); | ||
76 | - await _audioPlayer.onPlayerComplete.first; | 79 | + try { |
80 | + await _audioPlayer!.play(AssetSource(path.assetMp3), volume: 0.5); | ||
81 | + await _audioPlayer!.onPlayerComplete.first; | ||
82 | + } catch (e) { | ||
83 | + Log.d('$TAG playAudio error: $e'); | ||
84 | + } | ||
77 | Log.d('$TAG playAudio end $type'); | 85 | Log.d('$TAG playAudio end $type'); |
78 | } | 86 | } |
79 | 87 | ||
80 | // stop | 88 | // stop |
81 | Future<void> stop() async { | 89 | Future<void> stop() async { |
82 | - Log.d("$TAG stop _audioPlayer.state=${_audioPlayer.state}"); | ||
83 | - if (_audioPlayer.state == PlayerState.playing) { | ||
84 | - await _audioPlayer.stop(); | 90 | + if (_audioPlayer == null) return; |
91 | + Log.d("$TAG stop _audioPlayer.state=${_audioPlayer!.state}"); | ||
92 | + try { | ||
93 | + if (_audioPlayer!.state == PlayerState.playing) { | ||
94 | + await _audioPlayer!.stop(); | ||
95 | + } | ||
96 | + } catch (e) { | ||
97 | + Log.d("$TAG stop error: $e"); | ||
85 | } | 98 | } |
86 | } | 99 | } |
87 | 100 | ||
88 | // pause | 101 | // pause |
89 | Future<void> pause() async { | 102 | Future<void> pause() async { |
90 | - Log.d("$TAG pause _audioPlayer.state=${_audioPlayer.state}"); | ||
91 | - if (_audioPlayer.state == PlayerState.playing) { | ||
92 | - await _audioPlayer.pause(); | 103 | + if (_audioPlayer == null) return; |
104 | + Log.d("$TAG pause _audioPlayer.state=${_audioPlayer!.state}"); | ||
105 | + try { | ||
106 | + if (_audioPlayer!.state == PlayerState.playing) { | ||
107 | + await _audioPlayer!.pause(); | ||
108 | + } | ||
109 | + } catch (e) { | ||
110 | + Log.d("$TAG pause error: $e"); | ||
93 | } | 111 | } |
94 | } | 112 | } |
95 | 113 | ||
96 | // resume | 114 | // resume |
97 | Future<void> resume() async { | 115 | Future<void> resume() async { |
98 | - Log.d("$TAG resume _audioPlayer.state=${_audioPlayer.state}"); | ||
99 | - if (_audioPlayer.state == PlayerState.paused) { | ||
100 | - await _audioPlayer.resume(); | 116 | + if (_audioPlayer == null) return; |
117 | + Log.d("$TAG resume _audioPlayer.state=${_audioPlayer!.state}"); | ||
118 | + try { | ||
119 | + if (_audioPlayer!.state == PlayerState.paused) { | ||
120 | + await _audioPlayer!.resume(); | ||
121 | + } | ||
122 | + } catch (e) { | ||
123 | + Log.d("$TAG resume error: $e"); | ||
101 | } | 124 | } |
102 | } | 125 | } |
103 | 126 | ||
104 | @override | 127 | @override |
105 | void didChangeAppLifecycleState(AppLifecycleState state) async { | 128 | void didChangeAppLifecycleState(AppLifecycleState state) async { |
106 | - Log.d("$TAG didChangeAppLifecycleState appState=$state _wasPlaying=$_wasPlaying _audioPlayer.state=${_audioPlayer.state}"); | 129 | + if (_audioPlayer == null) return; |
130 | + Log.d("$TAG didChangeAppLifecycleState appState=$state _wasPlaying=$_wasPlaying _audioPlayer.state=${_audioPlayer!.state}"); | ||
107 | if (state == AppLifecycleState.paused) { | 131 | if (state == AppLifecycleState.paused) { |
108 | - if (_audioPlayer.state == PlayerState.playing) { | 132 | + if (_audioPlayer!.state == PlayerState.playing) { |
109 | _wasPlaying = true; | 133 | _wasPlaying = true; |
110 | await pause(); | 134 | await pause(); |
111 | }; | 135 | }; |
@@ -118,12 +142,15 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | @@ -118,12 +142,15 @@ class AudioPlayerUtil extends WidgetsBindingObserver { | ||
118 | } | 142 | } |
119 | 143 | ||
120 | bool isPlaying() { | 144 | bool isPlaying() { |
121 | - return _audioPlayer.state == PlayerState.playing; | 145 | + return _audioPlayer?.state == PlayerState.playing; |
122 | } | 146 | } |
123 | 147 | ||
124 | void dispose() { | 148 | void dispose() { |
125 | - Log.d("$TAG dispose _audioPlayer.state=${_audioPlayer.state}"); | ||
126 | - _audioPlayer.dispose(); | 149 | + if (_audioPlayer != null) { |
150 | + Log.d("$TAG dispose _audioPlayer.state=${_audioPlayer!.state}"); | ||
151 | + _audioPlayer!.dispose(); | ||
152 | + _audioPlayer = null; | ||
153 | + } | ||
127 | WidgetsBinding.instance.removeObserver(this); | 154 | WidgetsBinding.instance.removeObserver(this); |
128 | } | 155 | } |
129 | } | 156 | } |