Commit c272c662bd42c755c8ebf73b721a3a64bac78bed

Authored by 吴启风
1 parent 46385ad0

fix: 崩溃修复

lib/app/splash_page.dart
... ... @@ -140,19 +140,22 @@ class _TransitionViewState extends State<TransitionView> {
140 140 actions: <Widget>[
141 141 TextButton(
142 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 152 TextButton(
150 153 child: const Text('重试'),
151 154 onPressed: () async {
152 155 // 关闭对话框并重试
153 156 Navigator.of(context).pop();
154 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&lt;PermissionRequestPage&gt;
30 30 bool _isGoSetting = false;
31 31 late final List<String> msgList;
32 32 bool _isDialogShowing = false;
  33 + bool _isRequestingPermission = false;
33 34  
34 35 @override
35 36 void initState() {
... ... @@ -107,6 +108,11 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
107 108  
108 109 /// 校验权限
109 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 117 List<Permission> intentPermissionList = [];
112 118  
... ... @@ -129,37 +135,51 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
129 135  
130 136 ///实际触发请求权限
131 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 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&lt;bool&gt; requestPermissions(
35 35 if (allGranted) {
36 36 return true;
37 37 } else {
38   - return await Navigator.of(context).push(PageRouteBuilder(
  38 + final result = await Navigator.of(context).push(PageRouteBuilder(
39 39 opaque: false,
40 40 pageBuilder: ((context, animation, secondaryAnimation) {
41 41 return PermissionRequestPage(permissions, permissionNames, permissionDesc,
42 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 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 8 class ApiResponse<T> {
7 9 int? code;
8 10 String? msg;
... ...
lib/pages/home/widgets/ShakeImage.dart
... ... @@ -20,7 +20,7 @@ class _ShakeImageState extends State&lt;ShakeImage&gt;
20 20 with SingleTickerProviderStateMixin, WidgetsBindingObserver, RouteAware {
21 21 late AnimationController _controller;
22 22 late Animation<double> _animation;
23   - late Timer _timer;
  23 + Timer? _timer;
24 24 late final AppLifecycleListener appLifecycleListener;
25 25 static const TAG = 'ShakeImage';
26 26  
... ... @@ -43,7 +43,11 @@ class _ShakeImageState extends State&lt;ShakeImage&gt;
43 43 );
44 44  
45 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 51 WidgetsBinding.instance.addPostFrameCallback((_) {
48 52 final route = ModalRoute.of(context);
49 53 if (route is PageRoute) {
... ... @@ -100,16 +104,32 @@ class _ShakeImageState extends State&lt;ShakeImage&gt;
100 104  
101 105 void _startAnimation() {
102 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 127 printLog('_startAnimation');
109 128 }
110 129  
111 130 void _stopAnimation() {
112   - _timer.cancel();
  131 + _timer?.cancel();
  132 + _timer = null;
113 133 printLog('_stopAnimation');
114 134 }
115 135  
... ... @@ -162,9 +182,21 @@ class _ShakeImageState extends State&lt;ShakeImage&gt;
162 182 @override
163 183 void didChangeAppLifecycleState(AppLifecycleState state) {
164 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 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&lt;ShakeImage&gt;
172 204 void dispose() {
173 205 printLog('dispose');
174 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 212 _controller.dispose();
177   - _timer.cancel();
  213 + _timer?.cancel();
178 214 super.dispose();
179 215 }
180 216  
... ...
lib/pages/reading/bloc/reading_bloc.dart
... ... @@ -293,9 +293,13 @@ class ReadingPageBloc
293 293 }
294 294  
295 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 325 }
322 326 List<SingsoundResultDetailEntity> resultDetails;
323 327 if (currentPageData()?.resultDetails != null) {
324   - resultDetails = currentPageData()!.resultDetails!;
  328 + resultDetails = currentPageData()?.resultDetails ?? [];
325 329 } else {
326 330 List<String>? wordList = currentPageData()?.word?.split(RegExp(r'\s+'));
327   - resultDetails = wordList!
  331 + resultDetails = (wordList ?? [])
328 332 .map(
329 333 (word) => SingsoundResultDetailEntity.withCharAndScore(word, 0))
330 334 .toList();
... ...
lib/pages/section/section_page.dart
... ... @@ -85,13 +85,17 @@ class _SectionPageView extends StatelessWidget {
85 85 'courseLessonId': courseLessonId,
86 86 'isTopic': true
87 87 }).then((value) {
88   - if (value != null) {
  88 + if (value != null && !bloc.isClosed) {
89 89 Map<String, dynamic> dataMap =
90 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 100 AudioPlayerUtil.getInstance()
97 101 .playAudio(AudioPlayerUtilType.countWithMe);
... ... @@ -111,15 +115,19 @@ class _SectionPageView extends StatelessWidget {
111 115 'courseLessonId': courseLessonId,
112 116 'moduleColor': ModuleCache.instance.getCurrentThemeColor()
113 117 }).then((value) {
114   - if (value != null) {
  118 + if (value != null && !bloc.isClosed) {
115 119 Map<String, dynamic> dataMap =
116 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 131 AudioPlayerUtil.getInstance()
124 132 .playAudio(AudioPlayerUtilType.countWithMe);
125 133 }
... ... @@ -138,13 +146,17 @@ class _SectionPageView extends StatelessWidget {
138 146 'courseLessonId': courseLessonId,
139 147 'moduleColor': ModuleCache.instance.getCurrentThemeColor()
140 148 }).then((value) {
141   - if (value != null) {
  149 + if (value != null && !bloc.isClosed) {
142 150 Map<String, dynamic> dataMap =
143 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 161 AudioPlayerUtil.getInstance()
150 162 .playAudio(AudioPlayerUtilType.countWithMe);
... ... @@ -183,7 +195,9 @@ class _SectionPageView extends StatelessWidget {
183 195 itemCount: bloc.unlockPageCount(),
184 196 controller: bloc.pageController,
185 197 onPageChanged: (int index) {
186   - bloc.add(CurrentUnitIndexChangeEvent(index));
  198 + if (!bloc.isClosed) {
  199 + bloc.add(CurrentUnitIndexChangeEvent(index));
  200 + }
187 201 },
188 202 itemBuilder: (context, index) {
189 203 return ScrollConfiguration(
... ...
lib/pages/unit/view.dart
... ... @@ -78,7 +78,7 @@ class UnitPage extends StatelessWidget {
78 78 if (value != null) {
79 79 Map<String, dynamic> dataMap =
80 80 value as Map<String, dynamic>;
81   - bool needRefresh = dataMap['needRefresh'];
  81 + bool needRefresh = dataMap['needRefresh'] ?? false;
82 82 if (needRefresh) {
83 83 bloc.add(RequestUnitDataEvent(
84 84 courseModuleEntity?.id));
... ...
lib/pages/video/lookvideo/widgets/video_widget.dart
... ... @@ -81,7 +81,9 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
81 81 'nextSection': widget.isTopic
82 82 });
83 83 } as VoidCallback, againSectionTap: (() {
84   - lookVideoBloc.add(SectionAgainEvent());
  84 + if (mounted && !lookVideoBloc.isClosed) {
  85 + lookVideoBloc.add(SectionAgainEvent());
  86 + }
85 87 }), context: context);
86 88 }
87 89 }
... ...
lib/utils/audio_player_util.dart
... ... @@ -30,7 +30,7 @@ enum AudioPlayerUtilType {
30 30  
31 31 class AudioPlayerUtil extends WidgetsBindingObserver {
32 32 static AudioPlayerUtil? _instance;
33   - late AudioPlayer _audioPlayer;
  33 + AudioPlayer? _audioPlayer;
34 34 late AudioPlayerUtilType currentType;
35 35 bool _wasPlaying = false;
36 36 static const TAG = "AudioPlayerUtil";
... ... @@ -43,7 +43,7 @@ class AudioPlayerUtil extends WidgetsBindingObserver {
43 43 if (!BasicConfig.isEnvProd()) {
44 44 AudioLogger.logLevel = AudioLogLevel.info;
45 45 }
46   - _audioPlayer.onPlayerStateChanged.listen((event) async {
  46 + _audioPlayer?.onPlayerStateChanged.listen((event) async {
47 47 Log.d("$TAG onPlayerStateChanged $event _wasPlaying=$_wasPlaying");
48 48 if (event == PlayerState.completed) {
49 49 // 播放结束再次播放
... ... @@ -70,42 +70,66 @@ class AudioPlayerUtil extends WidgetsBindingObserver {
70 70 // 播放音频
71 71 Future<void> playAudio(AudioPlayerUtilType type) async {
72 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 77 currentType = type;
74 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 85 Log.d('$TAG playAudio end $type');
78 86 }
79 87  
80 88 // stop
81 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 101 // pause
89 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 114 // resume
97 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 127 @override
105 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 131 if (state == AppLifecycleState.paused) {
108   - if (_audioPlayer.state == PlayerState.playing) {
  132 + if (_audioPlayer!.state == PlayerState.playing) {
109 133 _wasPlaying = true;
110 134 await pause();
111 135 };
... ... @@ -118,12 +142,15 @@ class AudioPlayerUtil extends WidgetsBindingObserver {
118 142 }
119 143  
120 144 bool isPlaying() {
121   - return _audioPlayer.state == PlayerState.playing;
  145 + return _audioPlayer?.state == PlayerState.playing;
122 146 }
123 147  
124 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 154 WidgetsBinding.instance.removeObserver(this);
128 155 }
129 156 }
... ...