Save scroll position when scrolling. (#9740)
This commit is contained in:
parent
89044a0e9e
commit
c8ad201484
@ -12,6 +12,7 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'notification_listener.dart';
|
import 'notification_listener.dart';
|
||||||
|
import 'page_storage.dart';
|
||||||
import 'scroll_context.dart';
|
import 'scroll_context.dart';
|
||||||
import 'scroll_controller.dart';
|
import 'scroll_controller.dart';
|
||||||
import 'scroll_metrics.dart';
|
import 'scroll_metrics.dart';
|
||||||
@ -187,6 +188,20 @@ class _PagePosition extends ScrollPositionWithSingleContext {
|
|||||||
|
|
||||||
double get page => pixels == null ? null : getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
|
double get page => pixels == null ? null : getPageFromPixels(pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void saveScrollOffset() {
|
||||||
|
PageStorage.of(context.storageContext)?.writeState(context.storageContext, getPageFromPixels(pixels, viewportDimension));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreScrollOffset() {
|
||||||
|
if (pixels == null) {
|
||||||
|
final double value = PageStorage.of(context.storageContext)?.readState(context.storageContext);
|
||||||
|
if (value != null)
|
||||||
|
correctPixels(getPixelsFromPage(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool applyViewportDimension(double viewportDimension) {
|
bool applyViewportDimension(double viewportDimension) {
|
||||||
final double oldViewportDimensions = this.viewportDimension;
|
final double oldViewportDimensions = this.viewportDimension;
|
||||||
|
@ -27,6 +27,14 @@ abstract class ScrollContext {
|
|||||||
/// [ScrollBehavior.buildViewportChrome].
|
/// [ScrollBehavior.buildViewportChrome].
|
||||||
BuildContext get notificationContext;
|
BuildContext get notificationContext;
|
||||||
|
|
||||||
|
/// The [BuildContext] that should be used when searching for a [PageStorage].
|
||||||
|
///
|
||||||
|
/// This context is typically the context of the scrollable widget itself. In
|
||||||
|
/// particular, it should involve any [GlobalKey]s that are dynamically
|
||||||
|
/// created as part of creating the scrolling widget, since those would be
|
||||||
|
/// different each time the widget is created.
|
||||||
|
BuildContext get storageContext;
|
||||||
|
|
||||||
/// A [TickerProvider] to use when animating the scroll position.
|
/// A [TickerProvider] to use when animating the scroll position.
|
||||||
TickerProvider get vsync;
|
TickerProvider get vsync;
|
||||||
|
|
||||||
|
@ -180,7 +180,24 @@ class ScrollController extends ChangeNotifier {
|
|||||||
/// resizes.
|
/// resizes.
|
||||||
///
|
///
|
||||||
/// By default, returns a [ScrollPositionWithSingleContext].
|
/// By default, returns a [ScrollPositionWithSingleContext].
|
||||||
ScrollPosition createScrollPosition(
|
///
|
||||||
|
/// The arguments are generally passed to the [ScrollPosition] being created:
|
||||||
|
///
|
||||||
|
/// * `physics`: An instance of [ScrollPhysics] that determines how the
|
||||||
|
/// [ScrollPosition] should react to user interactions, how it should
|
||||||
|
/// simulate scrolling when released or flung, etc. The value will not be
|
||||||
|
/// null. It typically comes from the [ScrollView] or other widget that
|
||||||
|
/// creates the [Scrollable], or, if none was provided, from the ambient
|
||||||
|
/// [ScrollConfiguration].
|
||||||
|
/// * `context`: A [ScrollContext] used for communicating with the object
|
||||||
|
/// that is to own the [ScrollPosition] (typically, this is the
|
||||||
|
/// [Scrollable] itself).
|
||||||
|
/// * `oldPosition`: If this is not the first time a [ScrollPosition] has
|
||||||
|
/// been created for this [Scrollable], this will be the previous instance.
|
||||||
|
/// This is used when the environment has changed and the [Scrollable]
|
||||||
|
/// needs to recreate the [ScrollPosition] object. It is null the first
|
||||||
|
/// time the [ScrollPosition] is created.
|
||||||
|
ScrollPosition createScrollPosition(
|
||||||
ScrollPhysics physics,
|
ScrollPhysics physics,
|
||||||
ScrollContext context,
|
ScrollContext context,
|
||||||
ScrollPosition oldPosition,
|
ScrollPosition oldPosition,
|
||||||
|
@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'gesture_detector.dart';
|
import 'gesture_detector.dart';
|
||||||
|
import 'page_storage.dart';
|
||||||
import 'scroll_activity.dart';
|
import 'scroll_activity.dart';
|
||||||
import 'scroll_context.dart';
|
import 'scroll_context.dart';
|
||||||
import 'scroll_metrics.dart';
|
import 'scroll_metrics.dart';
|
||||||
@ -71,6 +72,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
|||||||
assert(context.vsync != null);
|
assert(context.vsync != null);
|
||||||
if (oldPosition != null)
|
if (oldPosition != null)
|
||||||
absorb(oldPosition);
|
absorb(oldPosition);
|
||||||
|
restoreScrollOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How the scroll position should respond to user input.
|
/// How the scroll position should respond to user input.
|
||||||
@ -259,15 +261,54 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
|||||||
/// To cause the position to jump or animate to a new value, consider [jumpTo]
|
/// To cause the position to jump or animate to a new value, consider [jumpTo]
|
||||||
/// or [animateTo].
|
/// or [animateTo].
|
||||||
///
|
///
|
||||||
/// This should not be called during layout. Consider [correctPixels] if you
|
/// This should not be called during layout (e.g. when setting the initial
|
||||||
/// find you need to adjust the position during layout.
|
/// scroll offset). Consider [correctPixels] if you find you need to adjust
|
||||||
|
/// the position during layout.
|
||||||
@protected
|
@protected
|
||||||
void forcePixels(double value) {
|
void forcePixels(double value) {
|
||||||
assert(_pixels != null);
|
assert(pixels != null);
|
||||||
_pixels = value;
|
_pixels = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Called whenever scrolling ends, to store the current scroll offset in a
|
||||||
|
/// storage mechanism with a lifetime that matches the app's lifetime.
|
||||||
|
///
|
||||||
|
/// The stored value will be used by [restoreScrollOffset] when the
|
||||||
|
/// [ScrollPosition] is recreated, in the case of the [Scrollable] being
|
||||||
|
/// disposed then recreated in the same session. This might happen, for
|
||||||
|
/// instance, if a [ListView] is on one of the pages inside a [TabBarView],
|
||||||
|
/// and that page is displayed, then hidden, then displayed again.
|
||||||
|
///
|
||||||
|
/// The default implementation writes the [pixels] using the nearest
|
||||||
|
/// [PageStorage] found from the [context]'s [ScrollContext.storageContext]
|
||||||
|
/// property.
|
||||||
|
@protected
|
||||||
|
void saveScrollOffset() {
|
||||||
|
PageStorage.of(context.storageContext)?.writeState(context.storageContext, pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called whenever the [ScrollPosition] is created, to restore the scroll
|
||||||
|
/// offset if possible.
|
||||||
|
///
|
||||||
|
/// The value is stored by [saveScrollOffset] when the scroll position
|
||||||
|
/// changes, so that it can be restored in the case of the [Scrollable] being
|
||||||
|
/// disposed then recreated in the same session. This might happen, for
|
||||||
|
/// instance, if a [ListView] is on one of the pages inside a [TabBarView],
|
||||||
|
/// and that page is displayed, then hidden, then displayed again.
|
||||||
|
///
|
||||||
|
/// The default implementation reads the value from the nearest [PageStorage]
|
||||||
|
/// found from the [context]'s [ScrollContext.storageContext] property, and
|
||||||
|
/// sets it using [correctPixels], if [pixels] is still null.
|
||||||
|
@protected
|
||||||
|
void restoreScrollOffset() {
|
||||||
|
if (pixels == null) {
|
||||||
|
final double value = PageStorage.of(context.storageContext)?.readState(context.storageContext);
|
||||||
|
if (value != null)
|
||||||
|
correctPixels(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the overscroll by applying the boundary conditions.
|
/// Returns the overscroll by applying the boundary conditions.
|
||||||
///
|
///
|
||||||
/// If the given value is in bounds, returns 0.0. Otherwise, returns the
|
/// If the given value is in bounds, returns 0.0. Otherwise, returns the
|
||||||
@ -467,7 +508,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
|||||||
oldIgnorePointer = _activity.shouldIgnorePointer;
|
oldIgnorePointer = _activity.shouldIgnorePointer;
|
||||||
wasScrolling = _activity.isScrolling;
|
wasScrolling = _activity.isScrolling;
|
||||||
if (wasScrolling && !newActivity.isScrolling)
|
if (wasScrolling && !newActivity.isScrolling)
|
||||||
didEndScroll();
|
didEndScroll(); // notifies and then saves the scroll offset
|
||||||
_activity.dispose();
|
_activity.dispose();
|
||||||
} else {
|
} else {
|
||||||
oldIgnorePointer = false;
|
oldIgnorePointer = false;
|
||||||
@ -495,8 +536,11 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Called by [beginActivity] to report when an activity has ended.
|
/// Called by [beginActivity] to report when an activity has ended.
|
||||||
|
///
|
||||||
|
/// This also saves the scroll offset using [saveScrollOffset].
|
||||||
void didEndScroll() {
|
void didEndScroll() {
|
||||||
activity.dispatchScrollEndNotification(cloneMetrics(), context.notificationContext);
|
activity.dispatchScrollEndNotification(cloneMetrics(), context.notificationContext);
|
||||||
|
saveScrollOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called by [setPixels] to report overscroll when an attempt is made to
|
/// Called by [setPixels] to report overscroll when an attempt is made to
|
||||||
|
@ -358,6 +358,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
|||||||
@override
|
@override
|
||||||
BuildContext get notificationContext => _gestureDetectorKey.currentContext;
|
BuildContext get notificationContext => _gestureDetectorKey.currentContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BuildContext get storageContext => context;
|
||||||
|
|
||||||
// TOUCH HANDLERS
|
// TOUCH HANDLERS
|
||||||
|
|
||||||
Drag _drag;
|
Drag _drag;
|
||||||
|
@ -8,6 +8,10 @@ import 'package:meta/meta.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ScrollController _controller = new ScrollController(
|
||||||
|
initialScrollOffset: 110.0,
|
||||||
|
);
|
||||||
|
|
||||||
class ThePositiveNumbers extends StatelessWidget {
|
class ThePositiveNumbers extends StatelessWidget {
|
||||||
const ThePositiveNumbers({ @required this.from });
|
const ThePositiveNumbers({ @required this.from });
|
||||||
final int from;
|
final int from;
|
||||||
@ -15,6 +19,7 @@ class ThePositiveNumbers extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new ListView.builder(
|
return new ListView.builder(
|
||||||
itemExtent: 100.0,
|
itemExtent: 100.0,
|
||||||
|
controller: _controller,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return new Text('${index + from}', key: new ValueKey<int>(index));
|
return new Text('${index + from}', key: new ValueKey<int>(index));
|
||||||
}
|
}
|
||||||
@ -22,98 +27,112 @@ class ThePositiveNumbers extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Null> performTest(WidgetTester tester) async {
|
Future<Null> performTest(WidgetTester tester, bool maintainState) async {
|
||||||
|
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
|
||||||
|
await tester.pumpWidget(new Navigator(
|
||||||
|
key: navigatorKey,
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
if (settings.name == '/') {
|
||||||
|
return new MaterialPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (_) => new Container(child: const ThePositiveNumbers(from: 0)),
|
||||||
|
maintainState: maintainState,
|
||||||
|
);
|
||||||
|
} else if (settings.name == '/second') {
|
||||||
|
return new MaterialPageRoute<Null>(
|
||||||
|
settings: settings,
|
||||||
|
builder: (_) => new Container(child: const ThePositiveNumbers(from: 10000)),
|
||||||
|
maintainState: maintainState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||||
|
// 110.0, so we should have 7 items, 1..7.
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
expect(find.text('2'), findsOneWidget);
|
||||||
|
expect(find.text('3'), findsOneWidget);
|
||||||
|
expect(find.text('4'), findsOneWidget);
|
||||||
|
expect(find.text('5'), findsOneWidget);
|
||||||
|
expect(find.text('6'), findsOneWidget);
|
||||||
|
expect(find.text('7'), findsOneWidget);
|
||||||
|
expect(find.text('8'), findsNothing);
|
||||||
|
expect(find.text('10'), findsNothing);
|
||||||
|
expect(find.text('100'), findsNothing);
|
||||||
|
|
||||||
|
tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(1000.0);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||||
|
// 1000, so we should have exactly 6 items, 10..15.
|
||||||
|
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
expect(find.text('8'), findsNothing);
|
||||||
|
expect(find.text('9'), findsNothing);
|
||||||
|
expect(find.text('10'), findsOneWidget);
|
||||||
|
expect(find.text('11'), findsOneWidget);
|
||||||
|
expect(find.text('12'), findsOneWidget);
|
||||||
|
expect(find.text('13'), findsOneWidget);
|
||||||
|
expect(find.text('14'), findsOneWidget);
|
||||||
|
expect(find.text('15'), findsOneWidget);
|
||||||
|
expect(find.text('16'), findsNothing);
|
||||||
|
expect(find.text('100'), findsNothing);
|
||||||
|
|
||||||
|
navigatorKey.currentState.pushNamed('/second');
|
||||||
|
await tester.pump(); // navigating always takes two frames, one to start...
|
||||||
|
await tester.pump(const Duration(seconds: 1)); // ...and one to end the transition
|
||||||
|
|
||||||
|
// the second list is now visible, starting at 10001
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
expect(find.text('10'), findsNothing);
|
||||||
|
expect(find.text('11'), findsNothing);
|
||||||
|
expect(find.text('10000'), findsNothing);
|
||||||
|
expect(find.text('10001'), findsOneWidget);
|
||||||
|
expect(find.text('10002'), findsOneWidget);
|
||||||
|
expect(find.text('10003'), findsOneWidget);
|
||||||
|
expect(find.text('10004'), findsOneWidget);
|
||||||
|
expect(find.text('10005'), findsOneWidget);
|
||||||
|
expect(find.text('10006'), findsOneWidget);
|
||||||
|
expect(find.text('10007'), findsOneWidget);
|
||||||
|
expect(find.text('10008'), findsNothing);
|
||||||
|
expect(find.text('10010'), findsNothing);
|
||||||
|
expect(find.text('10100'), findsNothing);
|
||||||
|
|
||||||
|
navigatorKey.currentState.pop();
|
||||||
|
await tester.pump(); // again, navigating always takes two frames
|
||||||
|
|
||||||
|
// Ensure we don't clamp the scroll offset even during the navigation.
|
||||||
|
// https://github.com/flutter/flutter/issues/4883
|
||||||
|
final ScrollableState state = tester.state(find.byType(Scrollable).first);
|
||||||
|
expect(state.position.pixels, equals(1000.0));
|
||||||
|
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||||
|
// 1000, so we should have exactly 6 items, 10..15.
|
||||||
|
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
expect(find.text('8'), findsNothing);
|
||||||
|
expect(find.text('9'), findsNothing);
|
||||||
|
expect(find.text('10'), findsOneWidget);
|
||||||
|
expect(find.text('11'), findsOneWidget);
|
||||||
|
expect(find.text('12'), findsOneWidget);
|
||||||
|
expect(find.text('13'), findsOneWidget);
|
||||||
|
expect(find.text('14'), findsOneWidget);
|
||||||
|
expect(find.text('15'), findsOneWidget);
|
||||||
|
expect(find.text('16'), findsNothing);
|
||||||
|
expect(find.text('100'), findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
|
testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
|
||||||
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
|
await performTest(tester, true);
|
||||||
await tester.pumpWidget(new Navigator(
|
await performTest(tester, false);
|
||||||
key: navigatorKey,
|
|
||||||
onGenerateRoute: (RouteSettings settings) {
|
|
||||||
if (settings.name == '/') {
|
|
||||||
return new MaterialPageRoute<Null>(
|
|
||||||
settings: settings,
|
|
||||||
builder: (_) => new Container(child: const ThePositiveNumbers(from: 0)),
|
|
||||||
);
|
|
||||||
} else if (settings.name == '/second') {
|
|
||||||
return new MaterialPageRoute<Null>(
|
|
||||||
settings: settings,
|
|
||||||
builder: (_) => new Container(child: const ThePositiveNumbers(from: 10000)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
|
||||||
// zero, so we should have exactly 6 items, 0..5.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
expect(find.text('2'), findsOneWidget);
|
|
||||||
expect(find.text('3'), findsOneWidget);
|
|
||||||
expect(find.text('4'), findsOneWidget);
|
|
||||||
expect(find.text('5'), findsOneWidget);
|
|
||||||
expect(find.text('6'), findsNothing);
|
|
||||||
expect(find.text('10'), findsNothing);
|
|
||||||
expect(find.text('100'), findsNothing);
|
|
||||||
|
|
||||||
tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(1000.0);
|
|
||||||
await tester.pump(const Duration(seconds: 1));
|
|
||||||
|
|
||||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
|
||||||
// 1000, so we should have exactly 6 items, 10..15.
|
|
||||||
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('8'), findsNothing);
|
|
||||||
expect(find.text('9'), findsNothing);
|
|
||||||
expect(find.text('10'), findsOneWidget);
|
|
||||||
expect(find.text('11'), findsOneWidget);
|
|
||||||
expect(find.text('12'), findsOneWidget);
|
|
||||||
expect(find.text('13'), findsOneWidget);
|
|
||||||
expect(find.text('14'), findsOneWidget);
|
|
||||||
expect(find.text('15'), findsOneWidget);
|
|
||||||
expect(find.text('16'), findsNothing);
|
|
||||||
expect(find.text('100'), findsNothing);
|
|
||||||
|
|
||||||
navigatorKey.currentState.pushNamed('/second');
|
|
||||||
await tester.pump(); // navigating always takes two frames, one to start...
|
|
||||||
await tester.pump(const Duration(seconds: 1)); // ...and one to end the transition
|
|
||||||
|
|
||||||
// the second list is now visible, starting at 10000
|
|
||||||
expect(find.text('10000'), findsOneWidget);
|
|
||||||
expect(find.text('10001'), findsOneWidget);
|
|
||||||
expect(find.text('10002'), findsOneWidget);
|
|
||||||
expect(find.text('10003'), findsOneWidget);
|
|
||||||
expect(find.text('10004'), findsOneWidget);
|
|
||||||
expect(find.text('10005'), findsOneWidget);
|
|
||||||
expect(find.text('10006'), findsNothing);
|
|
||||||
expect(find.text('10010'), findsNothing);
|
|
||||||
expect(find.text('10100'), findsNothing);
|
|
||||||
|
|
||||||
navigatorKey.currentState.pop();
|
|
||||||
await tester.pump(); // again, navigating always takes two frames
|
|
||||||
|
|
||||||
// Ensure we don't clamp the scroll offset even during the navigation.
|
|
||||||
// https://github.com/flutter/flutter/issues/4883
|
|
||||||
final ScrollableState state = tester.state(find.byType(Scrollable).first);
|
|
||||||
expect(state.position.pixels, equals(1000.0));
|
|
||||||
|
|
||||||
await tester.pump(const Duration(seconds: 1));
|
|
||||||
|
|
||||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
|
||||||
// 1000, so we should have exactly 6 items, 10..15.
|
|
||||||
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('8'), findsNothing);
|
|
||||||
expect(find.text('9'), findsNothing);
|
|
||||||
expect(find.text('10'), findsOneWidget);
|
|
||||||
expect(find.text('11'), findsOneWidget);
|
|
||||||
expect(find.text('12'), findsOneWidget);
|
|
||||||
expect(find.text('13'), findsOneWidget);
|
|
||||||
expect(find.text('14'), findsOneWidget);
|
|
||||||
expect(find.text('15'), findsOneWidget);
|
|
||||||
expect(find.text('16'), findsNothing);
|
|
||||||
expect(find.text('100'), findsNothing);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user