How to access TabsRouter from child route in Flutter - AutoRoute - flutter

I am trying to get TabsRouter from its child route but it return null (so I guess it cant find any related router in context). Tried a few things read documentation several times. Its either not working as it should or I don't understand.
Project is only Android and IOS, other platforms not supported.
Used package: auto_route
AutoRoute(
path: NavRoutes.authPage,
page: AuthPage,
children: [
AutoRoute(
path: NavRoutes.loginPage,
page: LoginPage,
),
AutoRoute(
path: NavRoutes.signupPage,
page: SignUpPage,
),
],
)
class AuthView extends StatelessWidget {
const AuthView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final cubit = BlocProvider.of<AuthCubit>(context);
return Scaffold(
body: SafeArea(
child: AutoTabsRouter(
routes: [
const LoginPageRoute(),
SignUpPageRoute(
key: UniqueKey(),
onGooglePressed: cubit.signInGoogle,
),
],
builder: (context, child, animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
),
);
}
}
// Here I create `AutoTabsRouter` inside AuthView, and try to get it inside any of its child like:
context.innerRouterOf<TabsRouter>(AuthPageRoute.name)?.setActiveIndex(1);
context.innerRouterOf<TabsRouter>(LoginPageRoute.name)?.setActiveIndex(1);
AutoTabsRouter.of(context).setActiveIndex(1);
//tried a few more using innerNavKeys but it also failed.

Related

How to change the app bar title depending on the selected GoRouter route?

I want to implement a GoRouter based navigation with a fixed Scaffold and AppBar, but change the title of the AppBar dynamically based on the selected route.
I'm using GoRouter's ShellRoute to have a fixed Scaffold and AppBar and tried changing the title using a riverpod Provider:
final titleProvider = StateProvider((ref) => 'Title');
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
body: child,
appBar: CustomAppBar()
);
},
routes: [
GoRoute(
path: DashboardScreenWeb.routeLocation,
name: DashboardScreenWeb.routeName,
builder: (context, state) {
ref.read(titleProvider.state).state = DashboardScreenWeb.title;
return const DashboardScreenWeb();
},
),
GoRoute(
path: BusinessDataScreen.routeLocation,
name: BusinessDataScreen.routeName,
builder: (context, state) {
ref.read(titleProvider.state).state = BusinessDataScreen.title;
return const BusinessDataScreen();
},
),
....
My CustomAppBar widget uses this provider like this:
class CustomAppBar extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
var title = ref.watch(titleProvider);
return new AppBar(
title: Text(title!)
);
}
}
However, I get a lot of exceptions, most likely because I'm changing the state of the provider at the wrong time. What can I do about it?
======== Exception caught by widgets library =======================================================
The following StateNotifierListenerError was thrown building Builder(dirty):
At least listener of the StateNotifier Instance of 'StateController<String>' threw an exception
when the notifier tried to update its state.
The exceptions thrown are:
setState() or markNeedsBuild() called during build.
This UncontrolledProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
UncontrolledProviderScope
The widget which was currently being built when the offending call was made was:
Use the state property state.location and pass the title to the AppBar
Go Router
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
initialLocation: '/',
navigatorKey: _rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
pageBuilder: (context, state, child) {
String title;
switch (state.location) { // ๐Ÿ‘ˆ Using state.location to set title
case '/':
title = "Initial Screen";
break;
case '/home':
title = "Home Screen";
break;
default:
title = "Default Screen";
}
return NoTransitionPage(
child: ScaffoldAppAndBottomBar(
appTitle: title, // ๐Ÿ‘ˆ pass title here
child: child,
));
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/home',
name: 'Home Title',
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(
child: Text("Home"),
),
),
);
},
),
GoRoute(
path: '/',
name: 'App Title',
parentNavigatorKey: _shellNavigatorKey,
pageBuilder: (context, state) {
return const NoTransitionPage(
child: Scaffold(
body: Center(child: Text("Initial")),
),
);
},
),
],
),
],
);
Custom AppBar
class CustomAppBar extends StatelessWidget {
Widget child;
String? appTitle;
CustomAppBar(
{super.key, required this.child, required this.appTitle});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(appTitle ?? "Default"),
),
body: SafeArea(child: child),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.go('/home');
},
child: const Icon(Icons.home),
),
);
}
}
Output:
you should define your titleProvider like this:
final titleProvider = Provider<String>((ref) => 'Title');
and to update the provider:
GoRoute(
path: BusinessDataScreen.routeLocation,
name: BusinessDataScreen.routeName,
builder: (context, state) {
return ProviderScope(
overrides: [
titleProvider.overrideWithValue('youTitleHere')
]
child: const BusinessDataScreen(),
);
},
),

Flutter go_router: how to use ShellRoute with an expanded child?

Given the following go_router config:
GoRouter(
initialLocation: "/one",
routes: [
ShellRoute(
builder: (_, __, child) => Scaffold(body: Column(children: [const Text("Header"), child],)),
routes: [
GoRoute(
path: '/one',
builder: (_, __) => const Expanded(child: Text("one")),
),
],
),
],
)
the framework won't be able to render the tree due to the following error: Assertion failed: ... hasSize. If I understand correctly that is because ShellRoute wraps its child into a Navigator which will impose max constraints on the nested content.
How can I build a nested navigation as above where I have some fixed elements in a Column as part of the shell, and the child route should fill up the remaining available space vertically?
Do this:
...
ShellRoute(
builder: (_, __, child) => ScaffoldWithNavBar(child: child),
routes: [
...
],
),
...
ScaffoldWithNavBar
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: child),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
Widget _buildBottomNavigationBar() {
...
}
}
See ShellRoute class

dismissed Dissmisible widget is still part of the tree error

I have a problem with Dismissible widget. I use that to close actual page by calling a function:
onDismissed: (_) => Navigator.of(context).pop().
so what I did: wrap whole scaffold by Dismissible.
Everything worked fine until delivered to the page state with bool information about the logged in user (admin or no).
Now i get error like this:
FlutterError (A dismissed Dissmisible widget is still part of the
tree. Make sure to implement the onDismissed handler and to
immediately remove the Dismissible widget from the application once
that has fired.)
Screen: ErrorScreen
I tried "key" changes but it didn't help.
Page code with Dismissible:
class OtherDetailsPage extends StatelessWidget {
const OtherDetailsPage({required this.id, Key? key}) : super(key: key);
final String id;
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
OtherDetailsCubit(OtherRemoteDataSource())..getDetailsWithID(id),
child: BlocBuilder<OtherDetailsCubit, OtherDetailsState>(
builder: (context, details) {
final detail = details.detailsState;
return BlocBuilder<RootCubit, RootState>(
builder: (context, root) {
bool? admin = root.admin;
if (admin == null || detail == null) {
return const LoadingPage();
}
return Dismissible(
key: const Key('key'),
// key: Key(UniqueKey().toString()),
direction: DismissDirection.down,
onDismissed: (_) => Navigator.of(context).pop(),
child: Scaffold(
body: Column(
children: [
Header(detail: detail),
const SizedBox(
height: 20,
),
DetailsDescription(detail: detail),
const SizedBox(
height: 50,
),
admin
? SwitchValueButton(id: id, detail: detail)
: const SizedBox(
height: 1,
),
const AnimatedArrowDown()
],
)),
);
},
);
},
),
);
}
}

Error: Could not find the correct Provider<Networkprovider> above this Widget

I am new to flutter and was trying out the implementation of Network Connectivity with the Flutter Provider. I got across this error and have tried every bit of code on the Internet from changing the context and changing the place where the Provider might lie so that the child widgets will get the context. When I am trying to get the value of res in welcome. dart I am getting the error.
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that Welcome is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider(
create: (context) => Networkprovider().networkController.stream,
initialData: Networkprovider().initRes),
],
child: MaterialApp(
theme: ThemeData(),
initialRoute: Welcome.id,
routes: {
Welcome.id: (context) => Welcome(),
NavigatorPage.id: (context) => const NavigatorPage(),
},
),
);
}
}
NetworkProvider.dart
class Networkprovider extends ChangeNotifier {
late StreamSubscription<ConnectivityResult> _subscription;
late StreamController<ConnectivityResult> _networkController;
late ConnectivityResult initRes = ConnectivityResult.none;
StreamSubscription<ConnectivityResult> get subscription => _subscription;
StreamController<ConnectivityResult> get networkController =>
_networkController;
Networkprovider() {
startup();
}
void startup() {
_networkController = StreamController<ConnectivityResult>();
networkStatusChangeListener();
}
void networkStatusChangeListener() async {
_networkController.sink.add(await Connectivity().checkConnectivity());
_subscription = Connectivity().onConnectivityChanged.listen((event) {
_networkController.sink.add(event);
});
}
void disposeStreams() {
_subscription.cancel();
_networkController.close();
}
}
Welcome.dart
class Welcome extends StatelessWidget {
static String id = "welcome_screen";
const Welcome({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var res = Provider.of<Networkprovider>(context);
return SafeArea(
child: Scaffold(
backgroundColor: MAIN_BACKGROUND_COLOR,
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: const <Widget>[
Image(image: AssetImage("assets/images/Magnet_logo.png"))
],
),
),
const Padding(
padding: EdgeInsets.all(20.0),
child:
Image(image: AssetImage("assets/images/Mannify_logo.png")),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Spinner(),
),
],
),
),
),
);
}
}
Declare ChangeNotifierProvider like this
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Networkprovider()),
],
child: <your widget>,
)
then access like this
final provider = Provider.of<Networkprovider>(context);

Flutter change a "shared" widget as the route changes, like the navBar of facebook.com

I don't know if I used correct terms in the title. I meant share by being displayed in diffrent pages with the same state, so that even if I push a new page, the โ€œsharedโ€ widget will stay the same.
I'm trying to share the same widget across several pages, like the navigation bar of facebook.com.
As I know, Navigator widget allows to build up a seperate route. I've attempted to use the widget here, and it works quite well.
...
Scaffold(
body: Stack(
children: [
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) => MainPage());
},
// observers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
),
Positioned(
bottom: 0,
child: BottomBarWithRecord(),
)
],
));
...
To summarize the situation, there used to be only one root Navigator (I guess it's provided in MaterialApp, but anyway), and I added another Navigator in the route under a Stack (which always display BottomBarWithRecord).
This code works perfect as I expected, that BottomBarWithRecord stays the same even if I open a new page in that new Navigator. I can also open a new page without BottomBarWithRecord by pushing the page in the root Navigator: Navigator.of(context, rootNavigator: true).push(smthsmth)
However, I couldn't find a way to change BottomBarWithRecord() as the route changes, like the appbar of facebook.com.
What I've tried
Subscribe to route using navigator key
As I know, to define a navigator key, I have to write final navigatorKey = GlobalObjectKey<NavigatorState>(context);. This doesn't seem to have addListener thing, so I couldn't find a solution here
Subscribe to route using navigator observer
It was quite complicated. Normally, a super complicated solutions works quite well, but it didn't. By putting with RouteAware after class ClassName, I could use some functions like void didPush() {} didPop() didPushNext to subscribe to the route. However, it was not actually "subscribing" to the route change; it was just checking if user opened this page / opened a new page from this page / ... , which would be complicated to deal with in my situation.
React.js?
When I learned a bit of js with React, I remember that this was done quite easily; I just had to put something like
...
const [appBarIndex, setAppBarIndex] = useState(0);
//0 --> highlight Home icon, 1 --> highlight Chats icon, 2 --> highlight nothing
...
window.addEventListener("locationChange", () => {
//location is the thing like "/post/postID/..."
if (window.location == "/chats") {
setAppBarIndex(1);
} else if (window.location == "/") {
setAppBarIndex(0);
} else {
setAppBarIndex(2);
}
})
Obviously I cannot use React in flutter, so I was finding for a similar easy way to do it on flutter.
How can I make the shared BottomBarWithRecord widget change as the route changes?
Oh man it's already 2AM ahhhh
Thx for reading this till here, and I gotta go sleep rn
If I've mad e any typo, just ignore them
You can define a root widget from which you'll control what screen should be displayed and position the screen and the BottomBar accordingly. So instead of having a Navigator() and BottomBar() inside your Stack, you'll have YourScreen() and BottomBar().
Scaffold(
body: SafeArea(
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: _buildScreen(screenIndex),
),
Align(
alignment: Alignment.bottomCenter,
child: BottomBar(
screenIndex,
onChange: (newIndex) {
setState(() {
screenIndex = newIndex;
});
},
),
),
],
),
),
)
BotttomBar will use the screenIndex passed to it to do what you had in mind and highlight the selected item.
_buildScreen will display the corresponding screen based on screenIndex and you pass the onChange to your BottomBar so that it can update the screen if another item was selected. You won't be using Navigator.of(context).push() in this case unless you want to route to a screen without the BottomBar. Otherwise the onChange passed to BottomBar will be responsible for updating the index and building the new screen.
This is how you could go about it if you wanted to implement it yourself. This package can do what you want as well. Here is a simple example:
class Dashboard extends StatefulWidget {
const Dashboard({Key? key}) : super(key: key);
#override
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
final PersistentTabController _controller = PersistentTabController(initialIndex: 0);
#override
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: _controller,
screens: _buildScreens(),
items: _navBarsItems(),
);
}
List<Widget> _buildScreens() {
return [
const FirstScreen(),
const SecondScreen(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
PersistentBottomNavBarItem(
icon: const Icon(Icons.home),
title: ('First Screen'),
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.edit),
title: ('Second Screen'),
),
];
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('First Screen'),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('Second Screen'),
);
}
}