feat: 优化 H5 线路切换逻辑;实现独立 WebView 管理和状态控制
This commit is contained in:
212
lib/main.dart
212
lib/main.dart
@@ -93,67 +93,68 @@ class H5ShellPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _H5ShellPageState extends State<H5ShellPage> {
|
||||
late final WebViewController _controller;
|
||||
late final List<H5Line> _h5Lines;
|
||||
late final List<_H5LineWebViewSlot> _lineSlots;
|
||||
|
||||
int _progress = 0;
|
||||
String? _loadError;
|
||||
bool _showShellCover = true;
|
||||
bool _shellBrandingLoaded = false;
|
||||
int _currentLineIndex = 0;
|
||||
Timer? _shellCoverFallbackTimer;
|
||||
late ShellBranding _shellBranding;
|
||||
|
||||
H5Line get _currentLine => _h5Lines[_currentLineIndex];
|
||||
_H5LineWebViewSlot get _currentSlot => _lineSlots[_currentLineIndex];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_h5Lines = AppConfig.h5Lines;
|
||||
_shellBranding = widget.initialShellBranding;
|
||||
_controller = _buildController();
|
||||
unawaited(_loadHome());
|
||||
_lineSlots = [
|
||||
for (var index = 0; index < _h5Lines.length; index += 1)
|
||||
_H5LineWebViewSlot(
|
||||
line: _h5Lines[index],
|
||||
controller: _buildController(index),
|
||||
),
|
||||
];
|
||||
unawaited(_ensureLineLoaded(_currentLineIndex));
|
||||
}
|
||||
|
||||
WebViewController _buildController() {
|
||||
WebViewController _buildController(int lineIndex) {
|
||||
return WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..addJavaScriptChannel(
|
||||
'OpenIMShell',
|
||||
onMessageReceived: _handleShellMessage,
|
||||
onMessageReceived: (message) => _handleShellMessage(lineIndex, message),
|
||||
)
|
||||
..setBackgroundColor(_shellBackground)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (progress) {
|
||||
if (mounted) {
|
||||
setState(() => _progress = progress);
|
||||
setState(() => _lineSlots[lineIndex].progress = progress);
|
||||
}
|
||||
},
|
||||
onPageStarted: (url) {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
final lineIndex = AppConfig.lineIndexForUrl(url);
|
||||
onPageStarted: (_) {
|
||||
final slot = _lineSlots[lineIndex];
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (lineIndex != null) {
|
||||
_currentLineIndex = lineIndex;
|
||||
}
|
||||
_loadError = null;
|
||||
_progress = 0;
|
||||
_showShellCover = true;
|
||||
slot.loadError = null;
|
||||
slot.progress = 0;
|
||||
slot.showShellCover = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
unawaited(_handlePageFinished());
|
||||
unawaited(_handlePageFinished(lineIndex));
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
if (error.isForMainFrame ?? true) {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
final slot = _lineSlots[lineIndex];
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loadError = error.description;
|
||||
_showShellCover = false;
|
||||
slot.loadError = error.description;
|
||||
slot.showShellCover = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -165,63 +166,63 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
for (final slot in _lineSlots) {
|
||||
slot.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _runJavaScriptSafely(String source) async {
|
||||
Future<void> _runJavaScriptSafely(int lineIndex, String source) async {
|
||||
try {
|
||||
await _controller.runJavaScript(source);
|
||||
await _lineSlots[lineIndex].controller.runJavaScript(source);
|
||||
} catch (_) {
|
||||
// WebView can reject JavaScript while a page is still navigating.
|
||||
}
|
||||
}
|
||||
|
||||
String _freshHomeUrl() {
|
||||
return AppConfig.homeUrl(lineIndex: _currentLineIndex);
|
||||
}
|
||||
|
||||
Future<void> _handlePageFinished() async {
|
||||
Future<void> _handlePageFinished(int lineIndex) async {
|
||||
await _loadShellBrandingIfNeeded();
|
||||
await _syncShellBranding();
|
||||
await _syncShellBranding(lineIndex);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progress = 100;
|
||||
_lineSlots[lineIndex].progress = 100;
|
||||
});
|
||||
}
|
||||
_scheduleShellCoverFallback();
|
||||
_scheduleShellCoverFallback(lineIndex);
|
||||
}
|
||||
|
||||
void _handleShellMessage(JavaScriptMessage message) {
|
||||
void _handleShellMessage(int lineIndex, JavaScriptMessage message) {
|
||||
try {
|
||||
final decoded = jsonDecode(message.message);
|
||||
if (decoded is Map && decoded['type'] == 'first-screen-ready') {
|
||||
_hideShellCover();
|
||||
_hideShellCover(lineIndex);
|
||||
}
|
||||
} catch (_) {
|
||||
if (message.message == 'first-screen-ready') {
|
||||
_hideShellCover();
|
||||
_hideShellCover(lineIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleShellCoverFallback() {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
_shellCoverFallbackTimer = Timer(
|
||||
void _scheduleShellCoverFallback(int lineIndex) {
|
||||
final slot = _lineSlots[lineIndex];
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
slot.shellCoverFallbackTimer = Timer(
|
||||
const Duration(seconds: 4),
|
||||
_hideShellCover,
|
||||
() => _hideShellCover(lineIndex),
|
||||
);
|
||||
}
|
||||
|
||||
void _hideShellCover() {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
if (!mounted || !_showShellCover) {
|
||||
void _hideShellCover(int lineIndex) {
|
||||
final slot = _lineSlots[lineIndex];
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
if (!mounted || !slot.showShellCover) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_progress = 100;
|
||||
_showShellCover = false;
|
||||
slot.progress = 100;
|
||||
slot.showShellCover = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,7 +243,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _syncShellBranding() {
|
||||
Future<void> _syncShellBranding(int lineIndex) {
|
||||
final payload = jsonEncode({
|
||||
'name': _shellBranding.appName,
|
||||
'logo': _shellBranding.appLogo,
|
||||
@@ -256,30 +257,44 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
} catch (_) {}
|
||||
})();
|
||||
''';
|
||||
return _runJavaScriptSafely(script);
|
||||
return _runJavaScriptSafely(lineIndex, script);
|
||||
}
|
||||
|
||||
Future<void> _loadHome() async {
|
||||
await _loadUrl(_freshHomeUrl());
|
||||
}
|
||||
|
||||
Future<void> _loadUrl(String url) async {
|
||||
final targetUrl = AppConfig.canonicalizeMainFrameUrl(url);
|
||||
final lineIndex = AppConfig.lineIndexForUrl(targetUrl);
|
||||
Future<void> _ensureLineLoaded(int lineIndex) async {
|
||||
final slot = _lineSlots[lineIndex];
|
||||
if (slot.hasLoadedInitialRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
slot.hasLoadedInitialRequest = true;
|
||||
if (mounted) {
|
||||
_shellCoverFallbackTimer?.cancel();
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
setState(() {
|
||||
if (lineIndex != null) {
|
||||
_currentLineIndex = lineIndex;
|
||||
}
|
||||
_loadError = null;
|
||||
_progress = 0;
|
||||
_showShellCover = true;
|
||||
slot.loadError = null;
|
||||
slot.progress = 0;
|
||||
slot.showShellCover = true;
|
||||
});
|
||||
}
|
||||
|
||||
await _controller.loadRequest(Uri.parse(targetUrl));
|
||||
await slot.controller.loadRequest(Uri.parse(slot.line.url));
|
||||
}
|
||||
|
||||
Future<void> _reloadCurrentLine() async {
|
||||
final slot = _currentSlot;
|
||||
slot.shellCoverFallbackTimer?.cancel();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
slot.loadError = null;
|
||||
slot.progress = 0;
|
||||
slot.showShellCover = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (slot.hasLoadedInitialRequest) {
|
||||
await slot.controller.reload();
|
||||
} else {
|
||||
await _ensureLineLoaded(_currentLineIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadLine(int index) async {
|
||||
@@ -290,7 +305,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
});
|
||||
}
|
||||
|
||||
await _loadUrl(_h5Lines[safeIndex].url);
|
||||
await _ensureLineLoaded(safeIndex);
|
||||
}
|
||||
|
||||
void _showLineSwitcher() {
|
||||
@@ -327,11 +342,6 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
|
||||
const webSchemes = {'http', 'https', 'about', 'data'};
|
||||
if (webSchemes.contains(uri.scheme)) {
|
||||
if (request.isMainFrame &&
|
||||
AppConfig.shouldRewriteMainFrameUrl(request.url)) {
|
||||
unawaited(_loadUrl(AppConfig.canonicalizeMainFrameUrl(request.url)));
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
}
|
||||
|
||||
@@ -345,8 +355,9 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
}
|
||||
|
||||
Future<void> _handleBackNavigation() async {
|
||||
if (await _controller.canGoBack()) {
|
||||
await _controller.goBack();
|
||||
final controller = _currentSlot.controller;
|
||||
if (await controller.canGoBack()) {
|
||||
await controller.goBack();
|
||||
} else {
|
||||
await SystemNavigator.pop();
|
||||
}
|
||||
@@ -354,9 +365,11 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentSlot = _currentSlot;
|
||||
final bottomInset = MediaQuery.viewInsetsOf(context).bottom;
|
||||
final showLineSwitch =
|
||||
!_showShellCover && _loadError == null && bottomInset == 0;
|
||||
final showLineSwitch = !currentSlot.showShellCover &&
|
||||
currentSlot.loadError == null &&
|
||||
bottomInset == 0;
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
@@ -371,23 +384,38 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
bottom: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _ShellFallback(progress: _progress)),
|
||||
WebViewWidget(controller: _controller),
|
||||
if (_showShellCover)
|
||||
Positioned.fill(
|
||||
child: _ShellFallback(progress: currentSlot.progress),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IndexedStack(
|
||||
index: _currentLineIndex,
|
||||
children: [
|
||||
for (var index = 0; index < _lineSlots.length; index += 1)
|
||||
WebViewWidget(
|
||||
key: ValueKey<int>(index),
|
||||
controller: _lineSlots[index].controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (currentSlot.showShellCover)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: _ShellFallback(progress: _progress),
|
||||
child: _ShellFallback(progress: currentSlot.progress),
|
||||
),
|
||||
),
|
||||
if (!_showShellCover && _progress < 100)
|
||||
if (!currentSlot.showShellCover && currentSlot.progress < 100)
|
||||
LinearProgressIndicator(
|
||||
value: _progress == 0 ? null : _progress / 100,
|
||||
value: currentSlot.progress == 0
|
||||
? null
|
||||
: currentSlot.progress / 100,
|
||||
minHeight: 2,
|
||||
),
|
||||
if (_loadError != null)
|
||||
if (currentSlot.loadError != null)
|
||||
_ErrorPanel(
|
||||
message: _loadError!,
|
||||
onRetry: () => unawaited(_loadHome()),
|
||||
message: currentSlot.loadError!,
|
||||
onRetry: () => unawaited(_reloadCurrentLine()),
|
||||
),
|
||||
if (showLineSwitch)
|
||||
Positioned(
|
||||
@@ -407,6 +435,26 @@ class _H5ShellPageState extends State<H5ShellPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _H5LineWebViewSlot {
|
||||
_H5LineWebViewSlot({
|
||||
required this.line,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final H5Line line;
|
||||
final WebViewController controller;
|
||||
|
||||
int progress = 0;
|
||||
String? loadError;
|
||||
bool showShellCover = true;
|
||||
bool hasLoadedInitialRequest = false;
|
||||
Timer? shellCoverFallbackTimer;
|
||||
|
||||
void dispose() {
|
||||
shellCoverFallbackTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorPanel extends StatelessWidget {
|
||||
const _ErrorPanel({required this.message, required this.onRetry});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user