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

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

Related

How to access TabsRouter from child route in Flutter - AutoRoute

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.

How to use MultiBlocProvider in specific level in flutter tree widget?

I want to use MultiBlocProvider as shown below.
How to use MultiBlocProvider in specific level in flutter tree widget ?
In other words, when we use MultiBlocProvideron top of MaterialApp, there is no problem. But according to the code below, this item gets an error.
example:
void main() {
runApp(MaterialApp(
onGenerateRoute: (settings) {
switch (settings.name) {
case "/":
return MaterialPageRoute(
builder: (_) => MultiBlocProvider(providers: [
BlocProvider(
create: (_) => CounterBloc(),
)
], child: const GroupA()),
settings: settings);
case "/ScopeA":
return MaterialPageRoute(
builder: (_) => const ScopeA(), settings: settings);
default:
return MaterialPageRoute(
builder: (_) => const Text("ERROR"), settings: settings);
}
},
));
}
class GroupA extends StatelessWidget {
const GroupA({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Group A:')),
body: Center(
child: MaterialButton(
onPressed: () => Navigator.pushNamed(context, '/ScopeA'),
child: const Text("Go To Scope A")),
),
);
}
}
class ScopeA extends StatelessWidget {
const ScopeA({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Scope A:')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text(
'$count',
style: Theme.of(context).textTheme.displayLarge,
);
},
),
),
);
}
}
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}
}
i using below version :
flutter_bloc : 8.1.2
bloc : 8.1.1
error:
Error: Cannot hit test a render box that has never been laid out.
You're misleading the concept behind passing blocs through sub-tree and through Navigator widgets.
Basically, the MultiBlocProvideror BlocProvider make a bloc accessible in all the subtrees, so the bloc will be available only in GroupA's subtree of widgets, by calling Navigator.pushNamed(), what does happen is that another separated sub-tree will be put in the Navigator child, so at this point the GroupA and ScopeA will not be in the same widget-tree, even if it seems to when you see a page route is set on top of other's on the Flutter UI, I can represent it like this:
-> MultiBlocProvider -> GroupA
Navigator => |
-> ScopeA
and as you conclude, the bloc that is available inside the GroupA will not be available in ScopeA, until you pass it in somehow, like using BlocProvider.value():
case "/ScopeA":
return MaterialPageRoute(
builder: (context) {
return BlocProvider.value(
value: context.read<CounterBloc>(),
child: const ScopeA(),
);
},
settings: settings,
);
or by making the bloc accessible through the whole app, so you will have a Flutter tree like this:
-> GroupA
MultiBlocProvider -> Navigator => |
-> ScopeA

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(),
);
},
),

How to use bloc initialised in one screen in a different screen

I have two screens in my flutter application Screen1 and Screen2. Screen1 is the home screen. I navigate from Screen1 to Screen2 via
Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder: (context, animation, secondaryAnimation) => Screen2());
and Screen2 to Screen1 via
Navigator.pop(context);
Screen1 is statelesswidget:
class Screen1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BlocA>(create: (_) => BlocA()),
BlocProvider<BlocB>(create: (_) => BlocB()),
]
child: RaisedButton(
child: Text('Goto Screen 2'),
onPressed: Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder: (context, animation, secondaryAnimation) => Screen2());
),
)
}
}
I would appreciate anyone can provide an answer that will satisfy the following :
Want to access the two bloc initialised in the Screen1 from Screen2 using
BlocProvider.value(value: BlocProvider.of(context), child: ...)
without bringing the initialisation of blocs upto the MaterialApp widget. Cannot make the MultiBlocProvider the parent of MaterialApp. I want the blocs only accessed in Screen1 and Screen2. It should not be accessed by other screens.
Also when popped from Screen2 to Screen1, the blocs should not be disposed. Hence, continue to maintain state when popped from Screen2
Should not pass the bloc via constructor or as arguments in Navigator
Currently getting following error:
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Screen2(dirty):
flutter: BlocProvider.of() called with a context that does not contain a BlocA.
flutter: No ancestor could be found starting from the context that was passed to
flutter: BlocProvider.of<BlocA>().
flutter:
flutter: This can happen if the context you used comes from a widget above the BlocProvider.
flutter:
flutter: The context used was: Screen2(dirty)
The use the already created bloc instance on new page, you can use BlocProvider.value.
Like passing BlocX to next route will be like
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<BlocX>(context),
child: Screen2(),
),
),
);
I might go for repository provider on your case. But to pass multiple instance, you can wrap BlocProvider two times on route.
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: BlocProvider.value(
value: BlocProvider.of<BlocB>(context),
child: Screen2(),
),
),
),
);
Currently, I cannot remember any better option, let me know if you've got any.
Now, your second route Screen2 can access both BlocB and BlocB instance.
You can get the instance it like, depend on your code structure.
BlocConsumer<BlocA, BlocAState>(
builder: (context, state) {
if (state is BlocAInitial) {
return Text(state.name);
}
return Text("un impleneted");
},
listener: (context, state) {},
),
When you create bloc, and like to pass it with BlocProvider.value(value: BlocProvider.of<BlocA>(context),, you need to use separate context.
More about blocprovider.
Check the demo, It will clarify, I am using Builder instead of creating new widget for context.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Screen1(),
);
}
}
class Screen1 extends StatelessWidget {
const Screen1({super.key});
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<BlocA>(create: (_) => BlocA()),
BlocProvider<BlocB>(create: (_) => BlocB()),
],
child: Builder(builder: (context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: BlocProvider.value(
value: BlocProvider.of<BlocB>(context),
child: Screen2(),
),
),
),
);
},
),
);
}),
);
}
}
class Screen2 extends StatelessWidget {
const Screen2({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
BlocConsumer<BlocA, BlocAState>(
builder: (context, state) {
if (state is BlocAInitial) {
return Text(state.name);
}
return Text("un impleneted");
},
listener: (context, state) {},
),
BlocConsumer<BlocB, BlocBState>(
builder: (context, state) {
if (state is BlocBInitial) {
return Text(state.name);
}
return Text("un impleneted");
},
listener: (context, state) {},
),
],
),
);
}
}
Find more about flutterbloccoreconcepts
you have to elevate MultiBlocProvider in the widget tree so that it wraps both screens, e.g. make it a parent of MaterialApp
You can pass bloc elements as a parameter to Screen2
final blocAObject = BlocProvider.of<BlocA>(context);
Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder: (context, animation, secondaryAnimation) => Screen2(bloca:blocAObject));
If you're ok with initializing in MaterialApp while only having the blocs accessible from the two screens, try the following:
final blocA = BlocA(); // shared bloc instance
final blocB = BlocB(); // shared bloc instance
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'screen1': (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => blocA,
),
BlocProvider(
create: (context) => blocB,
),
],
child: Screen1(),
),
'screen2': (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => blocA,
),
BlocProvider(
create: (context) => blocB,
),
],
child: Screen2(),
),
},
);
}

How to use multiple Blocs for different UIs?

I have a Landing UI that doesn't have any Blocs, a Register UI with it's Bloc, a Verification UI with it's Bloc, and a Home UI with it's Bloc.
In each one I defined the BlocProvider.of.
In the main I defined at the Home of Material App a Multiple Bloc Provider with each has it's child and the main child of the provider is the landing Page like this :
home: MultiBlocProvider(
providers: [
BlocProvider<UserBloc>(
create: (context) => UserBloc(UsRepoImp()),
child: RegisterUi(),
),
BlocProvider<VerificationBloc>(
create: (context) => VerificationBloc(VerRepoImp()),
child: VerificationUi(),
),
BlocProvider<HomeBloc>(
create: (context) => HomeBloc(HomeRepoImp()),
child: HomeUi(),
),
],
child: LandingUi(),
),
and one more thing the Verification UI is returned from a Register Bloc state like so :
BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Color(0xff7b68ee)),
);
} else if (state is UserRegistered) {
return VerifyAccount();
} else if (state is UserError) {
return Text('Error');
}
return SizedBox(
height: 10.0,
);
},
),
But when I run I have an error that the Bloc shouldn't have an ancestor.
How am I supposed to make these Blocs to communicate with UI changings correctly?
I think you are using MultiBlocProvider in a wrong way. you should not provide child there, instead only provide the argument of the create function there, and then in your widget tree below this MultiBlocProvider you can use BlocBuilder to listen to any of the provided blocs above in the tree, and if you need to listen to multiple blocs in the same widget, you need to nest BlocBuilders.
example:
#override
Widget build(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider<RecorderBloc>(
create: (context) => myFirstBloc(),
),
BlocProvider<PermissionBloc>(
create: (context) => mySecondBloc(),
)
],
child:myChild()
);
Then inside my_child.dart :
#override
Widget build(BuildContext context) {
return BlocBuilder<MyFirstBloc, MyFirstBlocState>(
builder: (context, myFirstBlocState) =>
BlocBuilder<MySecondBloc, MySecondBlocState>(
builder: (context, secondBlocState) {
//return widget based on the states of both blocs...
},
),
);
}