Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
053ebf2c
Unverified
Commit
053ebf2c
authored
Oct 12, 2020
by
Michael Goderbauer
Committed by
GitHub
Oct 12, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Make CupertinoTabScaffold restorable (#67770)
parent
daa6b2cc
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
247 additions
and
56 deletions
+247
-56
tab_scaffold.dart
packages/flutter/lib/src/cupertino/tab_scaffold.dart
+105
-32
restoration_properties.dart
packages/flutter/lib/src/widgets/restoration_properties.dart
+38
-24
tab_scaffold_test.dart
packages/flutter/test/cupertino/tab_scaffold_test.dart
+104
-0
No files found.
packages/flutter/lib/src/cupertino/tab_scaffold.dart
View file @
053ebf2c
...
...
@@ -61,6 +61,8 @@ import 'theme.dart';
///
/// * [CupertinoTabScaffold], a tabbed application root layout that can be
/// controlled by a [CupertinoTabController].
/// * [RestorableCupertinoTabController], which is a restorable version
/// of this controller.
class
CupertinoTabController
extends
ChangeNotifier
{
/// Creates a [CupertinoTabController] to control the tab index of [CupertinoTabScaffold]
/// and [CupertinoTabBar].
...
...
@@ -211,6 +213,7 @@ class CupertinoTabScaffold extends StatefulWidget {
this
.
controller
,
this
.
backgroundColor
,
this
.
resizeToAvoidBottomInset
=
true
,
this
.
restorationId
,
})
:
assert
(
tabBar
!=
null
),
assert
(
tabBuilder
!=
null
),
assert
(
...
...
@@ -289,12 +292,46 @@ class CupertinoTabScaffold extends StatefulWidget {
/// Defaults to true and cannot be null.
final
bool
resizeToAvoidBottomInset
;
/// Restoration ID to save and restore the state of the [CupertinoTabScaffold].
///
/// This property only has an effect when no [controller] has been provided:
/// If it is non-null (and no [controller] has been provided), the scaffold
/// will persist and restore the currently selected tab index. If a
/// [controller] has been provided, it is the responsibility of the owner of
/// that controller to persist and restore it, e.g. by using a
/// [RestorableCupertinoTabController].
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final
String
?
restorationId
;
@override
_CupertinoTabScaffoldState
createState
()
=>
_CupertinoTabScaffoldState
();
}
class
_CupertinoTabScaffoldState
extends
State
<
CupertinoTabScaffold
>
{
CupertinoTabController
?
_controller
;
class
_CupertinoTabScaffoldState
extends
State
<
CupertinoTabScaffold
>
with
RestorationMixin
{
RestorableCupertinoTabController
?
_internalController
;
CupertinoTabController
get
_controller
=>
widget
.
controller
??
_internalController
!.
value
;
@override
String
?
get
restorationId
=>
widget
.
restorationId
;
@override
void
restoreState
(
RestorationBucket
?
oldBucket
,
bool
initialRestore
)
{
_restoreInternalController
();
}
void
_restoreInternalController
()
{
if
(
_internalController
!=
null
)
{
registerForRestoration
(
_internalController
!,
'controller'
);
_internalController
!.
value
.
addListener
(
_onCurrentIndexChange
);
}
}
@override
void
initState
()
{
...
...
@@ -302,30 +339,33 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
_updateTabController
();
}
void
_updateTabController
({
bool
shouldDisposeOldController
=
false
})
{
final
CupertinoTabController
newController
=
// User provided a new controller, update `_controller` with it.
widget
.
controller
??
CupertinoTabController
(
initialIndex:
widget
.
tabBar
.
currentIndex
);
if
(
newController
==
_controller
)
{
return
;
void
_updateTabController
([
CupertinoTabController
?
oldWidgetController
])
{
if
(
widget
.
controller
==
null
&&
_internalController
==
null
)
{
// No widget-provided controller: create an internal controller.
_internalController
=
RestorableCupertinoTabController
(
initialIndex:
widget
.
tabBar
.
currentIndex
);
if
(!
restorePending
)
{
_restoreInternalController
();
// Also adds the listener to the controller.
}
}
if
(
shouldDisposeOldController
)
{
_controller
?.
dispose
();
}
else
if
(
_controller
?.
_isDisposed
==
false
)
{
_controller
!.
removeListener
(
_onCurrentIndexChange
);
if
(
widget
.
controller
!=
null
&&
_internalController
!=
null
)
{
// Use the widget-provided controller.
unregisterFromRestoration
(
_internalController
!);
_internalController
!.
dispose
();
_internalController
=
null
;
}
if
(
oldWidgetController
!=
widget
.
controller
)
{
// The widget-provided controller has changed: move listeners.
if
(
oldWidgetController
?.
_isDisposed
==
false
)
{
oldWidgetController
!.
removeListener
(
_onCurrentIndexChange
);
}
widget
.
controller
?.
addListener
(
_onCurrentIndexChange
);
}
newController
.
addListener
(
_onCurrentIndexChange
);
_controller
=
newController
;
}
void
_onCurrentIndexChange
()
{
assert
(
_controller
!.
index
>=
0
&&
_controller
!
.
index
<
widget
.
tabBar
.
items
.
length
,
"The
$runtimeType
's current index
${_controller
!
.index}
is "
_controller
.
index
>=
0
&&
_controller
.
index
<
widget
.
tabBar
.
items
.
length
,
"The
$runtimeType
's current index
${_controller.index}
is "
'out of bounds for the tab bar with
${widget.tabBar.items.length}
tabs'
);
...
...
@@ -338,11 +378,11 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
void
didUpdateWidget
(
CupertinoTabScaffold
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
widget
.
controller
!=
oldWidget
.
controller
)
{
_updateTabController
(
shouldDisposeOldController:
oldWidget
.
controller
==
null
);
}
else
if
(
_controller
!
.
index
>=
widget
.
tabBar
.
items
.
length
)
{
_updateTabController
(
oldWidget
.
controller
);
}
else
if
(
_controller
.
index
>=
widget
.
tabBar
.
items
.
length
)
{
// If a new [tabBar] with less than (_controller.index + 1) items is provided,
// clamp the current index.
_controller
!
.
index
=
widget
.
tabBar
.
items
.
length
-
1
;
_controller
.
index
=
widget
.
tabBar
.
items
.
length
-
1
;
}
}
...
...
@@ -352,7 +392,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
MediaQueryData
newMediaQuery
=
MediaQuery
.
of
(
context
)!;
Widget
content
=
_TabSwitchingView
(
currentTabIndex:
_controller
!
.
index
,
currentTabIndex:
_controller
.
index
,
tabCount:
widget
.
tabBar
.
items
.
length
,
tabBuilder:
widget
.
tabBuilder
,
);
...
...
@@ -415,9 +455,9 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
// our own listener to update the [_controller.currentIndex] on top of a possibly user
// provided callback.
child:
widget
.
tabBar
.
copyWith
(
currentIndex:
_controller
!
.
index
,
currentIndex:
_controller
.
index
,
onTap:
(
int
newIndex
)
{
_controller
!
.
index
=
newIndex
;
_controller
.
index
=
newIndex
;
// Chain the user's original callback.
widget
.
tabBar
.
onTap
?.
call
(
newIndex
);
},
...
...
@@ -431,13 +471,10 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
@override
void
dispose
()
{
// Only dispose `_controller` when the state instance owns it.
if
(
widget
.
controller
==
null
)
{
_controller
?.
dispose
();
}
else
if
(
_controller
?.
_isDisposed
==
false
)
{
_controller
!.
removeListener
(
_onCurrentIndexChange
);
if
(
widget
.
controller
?.
_isDisposed
==
false
)
{
_controller
.
removeListener
(
_onCurrentIndexChange
);
}
_internalController
?.
dispose
();
super
.
dispose
();
}
}
...
...
@@ -555,3 +592,39 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
);
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [CupertinoTabController].
///
/// The [CupertinoTabController] is accessible via the [value] getter. During
/// state restoration, the property will restore [CupertinoTabController.index]
/// to the value it had when the restoration data it is getting restored from
/// was collected.
class
RestorableCupertinoTabController
extends
RestorableChangeNotifier
<
CupertinoTabController
>
{
/// Creates a [RestorableCupertinoTabController] to control the tab index of
/// [CupertinoTabScaffold] and [CupertinoTabBar].
///
/// The `initialIndex` must not be null and defaults to 0. The value must be
/// greater than or equal to 0, and less than the total number of tabs.
RestorableCupertinoTabController
({
int
initialIndex
=
0
})
:
assert
(
initialIndex
!=
null
),
assert
(
initialIndex
>=
0
),
_initialIndex
=
initialIndex
;
final
int
_initialIndex
;
@override
CupertinoTabController
createDefaultValue
()
{
return
CupertinoTabController
(
initialIndex:
_initialIndex
);
}
@override
CupertinoTabController
fromPrimitives
(
Object
data
)
{
return
CupertinoTabController
(
initialIndex:
data
as
int
);
}
@override
Object
?
toPrimitives
()
{
return
value
.
index
;
}
}
packages/flutter/lib/src/widgets/restoration_properties.dart
View file @
053ebf2c
...
...
@@ -292,6 +292,43 @@ abstract class RestorableListenable<T extends Listenable> extends RestorableProp
}
}
/// A base class for creating a [RestorableProperty] that stores and restores a
/// [ChangeNotifier].
///
/// This class may be used to implement a [RestorableProperty] for a
/// [ChangeNotifier], whose information it needs to store in the restoration
/// data change whenever the [ChangeNotifier] notifies its listeners.
///
/// The [RestorationMixin] this property is registered with will call
/// [toPrimitives] whenever the wrapped [ChangeNotifier] notifies its listeners
/// to update the information that this property has stored in the restoration
/// data.
///
/// Furthermore, the property will dispose the wrapped [ChangeNotifier] when
/// either the property itself is disposed or its value is replaced with another
/// [ChangeNotifier] instance.
abstract
class
RestorableChangeNotifier
<
T
extends
ChangeNotifier
>
extends
RestorableListenable
<
T
>
{
@override
void
initWithValue
(
T
value
)
{
_diposeOldValue
();
super
.
initWithValue
(
value
);
}
@override
void
dispose
()
{
_diposeOldValue
();
super
.
dispose
();
}
void
_diposeOldValue
()
{
if
(
_value
!=
null
)
{
// Scheduling a microtask for dispose to give other entities a chance
// to remove their listeners first.
scheduleMicrotask
(
_value
!.
dispose
);
}
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [TextEditingController].
///
...
...
@@ -299,7 +336,7 @@ abstract class RestorableListenable<T extends Listenable> extends RestorableProp
/// state restoration, the property will restore [TextEditingController.text] to
/// the value it had when the restoration data it is getting restored from was
/// collected.
class
RestorableTextEditingController
extends
Restorable
Listenable
<
TextEditingController
>
{
class
RestorableTextEditingController
extends
Restorable
ChangeNotifier
<
TextEditingController
>
{
/// Creates a [RestorableTextEditingController].
///
/// This constructor treats a null `text` argument as if it were the empty
...
...
@@ -331,27 +368,4 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo
Object
toPrimitives
()
{
return
value
.
text
;
}
TextEditingController
?
_controller
;
@override
void
initWithValue
(
TextEditingController
value
)
{
_disposeControllerIfNecessary
();
_controller
=
value
;
super
.
initWithValue
(
value
);
}
@override
void
dispose
()
{
super
.
dispose
();
_disposeControllerIfNecessary
();
}
void
_disposeControllerIfNecessary
()
{
if
(
_controller
!=
null
)
{
// Scheduling a microtask for dispose to give other entities a chance
// to remove their listeners first.
scheduleMicrotask
(
_controller
!.
dispose
);
}
}
}
packages/flutter/test/cupertino/tab_scaffold_test.dart
View file @
053ebf2c
...
...
@@ -1110,6 +1110,110 @@ void main() {
expect
(
contents
.
length
,
greaterThan
(
0
));
expect
(
contents
.
any
((
RichText
t
)
=>
t
.
textScaleFactor
!=
99
),
isFalse
);
});
testWidgets
(
'state restoration'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
CupertinoApp
(
restorationScopeId:
'app'
,
home:
CupertinoTabScaffold
(
restorationId:
'scaffold'
,
tabBar:
CupertinoTabBar
(
items:
List
<
BottomNavigationBarItem
>.
generate
(
4
,
(
int
i
)
=>
BottomNavigationBarItem
(
icon:
const
Icon
(
CupertinoIcons
.
map
),
label:
'Tab
$i
'
),
),
),
tabBuilder:
(
BuildContext
context
,
int
i
)
=>
Text
(
'Content
$i
'
),
),
),
);
expect
(
find
.
text
(
'Content 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsNothing
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'Tab 2'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
await
tester
.
restartAndRestore
();
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
final
TestRestorationData
data
=
await
tester
.
getRestorationData
();
await
tester
.
tap
(
find
.
text
(
'Tab 1'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 2'
),
findsNothing
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
await
tester
.
restoreFrom
(
data
);
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
});
testWidgets
(
'switch from internal to external controller with state restoration'
,
(
WidgetTester
tester
)
async
{
Widget
buildWidget
({
CupertinoTabController
?
controller
})
{
return
CupertinoApp
(
restorationScopeId:
'app'
,
home:
CupertinoTabScaffold
(
controller:
controller
,
restorationId:
'scaffold'
,
tabBar:
CupertinoTabBar
(
items:
List
<
BottomNavigationBarItem
>.
generate
(
4
,
(
int
i
)
=>
BottomNavigationBarItem
(
icon:
const
Icon
(
CupertinoIcons
.
map
),
label:
'Tab
$i
'
),
),
),
tabBuilder:
(
BuildContext
context
,
int
i
)
=>
Text
(
'Content
$i
'
),
),
);
}
await
tester
.
pumpWidget
(
buildWidget
());
expect
(
find
.
text
(
'Content 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsNothing
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
await
tester
.
tap
(
find
.
text
(
'Tab 2'
));
await
tester
.
pumpAndSettle
();
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
final
CupertinoTabController
controller
=
CupertinoTabController
(
initialIndex:
3
);
await
tester
.
pumpWidget
(
buildWidget
(
controller:
controller
));
expect
(
find
.
text
(
'Content 0'
),
findsNothing
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsNothing
);
expect
(
find
.
text
(
'Content 3'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
buildWidget
());
expect
(
find
.
text
(
'Content 0'
),
findsOneWidget
);
expect
(
find
.
text
(
'Content 1'
),
findsNothing
);
expect
(
find
.
text
(
'Content 2'
),
findsNothing
);
expect
(
find
.
text
(
'Content 3'
),
findsNothing
);
});
}
CupertinoTabBar
_buildTabBar
(
{
int
selectedTab
=
0
})
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment