I am using the following package https://pub.dev/packages/get. Do I need to close my .obs in the onClose of a GetxController? I can't find anything about this in the docs. And looking at my memory it appears that the are being destroyed automatically.
In my understanding of GetX + Flutter so far...
No, you shouldn't have to remove .obs in the close() method of GetxControllers. Disposal of observables from a Controller are done automatically when the Controller is removed from memory.
GetX disposes/removes GetxControllers (and their observables) when the widget in which they are contained are popped off the widget stack / removed from the widget tree (by default, but can be overridden).
You can see this in the override of dispose() methods of various Get widgets.
Here's a snippet of dispose() that's run when GetX widgets are popped/removed:
#override
void dispose() {
if (widget.dispose != null) widget.dispose(this);
if (isCreator || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
GetInstance().delete<T>(tag: widget.tag);
}
}
subs.cancel();
_observer.close();
controller = null;
isCreator = null;
super.dispose();
}
When you use Bindings or Get.to() you're using GetPageRoute's which do cleanup by Route names:
#override
void dispose() {
if (Get.smartManagement != SmartManagement.onlyBuilder) {
WidgetsBinding.instance.addPostFrameCallback((_) => GetInstance()
.removeDependencyByRoute("${settings?.name ?? routeName}"));
}
super.dispose();
}
Test App
Below is a test App you can copy/paste into Android Studio / VSCode and run to watch the debug or run window output for GETX lifecycle events.
GetX will log the creation & disposal of Controllers in and out of memory.
The app has a HomePage and 3 ChildPages using Get Controllers in 3 ways, all which remove itself from memory:
GetX / GetBuilder
Get.put
Bindings
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
// MyCounterBinding().dependencies(); // usually where Bindings happen
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'GetX Dispose Ex',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('GetX/Builder Child'),
onPressed: () => Get.to(ChildPage()),
),
RaisedButton(
child: Text('Get.put Child'),
onPressed: () => Get.to(ChildPutPage()),
),
RaisedButton(
child: Text('Binding Child'),
onPressed: () => Get.to(ChildBindPage()),
),
],
),
),
);
}
}
/// GETX / GETBUILDER
/// Creates Controller within the Get widgets
class ChildPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
GetX<ChildX>(
init: ChildX(),
builder: (cx) => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
),
GetBuilder<ChildX>(
init: ChildX(),
builder: (cx) => RaisedButton(
child: Text('Increment'),
onPressed: cx.inc,
),
),
],
),
),
);
}
}
/// GET.PUT
/// Creates Controller instance upon Build, usable anywhere within the widget build context
class ChildPutPage extends StatelessWidget {
//final ChildX cx = Get.put(ChildX()); // wrong place to put
// see https://github.com/jonataslaw/getx/issues/818#issuecomment-733652172
#override
Widget build(BuildContext context) {
final ChildX cx = Get.put(ChildX());
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
Obx(
() => Text('Counter: ${cx.counter}', style: TextStyle(fontSize: 20),),
),
RaisedButton(
child: Text('Increment'),
onPressed: cx.inc,
)
],
),
),
);
}
}
class MyCounterBinding extends Bindings {
#override
void dependencies() {
Get.lazyPut(() => ChildX(), fenix: true);
}
}
/// GET BINDINGS
/// Normally the MyCounterBinding().dependencies() call is done in main(),
/// making it available throughout the entire app.
/// A lazyPut Controller /w [fenix:true] will be created/removed/recreated as needed or
/// as specified by SmartManagement settings.
/// But to keep the Bindings from polluting the other examples, it's done within this
/// widget's build context (you wouldn't normally do this.)
class ChildBindPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
MyCounterBinding().dependencies(); // just for illustration/example
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose Test Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('This is the Child Page'),
Obx(
() => Text('Counter: ${ChildX.i.counter}', style: TextStyle(fontSize: 20),),
),
RaisedButton(
child: Text('Increment'),
onPressed: ChildX.i.inc,
)
],
),
),
);
}
}
class ChildX extends GetxController {
static ChildX get i => Get.find();
RxInt counter = 0.obs;
void inc() => counter.value++;
}
Notes
Get.to vs. Navigator.push
When using Get.put() in a child widget be sure you're using Get.to() to navigate to that child rather than Flutter's built-in Navigator.push.
GetX wraps the destination widget in a GetPageRoute when using Get.to. This Route class will dispose of Controllers in this route when navigating away / popping the widget off the stack. If you use Navigator.push, GetX isn't involved and you won't get this automatic cleanup.
Navigator.push
onPressed: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => ChildPutPage())),
Get.to
onPressed: () => Get.to(ChildPutPage()),
Based from the code of the super implementation of onClose, by default it does nothing currently.
https://github.com/jonataslaw/getx/blob/7146b6a53c0648104e4f623385deaff055e0036a/lib/get_instance/src/lifecycle.dart#L56
And from the comments, it says:
/// Called before [onDelete] method. [onClose] might be used to
/// dispose resources used by the controller. Like closing events,
/// or streams before the controller is destroyed.
/// Or dispose objects that can potentially create some memory leaks,
/// like TextEditingControllers, AnimationControllers.
/// Might be useful as well to persist some data on disk.
void onClose() {}
from that I think you need to manually close your streams in YourController::onClose() override function.
It appears you can use obs safely when using GetWorkers. Run this code and you'll notice that when you click the buttons a few time there will only be one print per page switch.
void main(){
runApp(GetMaterialApp(home: TestWidget(),));
}
class TestWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('next'),
onPressed: () => Get.to<SomeWidget>(SomeWidget()),
),
);
}
}
class SomeWidget extends StatelessWidget {
RxBool isSubscribed = false.obs;
SomeWidget() {
ever(isSubscribed, (_) => print('test'));
}
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text('back'),
onPressed: () {
isSubscribed.value = !isSubscribed.value;
Get.back();
},
),
);
}
}
Related
I have a very simply scenario. A button where when pushed would transition to the next screen. In Navigator 1.0, it is very simple to do by:
ElevatedButton(
onPressed: () {
// Navigate to next screen.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NextScreen()),
);
},
child: Text('Next Screen!'),
)
It seems that with Navigator 2.0 I would need to have a state to keep track of the current screen.
...
Navigator(
pages: [
MainScreenPageRoute()
if (state.isNextScreen) {
NextScreenPageRoute()
}
],
onPopPage: (route, result) {
// would have to keep track of this value
state.isNextScreen = false;
return route.didPop(result);
},
)
...
As for before I don't have to keep track of a state just to navigate, In Navigator 2.0 it seems that it is required. Is it really the case? If so do you guys have any suggestion of how to handle this properly?
P.S.
It also feels like now I have to keep track of the state which adds to more work compared to before.
yes in navigator 2 you change the page when you change the state.
There are some packages that help you to avoid this like qlevar_router
see this example
import 'package:flutter/material.dart';
import 'package:qlevar_router/qlevar_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
#override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: QRouteInformationParser(),
routerDelegate: QRouterDelegate([
QRoute(path: '/', builder: () => BooksListScreen(books)),
QRoute(
path:
'/books/:id([0-${books.length - 1}])', // The only available pages are the pages in the list
builder: () => BookDetailsScreen(books[QR.params['id']!.asInt!])),
]));
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
BooksListScreen(this.books);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => QR.to('/books/${books.indexOf(book)}'))
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen(this.book);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
),
),
);
}
}
don't forget to add qlevar_router: 1.4.0 in pubspec.yaml
I'm a beginner and wanted to make a quick app where it shows how many times you have hit a button and later translate this knowledge of button interaction to making a calculator. I wrote some code but do not know what I should change to fix my mess-up. Specifically, when I click my button it does not show any change in the text. Here's my code:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(title: "Ame's Application", home: MyHomePage());
}
}
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("App"),
backgroundColor: Colors.amber,
),
body: TextInputWidget());
}
}
class TextInputWidget extends StatefulWidget {
#override
_TextInputWidgetState createState() => _TextInputWidgetState();
}
class _TextInputWidgetState extends State<TextInputWidget> {
int count = 0;
increaseCount() {
setState(() {
count++;
});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text("You have hit the button ${count} times"), SizedBox(height: 100,),
FloatingActionButton(
onPressed: increaseCount(), child: Text("Click"))
]
);
}
}```
You have to pass the reference of the function, not its return type so instead of:
FloatingActionButton(
onPressed: increaseCount(), child: Text("Click")
)
do this :
FloatingActionButton(
onPressed: increaseCount, child: Text("Click")
)
I also recommend you to set the return type of the increaseCount method to void (by default it is dynamic).
The problem comes with your FloatingActionButton. onPressed requires a callback that is called when the button is tapped.
When your method has no parameter as is the case here, you can do:
FloatingActionButton(onPressed: increaseCount, child: Text("Click")),
But, in general, you would do:
FloatingActionButton(onPressed: () => increaseCount(), child: Text("Click")),
That would also allow you to define more complex method such as one that increments your counter by x:
FloatingActionButton(onPressed: () => increaseCount(x), child: Text("Click")),
I'm trying to implement Provider state management on counter application to understand Provider's functionality better. I have added two buttons with respect to two different text widget. So, now whenever I click any of the two widget both the Text widgets get update and give same value. I want both the widgets independent to each other.
I have used ScopedModel already and got the desire result but now I want to try with provider.
Image Link : https://i.stack.imgur.com/ma3tR.png
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("====Home Page Rebuilt====");
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
],
)),
);
}
}
class CustomWidget extends StatelessWidget {
final String number;
const CustomWidget({Key key, this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
print("====Number Page Rebuilt====");
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return Text(
value.count.toString(),
style: Theme.of(context).textTheme.headline3,
);
},
),
FlatButton(
color: Colors.blue,
onPressed: () =>
Provider.of<CounterModel>(context, listen: false).increment(),
child: Text("Click"),
),
],
);
}
}
If you want them independent from each other, then you need to differentiate them somehow. I have a bit of a different style to implement the Provider and it hasn't failed me yet. Here is a complete example.
You should adapt your implementation to something like this:
Define your provider class that extends ChangeNotifier in a CounterProvider.dart file
import 'package:flutter/material.dart';
class CounterProvider extends ChangeNotifier {
/// You can either set an initial value here or use a UserProvider object
/// and call the setter to give it an initial value somewhere in your app, like in main.dart
int _counter = 0; // This will set the initial value of the counter to 0
int get counter => _counter;
set counter(int newValue) {
_counter = newValue;
/// MAKE SURE YOU NOTIFY LISTENERS IN YOUR SETTER
notifyListeners();
}
}
Wrap your app with a Provider Widget like so
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// don't forget to import it here too
import 'package:app/CounterProvider.dart';
void main() {
runApp(
MaterialApp(
initialRoute: '/root',
routes: {
'/root': (context) => MyApp(),
},
title: "Your App Title",
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// Makes data available to everything below it in the Widget tree
/// Basically the entire app.
ChangeNotifierProvider<CounterProvider>.value(value: CounterProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
Access and update data anywhere in the app
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// MAKE SURE TO IMPORT THE CounterProvider.dart file
import 'package:app/CounterProvider.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
CounterProvider counterProvider;
#override
Widget build(BuildContext context) {
/// LISTEN TO THE CHANGES / UPDATES IN THE PROVIDER
counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
_showCounterButton(1),
_showCounterButton(2),
],
),
),
);
}
Widget _showCounterButton(int i) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Text(
i == 1
? counterProvider.counter1.toString()
: counterProvider.counter2.toString(),
style: Theme.of(context).textTheme.headline3,
),
FlatButton(
color: Colors.blue,
onPressed: () {
/// UPDATE DATA IN THE PROVIDER. BECAUSE YOU're USING THE SETTER HERE,
/// THE LISTENERS WILL BE NOTIFIED AND UPDATE ACCORDINGLY
/// you can do this in any other file anywhere in the Widget tree, as long as
/// it it beneath the main.dart file where you defined the MultiProvider
i == 1
? counterProvider.counter1 += 1
: counterProvider.counter2 += 1;
setState(() {});
},
child: Text("Click"),
),
],
);
}
}
If you want, you can change the implementation a bit. If you have multiple counters, for multiple widgets, then just create more variables in the CounterProvider.dart file with separate setters and getters for each counter. Then, to display/update them properly, just use a switch case inside the _showCounterButton() method and inside the onPressed: (){ switch case here, before setState((){}); }.
Hope this helps and gives you a better understanding of how Provider works.
I want my page to hit the API always whenever it enters a page.
For example, I am having 2 screens i.e FirstSCreen and SecondScreen.
In FirstScreen I am calling an API to fetch some data. So if the user navigates from FirstScreen to SecondScreen and then comes back to FirstScreen by pressing the back button It should hit the API in FirstScreen again.
I want to know is there any inbuilt function in flutter where I should call my methods so that it can work every time it enters the screen. I have tried using didUpdateWidget() but it is not working the way I want.
initState() is also called only once the widget is loaded ..
Please explain me
You can use async await for it. Let's say you have a button that change the route.
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
ApiCall();
},
The ApiCall function will call only when user push the back on the second screen.
I have an example that might help. However, it might not cover all scenarios where your FirstScreen will always call a specific function whenever the page is displayed after coming from a different page/screen, or specifically resuming the app (coming from background -- not to be confused when coming from another screen or popping).
My example however, will always call a function when you come back from a specific screen, and you can re-implement it to other navigation functions to ensure that a specific function is always called when coming back to FirstScreen from other screens.
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
home_page.dart
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: RaisedButton(
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => SecondPage()))
.then((value) => _reload(value)),
child: Text('Navigate to Next Page'),
),
)
],
),
);
}
Future<void> _reload(var value) async {
print(
'Home Page resumed after popping/closing SecondPage with value {$value}. Do something.');
}
}
second_page.dart
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, 'Passed Value');
/// It's important that returned value (boolean) is false,
/// otherwise, it will pop the navigator stack twice;
/// since Navigator.pop is already called above ^
return false;
},
child: Scaffold(
appBar: AppBar(),
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(child: Text('Second Page')),
],
),
),
);
}
}
#Rick's answer works like a charm even for Riverpod state management as I coul d not get the firstscreen to rebuild to show changes to the 'used' status of a coupon on the second screen where there is a button to use it.
So I did on the details page:
return WillPopScope(
onWillPop: () async {
//final dumbo = ref.read(userProvider.notifier).initializeUser();
Navigator.pop(context);
return false; //true;
},
Then on the coupons list page, each coupon card has:
child: Card(
elevation: 10,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.0),
side: BorderSide(
color: coupons[index].isUsed
? Colors.grey
: Colors.blue),
),
child: InkWell(
splashColor: Colors.blue.withAlpha(30),
onTap: () {
//go to Details screen with Coupon instance
//https://api.flutter.dev/flutter/widgets/Navigator/pushNamed.html
Navigator.pushNamed(context, '/details',
arguments: coupons[index].couponId)
.then((_) => _reload());
/*Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => DetailsScreen(
coupon: coupons[index],
),
),
);*/
},
where I used _ since there is no parameter.
Then the function in class CouponScreen is:
void _reload() {
setState(() {});
}
I believe setState(){} is what triggers the rebuild. I apologize for the formatting but it's very difficult to copy and paste Flutter code that is nested deeply.
I am very new to Flutter but coming from an Angular background here. Say I want to build a BaseFrame for my app, and depending on the routing, I want to change the INSIDE of that BaseFrame. I don't understand how this would work?
In Angular it would be something like:
'/page-one': PageOne(),
'/page-two': PageTwo(), children: [
'/part-1': Part1(),
'/part-2': Part2(),
];
And in this case, if you navigate to /page-two/part-1 then it would load PageTwo(), and it would load Part1() wherever you specified <app-route></app-route> on PageTwo(). I don't understand how one would do this in Flutter because in Flutter it seems like you can only ever have a single flat route in your main.dart (in your MaterialApp builder)
The best thing I can think of is to have a variable on PageTwo(), and have a type of switch statement:
switch (subPage) {
case '/part-1':
return Part1();
case '/part-2':
return Part2();
}
But this seems like a crappy solution. You also now have the problem of fixing animations etc yourself (because the MaterialApp won't help you here automatically).
Here is something about this but this seems super complicated for a noob. Is this really the right/only way to do this:
Nesting routes with flutter
You can do something like that using this package qlevar_router
A full example of your case
import 'package:flutter/material.dart';
import 'package:qlevar_router/qlevar_router.dart';
void main() {
runApp(MyApp());
}
class AppRoutes {
final routes = <QRouteBase>[
QRoute(
path: '/page-one',
page: (c) => PageOne(),
),
QRoute(
path: '/page-two',
page: (c) => PageTwo(c),
initRoute: '/part-1',
children: [
QRoute(
path: '/part-1',
page: (c) => Part1(),
),
QRoute(
path: '/part-2',
page: (c) => Part2(),
)
])
];
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: QR.router(AppRoutes().routes, initRoute: '/page-one'),
routeInformationParser: QR.routeParser(),
);
}
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
Text('This is page one'),
TextButton(
onPressed: () => QR.to('/page-two/part-1'),
child: Text('To Part 1')),
TextButton(
onPressed: () => QR.to('/page-two/part-2'),
child: Text('To Part 2')),
]),
),
);
}
}
class PageTwo extends StatelessWidget {
final QRouteChild child;
PageTwo(this.child);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(children: [
Text('This is page one'),
TextButton(onPressed: () => QR.back(), child: Text('Back')),
TextButton(
onPressed: () => QR.to('/page-two/part-1'),
child: Text('To Part 1')),
TextButton(
onPressed: () => QR.to('/page-two/part-2'),
child: Text('To Part 2')),
SizedBox(
width: 500,
height: 500,
child: child.childRouter,
)
]),
),
);
}
}
class Part1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text('This is part 1'));
}
}
class Part2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(child: Text('This is part 2'));
}
}
Note that page-tow doesn't change at all when you navigate between the children part-1 and part-2
You could just use something like a TabBar/TabBarView to have other pages show up within the pages you're on. Which then you could give an integer as a parameter of your PageTwo class which you could use that integer to select which page you initially navigate to. This is a fairly straightforward way to do what I think you want but more limited since you won't have different navigators within each of those initial pages. Unless the Parts pages are meant to have their own navigation. If you want to have separate navigators for screens you can do this. That's how the CupertinoTabbedScaffold works (though with CupertinoApps). Which may be something to look into.