feat: 增加后台恢复处理,优化 WebView 状态管理
This commit is contained in:
117
lib/main.dart
117
lib/main.dart
@@ -27,6 +27,7 @@ const _keyboardAnimationDuration = Duration(milliseconds: 250);
|
||||
const _h5DomainCacheUrlKey = 'h5_random_domain_url_v1';
|
||||
const _lineProbeTimeout = Duration(seconds: 5);
|
||||
const _maxGeneratedLineAttempts = 6;
|
||||
const _longBackgroundRefreshThreshold = Duration(minutes: 20);
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -115,6 +116,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
int _keyboardSyncToken = 0;
|
||||
int _lineGeneration = 0;
|
||||
int _h5LineAttempt = 0;
|
||||
int _resumeRecoveryToken = 0;
|
||||
DateTime? _backgroundedAt;
|
||||
late ShellBranding _shellBranding;
|
||||
|
||||
_H5LineWebViewSlot get _currentSlot => _lineSlot;
|
||||
@@ -244,10 +247,19 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive ||
|
||||
state == AppLifecycleState.detached) {
|
||||
_backgroundedAt ??= DateTime.now();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
unawaited(_syncShellRequestUrl());
|
||||
unawaited(_syncShellBranding());
|
||||
unawaited(_syncKeyboardState());
|
||||
final backgroundDuration = _backgroundedAt == null
|
||||
? Duration.zero
|
||||
: DateTime.now().difference(_backgroundedAt!);
|
||||
_backgroundedAt = null;
|
||||
unawaited(_recoverWebViewAfterResume(backgroundDuration));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,6 +641,97 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
return _runJavaScriptSafely(script);
|
||||
}
|
||||
|
||||
Future<void> _notifyH5AppResumed(Duration backgroundDuration) {
|
||||
final payload = jsonEncode({
|
||||
'backgroundDurationMs': backgroundDuration.inMilliseconds,
|
||||
'refreshedAt': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
final script = '''
|
||||
(() => {
|
||||
try {
|
||||
const detail = $payload;
|
||||
window.dispatchEvent(new CustomEvent('openim-shell-app-resumed', { detail }));
|
||||
} catch (_) {}
|
||||
})();
|
||||
''';
|
||||
return _runJavaScriptSafely(script);
|
||||
}
|
||||
|
||||
Future<bool> _isH5RuntimeResponsive() async {
|
||||
try {
|
||||
final result = await _lineSlot.controller.runJavaScriptReturningResult(
|
||||
'''
|
||||
(() => {
|
||||
try {
|
||||
return JSON.stringify({
|
||||
marker: 'openim-h5-runtime-alive',
|
||||
readyState: document.readyState,
|
||||
href: window.location.href,
|
||||
now: Date.now()
|
||||
});
|
||||
} catch (_) {
|
||||
return 'openim-h5-runtime-error';
|
||||
}
|
||||
})();
|
||||
''',
|
||||
).timeout(const Duration(seconds: 2));
|
||||
return result.toString().contains('openim-h5-runtime-alive');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> _currentReloadUri({required bool cacheBust}) async {
|
||||
String? currentUrl;
|
||||
try {
|
||||
currentUrl = await _lineSlot.controller.currentUrl();
|
||||
} catch (_) {
|
||||
currentUrl = null;
|
||||
}
|
||||
|
||||
final parsedCurrentUrl =
|
||||
currentUrl == null ? null : Uri.tryParse(currentUrl);
|
||||
final uri = parsedCurrentUrl?.hasScheme == true
|
||||
? parsedCurrentUrl!
|
||||
: Uri.parse(_lineSlot.line.url);
|
||||
|
||||
if (!cacheBust) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
||||
..['_shell_resume_ts'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
return uri.replace(queryParameters: queryParameters);
|
||||
}
|
||||
|
||||
Future<void> _recoverWebViewAfterResume(Duration backgroundDuration) async {
|
||||
final token = ++_resumeRecoveryToken;
|
||||
final generation = _lineGeneration;
|
||||
await _syncShellRequestUrl();
|
||||
await _syncShellBranding();
|
||||
await _syncKeyboardState();
|
||||
await _notifyH5AppResumed(backgroundDuration);
|
||||
|
||||
if (!_isCurrentLineGeneration(generation) ||
|
||||
token != _resumeRecoveryToken ||
|
||||
backgroundDuration < _longBackgroundRefreshThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
final responsive = await _isH5RuntimeResponsive();
|
||||
if (!mounted ||
|
||||
!_isCurrentLineGeneration(generation) ||
|
||||
token != _resumeRecoveryToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
_logH5LineDebug(
|
||||
'后台 ${backgroundDuration.inMinutes} 分钟后恢复,'
|
||||
'${responsive ? '刷新 WebView 以拉取最新 H5' : 'WebView JS 无响应,重载当前页'}',
|
||||
);
|
||||
await _reloadCurrentLine(forceRefresh: true);
|
||||
}
|
||||
|
||||
Future<void> _handleShellMediaPermissionRequest({
|
||||
required int generation,
|
||||
required String requestId,
|
||||
@@ -769,7 +872,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
await _syncKeyboardState();
|
||||
}
|
||||
|
||||
Future<void> _reloadCurrentLine() async {
|
||||
Future<void> _reloadCurrentLine({bool forceRefresh = false}) async {
|
||||
final slot = _currentSlot;
|
||||
if (slot.availability == _LineAvailability.unavailable ||
|
||||
slot.loadError != null) {
|
||||
@@ -787,7 +890,11 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
if (slot.hasLoadedInitialRequest) {
|
||||
if (forceRefresh) {
|
||||
await slot.controller.loadRequest(
|
||||
await _currentReloadUri(cacheBust: true),
|
||||
);
|
||||
} else if (slot.hasLoadedInitialRequest) {
|
||||
await slot.controller.reload();
|
||||
} else {
|
||||
await _loadCurrentLine();
|
||||
|
||||
Reference in New Issue
Block a user