Fix crashes when current route parsing transactions are discarded (#100657)
* Fix crashes when current route parsing transactions are discarded * refactor * update
This commit is contained in:
parent
d60272a203
commit
26398e6821
@ -466,7 +466,7 @@ class Router<T> extends StatefulWidget {
|
||||
}
|
||||
|
||||
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q);
|
||||
typedef _DelegateRouteSetter<T> = Future<void> Function(T);
|
||||
typedef _RouteSetter<T> = Future<void> Function(T);
|
||||
|
||||
/// The [Router]'s intention when it reports a new [RouteInformation] to the
|
||||
/// [RouteInformationProvider].
|
||||
@ -492,8 +492,7 @@ enum RouteInformationReportingType {
|
||||
}
|
||||
|
||||
class _RouterState<T> extends State<Router<T>> with RestorationMixin {
|
||||
Object? _currentRouteInformationParserTransaction;
|
||||
Object? _currentRouterDelegateTransaction;
|
||||
Object? _currentRouterTransaction;
|
||||
RouteInformationReportingType? _currentIntentionToReport;
|
||||
final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
|
||||
|
||||
@ -593,8 +592,7 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
|
||||
widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
|
||||
widget.routeInformationParser != oldWidget.routeInformationParser ||
|
||||
widget.routerDelegate != oldWidget.routerDelegate) {
|
||||
_currentRouteInformationParserTransaction = Object();
|
||||
_currentRouterDelegateTransaction = Object();
|
||||
_currentRouterTransaction = Object();
|
||||
}
|
||||
if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
|
||||
oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
|
||||
@ -619,20 +617,27 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
|
||||
widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
|
||||
widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
|
||||
widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
|
||||
_currentRouteInformationParserTransaction = null;
|
||||
_currentRouterDelegateTransaction = null;
|
||||
_currentRouterTransaction = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _processRouteInformation(RouteInformation information, ValueGetter<_DelegateRouteSetter<T>> delegateRouteSetter) {
|
||||
_currentRouteInformationParserTransaction = Object();
|
||||
_currentRouterDelegateTransaction = Object();
|
||||
void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
|
||||
_currentRouterTransaction = Object();
|
||||
widget.routeInformationParser!
|
||||
.parseRouteInformation(information)
|
||||
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget))
|
||||
.then<void>(delegateRouteSetter())
|
||||
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
|
||||
.then<void>(_rebuild);
|
||||
.then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
|
||||
}
|
||||
|
||||
_RouteSetter<T> _processParsedRouteInformation(Object? transaction, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
|
||||
return (T data) async {
|
||||
if (_currentRouterTransaction != transaction) {
|
||||
return;
|
||||
}
|
||||
await delegateRouteSetter()(data);
|
||||
if (_currentRouterTransaction == transaction) {
|
||||
_rebuild();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void _handleRouteInformationProviderNotification() {
|
||||
@ -641,56 +646,21 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
|
||||
}
|
||||
|
||||
Future<bool> _handleBackButtonDispatcherNotification() {
|
||||
_currentRouteInformationParserTransaction = Object();
|
||||
_currentRouterDelegateTransaction = Object();
|
||||
_currentRouterTransaction = Object();
|
||||
return widget.routerDelegate
|
||||
.popRoute()
|
||||
.then<bool>(_verifyRouterDelegatePopStillCurrent(_currentRouterDelegateTransaction, widget))
|
||||
.then<bool>((bool data) {
|
||||
_rebuild();
|
||||
return SynchronousFuture<bool>(data);
|
||||
});
|
||||
.then<bool>(_handleRoutePopped(_currentRouterTransaction));
|
||||
}
|
||||
|
||||
static final Future<dynamic> _never = Completer<dynamic>().future; // won't ever complete
|
||||
|
||||
_AsyncPassthrough<T> _verifyRouteInformationParserStillCurrent(Object? transaction, Router<T> originalWidget) {
|
||||
return (T data) {
|
||||
if (transaction == _currentRouteInformationParserTransaction &&
|
||||
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
|
||||
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
|
||||
widget.routeInformationParser == originalWidget.routeInformationParser &&
|
||||
widget.routerDelegate == originalWidget.routerDelegate) {
|
||||
return SynchronousFuture<T>(data);
|
||||
}
|
||||
return _never as Future<T>;
|
||||
};
|
||||
}
|
||||
|
||||
_AsyncPassthrough<void> _verifyRouterDelegatePushStillCurrent(Object? transaction, Router<T> originalWidget) {
|
||||
return (void data) {
|
||||
if (transaction == _currentRouterDelegateTransaction &&
|
||||
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
|
||||
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
|
||||
widget.routeInformationParser == originalWidget.routeInformationParser &&
|
||||
widget.routerDelegate == originalWidget.routerDelegate)
|
||||
return SynchronousFuture<void>(data);
|
||||
return _never;
|
||||
};
|
||||
}
|
||||
|
||||
_AsyncPassthrough<bool> _verifyRouterDelegatePopStillCurrent(Object? transaction, Router<T> originalWidget) {
|
||||
_AsyncPassthrough<bool> _handleRoutePopped(Object? transaction) {
|
||||
return (bool data) {
|
||||
if (transaction == _currentRouterDelegateTransaction &&
|
||||
widget.routeInformationProvider == originalWidget.routeInformationProvider &&
|
||||
widget.backButtonDispatcher == originalWidget.backButtonDispatcher &&
|
||||
widget.routeInformationParser == originalWidget.routeInformationParser &&
|
||||
widget.routerDelegate == originalWidget.routerDelegate) {
|
||||
return SynchronousFuture<bool>(data);
|
||||
if (transaction != _currentRouterTransaction) {
|
||||
// A rebuilt was trigger from a different source. Returns true to
|
||||
// prevent bubbling.
|
||||
return SynchronousFuture<bool>(true);
|
||||
}
|
||||
// A rebuilt was trigger from a different source. Returns true to
|
||||
// prevent bubbling.
|
||||
return SynchronousFuture<bool>(true);
|
||||
_rebuild();
|
||||
return SynchronousFuture<bool>(data);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -77,6 +79,54 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Interrupts route parsing should not crash', (WidgetTester tester) async {
|
||||
final SimpleRouteInformationProvider provider = SimpleRouteInformationProvider();
|
||||
provider.value = const RouteInformation(
|
||||
location: 'initial',
|
||||
);
|
||||
final CompleterRouteInformationParser parser = CompleterRouteInformationParser();
|
||||
final SimpleAsyncRouterDelegate delegate = SimpleAsyncRouterDelegate(
|
||||
builder: (BuildContext context, RouteInformation? information) {
|
||||
if (information == null)
|
||||
return const Text('waiting');
|
||||
return Text(information.location!);
|
||||
},
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(buildBoilerPlate(
|
||||
Router<RouteInformation>(
|
||||
routeInformationProvider: provider,
|
||||
routeInformationParser: parser,
|
||||
routerDelegate: delegate,
|
||||
),
|
||||
));
|
||||
// Future has not yet completed.
|
||||
expect(find.text('waiting'), findsOneWidget);
|
||||
|
||||
final Completer<void> firstTransactionCompleter = parser.completer;
|
||||
|
||||
// Start a new parsing transaction before the previous one complete.
|
||||
provider.value = const RouteInformation(
|
||||
location: 'update',
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('waiting'), findsOneWidget);
|
||||
// Completing the previous transaction does not cause an update.
|
||||
firstTransactionCompleter.complete();
|
||||
await firstTransactionCompleter.future;
|
||||
await tester.pump();
|
||||
expect(find.text('waiting'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
// Make sure the new transaction can complete and update correctly.
|
||||
parser.completer.complete();
|
||||
await parser.completer.future;
|
||||
await delegate.setNewRouteFuture;
|
||||
await tester.pump();
|
||||
expect(find.text('update'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Router.maybeOf can be null', (WidgetTester tester) async {
|
||||
final GlobalKey key = GlobalKey();
|
||||
await tester.pumpWidget(buildBoilerPlate(
|
||||
@ -1414,6 +1464,24 @@ class SimpleAsyncRouteInformationParser extends RouteInformationParser<RouteInfo
|
||||
}
|
||||
}
|
||||
|
||||
class CompleterRouteInformationParser extends RouteInformationParser<RouteInformation> {
|
||||
CompleterRouteInformationParser();
|
||||
|
||||
late Completer<void> completer;
|
||||
|
||||
@override
|
||||
Future<RouteInformation> parseRouteInformation(RouteInformation information) async {
|
||||
completer = Completer<void>();
|
||||
await completer.future;
|
||||
return SynchronousFuture<RouteInformation>(information);
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation restoreRouteInformation(RouteInformation configuration) {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
|
||||
SimpleAsyncRouterDelegate({
|
||||
required this.builder,
|
||||
|
Loading…
x
Reference in New Issue
Block a user