feat: 增加后台恢复处理,优化 WebView 状态管理

This commit is contained in:
Booker
2026-06-09 13:24:50 +07:00
parent c6887b6583
commit 294f3d8af6

View File

@@ -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();