Unverified Commit 26398e68 authored by chunhtai's avatar chunhtai Committed by GitHub

Fix crashes when current route parsing transactions are discarded (#100657)

* Fix crashes when current route parsing transactions are discarded

* refactor

* update
parent d60272a2
...@@ -466,7 +466,7 @@ class Router<T> extends StatefulWidget { ...@@ -466,7 +466,7 @@ class Router<T> extends StatefulWidget {
} }
typedef _AsyncPassthrough<Q> = Future<Q> Function(Q); 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 /// The [Router]'s intention when it reports a new [RouteInformation] to the
/// [RouteInformationProvider]. /// [RouteInformationProvider].
...@@ -492,8 +492,7 @@ enum RouteInformationReportingType { ...@@ -492,8 +492,7 @@ enum RouteInformationReportingType {
} }
class _RouterState<T> extends State<Router<T>> with RestorationMixin { class _RouterState<T> extends State<Router<T>> with RestorationMixin {
Object? _currentRouteInformationParserTransaction; Object? _currentRouterTransaction;
Object? _currentRouterDelegateTransaction;
RouteInformationReportingType? _currentIntentionToReport; RouteInformationReportingType? _currentIntentionToReport;
final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation(); final _RestorableRouteInformation _routeInformation = _RestorableRouteInformation();
...@@ -593,8 +592,7 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -593,8 +592,7 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
widget.backButtonDispatcher != oldWidget.backButtonDispatcher || widget.backButtonDispatcher != oldWidget.backButtonDispatcher ||
widget.routeInformationParser != oldWidget.routeInformationParser || widget.routeInformationParser != oldWidget.routeInformationParser ||
widget.routerDelegate != oldWidget.routerDelegate) { widget.routerDelegate != oldWidget.routerDelegate) {
_currentRouteInformationParserTransaction = Object(); _currentRouterTransaction = Object();
_currentRouterDelegateTransaction = Object();
} }
if (widget.routeInformationProvider != oldWidget.routeInformationProvider) { if (widget.routeInformationProvider != oldWidget.routeInformationProvider) {
oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification); oldWidget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
...@@ -619,20 +617,27 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -619,20 +617,27 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification); widget.routeInformationProvider?.removeListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification); widget.backButtonDispatcher?.removeCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.removeListener(_handleRouterDelegateNotification); widget.routerDelegate.removeListener(_handleRouterDelegateNotification);
_currentRouteInformationParserTransaction = null; _currentRouterTransaction = null;
_currentRouterDelegateTransaction = null;
super.dispose(); super.dispose();
} }
void _processRouteInformation(RouteInformation information, ValueGetter<_DelegateRouteSetter<T>> delegateRouteSetter) { void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
_currentRouteInformationParserTransaction = Object(); _currentRouterTransaction = Object();
_currentRouterDelegateTransaction = Object();
widget.routeInformationParser! widget.routeInformationParser!
.parseRouteInformation(information) .parseRouteInformation(information)
.then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget)) .then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
.then<void>(delegateRouteSetter()) }
.then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget))
.then<void>(_rebuild); _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() { void _handleRouteInformationProviderNotification() {
...@@ -641,56 +646,21 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin { ...@@ -641,56 +646,21 @@ class _RouterState<T> extends State<Router<T>> with RestorationMixin {
} }
Future<bool> _handleBackButtonDispatcherNotification() { Future<bool> _handleBackButtonDispatcherNotification() {
_currentRouteInformationParserTransaction = Object(); _currentRouterTransaction = Object();
_currentRouterDelegateTransaction = Object();
return widget.routerDelegate return widget.routerDelegate
.popRoute() .popRoute()
.then<bool>(_verifyRouterDelegatePopStillCurrent(_currentRouterDelegateTransaction, widget)) .then<bool>(_handleRoutePopped(_currentRouterTransaction));
.then<bool>((bool data) {
_rebuild();
return SynchronousFuture<bool>(data);
});
}
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) { return (bool data) {
if (transaction == _currentRouterDelegateTransaction && if (transaction != _currentRouterTransaction) {
widget.routeInformationProvider == originalWidget.routeInformationProvider && // A rebuilt was trigger from a different source. Returns true to
widget.backButtonDispatcher == originalWidget.backButtonDispatcher && // prevent bubbling.
widget.routeInformationParser == originalWidget.routeInformationParser && return SynchronousFuture<bool>(true);
widget.routerDelegate == originalWidget.routerDelegate) {
return SynchronousFuture<bool>(data);
} }
// A rebuilt was trigger from a different source. Returns true to _rebuild();
// prevent bubbling. return SynchronousFuture<bool>(data);
return SynchronousFuture<bool>(true);
}; };
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -77,6 +79,54 @@ void main() { ...@@ -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 { testWidgets('Router.maybeOf can be null', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
await tester.pumpWidget(buildBoilerPlate( await tester.pumpWidget(buildBoilerPlate(
...@@ -1414,6 +1464,24 @@ class SimpleAsyncRouteInformationParser extends RouteInformationParser<RouteInfo ...@@ -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 { class SimpleAsyncRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
SimpleAsyncRouterDelegate({ SimpleAsyncRouterDelegate({
required this.builder, required this.builder,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment