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 _h5DomainCacheUrlKey = 'h5_random_domain_url_v1';
|
||||||
const _lineProbeTimeout = Duration(seconds: 5);
|
const _lineProbeTimeout = Duration(seconds: 5);
|
||||||
const _maxGeneratedLineAttempts = 6;
|
const _maxGeneratedLineAttempts = 6;
|
||||||
|
const _longBackgroundRefreshThreshold = Duration(minutes: 20);
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -115,6 +116,8 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
int _keyboardSyncToken = 0;
|
int _keyboardSyncToken = 0;
|
||||||
int _lineGeneration = 0;
|
int _lineGeneration = 0;
|
||||||
int _h5LineAttempt = 0;
|
int _h5LineAttempt = 0;
|
||||||
|
int _resumeRecoveryToken = 0;
|
||||||
|
DateTime? _backgroundedAt;
|
||||||
late ShellBranding _shellBranding;
|
late ShellBranding _shellBranding;
|
||||||
|
|
||||||
_H5LineWebViewSlot get _currentSlot => _lineSlot;
|
_H5LineWebViewSlot get _currentSlot => _lineSlot;
|
||||||
@@ -244,10 +247,19 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.paused ||
|
||||||
|
state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.detached) {
|
||||||
|
_backgroundedAt ??= DateTime.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
unawaited(_syncShellRequestUrl());
|
final backgroundDuration = _backgroundedAt == null
|
||||||
unawaited(_syncShellBranding());
|
? Duration.zero
|
||||||
unawaited(_syncKeyboardState());
|
: DateTime.now().difference(_backgroundedAt!);
|
||||||
|
_backgroundedAt = null;
|
||||||
|
unawaited(_recoverWebViewAfterResume(backgroundDuration));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,6 +641,97 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
return _runJavaScriptSafely(script);
|
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({
|
Future<void> _handleShellMediaPermissionRequest({
|
||||||
required int generation,
|
required int generation,
|
||||||
required String requestId,
|
required String requestId,
|
||||||
@@ -769,7 +872,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
await _syncKeyboardState();
|
await _syncKeyboardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reloadCurrentLine() async {
|
Future<void> _reloadCurrentLine({bool forceRefresh = false}) async {
|
||||||
final slot = _currentSlot;
|
final slot = _currentSlot;
|
||||||
if (slot.availability == _LineAvailability.unavailable ||
|
if (slot.availability == _LineAvailability.unavailable ||
|
||||||
slot.loadError != null) {
|
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();
|
await slot.controller.reload();
|
||||||
} else {
|
} else {
|
||||||
await _loadCurrentLine();
|
await _loadCurrentLine();
|
||||||
|
|||||||
Reference in New Issue
Block a user