Files
MrbWebLibre/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart
T

196 lines
6.4 KiB
Dart

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import 'dart:async';
import 'dart:developer' as developer;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart';
class GeckoView extends StatefulWidget {
/// Whether the native container backing this platform view is attached to the
/// window, as reported by `NativeFragmentView`.
///
/// The browser fragment can only be attached while this holds `true`, and the
/// container is only inserted into the Flutter view hierarchy once the
/// platform-view layer is first composited — which an ancestor that lays the
/// view out without painting it (`Offstage`) defers for as long as it stays
/// offstage. Every attach attempt is therefore driven by this stream rather
/// than by a single burst of retries after creation, which would otherwise
/// expire while the container is still unreachable and never run again.
/// See https://github.com/FaFre/WebLibre/issues/557.
final Stream<bool> viewReadyEvents;
final Future<void> Function()? postInitializationStep;
const GeckoView({
super.key,
required this.viewReadyEvents,
this.postInitializationStep,
});
@override
State<GeckoView> createState() => _GeckoViewState();
}
class _GeckoViewState extends State<GeckoView> {
static const platform = MethodChannel(
'eu.weblibre.flutter_mozilla_components/trim_memory',
);
final browserService = GeckoBrowserService();
late final AppLifecycleListener _listener;
StreamSubscription<bool>? _viewReadySubscription;
/// Serialises attach attempts.
///
/// The container can be reported attached while an earlier attempt is still
/// retrying, and two concurrent attempts would both find no usable fragment
/// and race to replace each other's.
Future<void> _attachQueue = Future<void>.value();
@override
void initState() {
super.initState();
_setupMethodCallHandler();
_listener = AppLifecycleListener(
onResume: () {
//Make sure fragment visible after resuming the app in case native resources have been disposed
unawaited(_enqueueShowNativeFragment());
},
);
_viewReadySubscription = widget.viewReadyEvents
.where((ready) => ready)
.listen((_) => unawaited(_enqueueShowNativeFragment()));
}
/// Queues an attach attempt behind any that is still running.
///
/// Returns when this attempt is done, so callers that need to sequence work
/// after it can await it; failures are contained so one bad attempt cannot
/// poison the queue for the ones the ready stream triggers later.
Future<void> _enqueueShowNativeFragment() {
final attempt = _attachQueue.then((_) async {
if (!mounted) {
return;
}
try {
await _showNativeFragment();
} catch (error, stackTrace) {
developer.log(
'Fragment attach attempt failed',
name: 'GeckoView',
level: 900,
error: error,
stackTrace: stackTrace,
);
}
});
_attachQueue = attempt;
return attempt;
}
void _setupMethodCallHandler() {
platform.setMethodCallHandler((MethodCall call) async {
if (call.method == 'onTrimMemory') {
await browserService.onTrimMemory(call.arguments as int);
}
});
}
/// Attaches the browser fragment to the native container.
///
/// The retries cover the transient reasons an attach can fail once the
/// container is reachable — a saved fragment-manager state, a frame in which
/// the fragment's view has no size yet. They deliberately do *not* cover
/// waiting for the container to appear in the first place: that wait is
/// unbounded, and [viewReadyEvents] reports it instead.
Future<bool> _showNativeFragment({
int maxRetries = 10,
/// Default ist about one frame
Duration retryDelay = const Duration(milliseconds: 1000 ~/ 60),
}) async {
for (int attempt = 0; attempt < maxRetries; attempt++) {
final result = await browserService.showNativeFragment();
if (result) {
developer.log(
'Fragment ATTACHED after $attempt tries',
name: 'GeckoView',
);
return true;
}
if (attempt < maxRetries - 1) {
await Future.delayed(retryDelay);
}
}
developer.log(
'Fragment FAILED after $maxRetries tries',
name: 'GeckoView',
level: 900,
);
return false;
}
@override
void dispose() {
platform.setMethodCallHandler(null);
unawaited(_viewReadySubscription?.cancel());
_listener.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: 'eu.weblibre/gecko',
surfaceFactory: (context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initExpensiveAndroidView(
id: params.id,
viewType: 'eu.weblibre/gecko',
layoutDirection: TextDirection.ltr,
creationParams: {},
creationParamsCodec: const StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener((value) {
params.onPlatformViewCreated(value);
SchedulerBinding.instance.addPostFrameCallback((_) async {
// A first attempt for the common case where the view is painted
// from the frame it is created in, so the container is already
// attached by now. When it is not, this attempt is cheap and the
// ready subscription takes over as soon as it becomes attached.
await _enqueueShowNativeFragment();
await widget.postInitializationStep?.call();
});
})
// ignore: discarded_futures that hos it is done in docs
..create();
},
);
}
}