Flutter authentification with streams - flutter

apparently I'm doing something fundamentally wrong here... After I login, I send new bool value (T) to isAuthorized stream. StreamBuilder reruns and ends up executing correct if-else branch, but for some reason Login Widget remains rendered on the screen?
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App',
theme: appTheme,
initialRoute: '/',
home: StreamBuilder<bool>(
stream: getIt.get<SessionBloc>().isAuthorized,
initialData: false,
builder: (context, snapshot) {
if (snapshot.data) {
return Home();
} else {
return Login();
}
},
),
routes: {
'/': (context) => Home(),
'/profile': (context) => Profile(),
'/login': (context) => Login(),
'/register': (context) => Register(),
},
),
);
}
}

Because I don't know what's your bloc doing, I created a simple stream that mimics the bloc. It is working fine with this code snippet. If the bloc gives you what you want and goes appropriate if-else block, then you should consider deleting '/': (context) => Home().
This was the exception that I had when I have home route:
If the home property is specified, the routes table cannot include an entry for "/", since it would be redundant.
class Authorization {
final _authorization = StreamController<bool>();
StreamSink<bool> get send => _authorization.sink;
Stream<bool> get isAuthorized => _authorization.stream;
get close => _authorization.close();
}
class MyApp extends StatelessWidget {
Authorization a = new Authorization();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App',
home: StreamBuilder<bool>(
stream: a.isAuthorized,
initialData: false,
builder: (context, snapshot) {
if (snapshot.data) {
return Home(auth: a);
} else {
return Login(auth: a);
}
},
),
routes: {
'/login': (context) => Login(auth: a),
},
);
}
}
class Home extends StatelessWidget {
final Authorization _auth;
Home({#required Authorization auth})
: assert(auth != null),
_auth = auth;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[Text("Home"), RaisedButton(
onPressed: () {
_auth.send.add(false);
},
)],
),
);
}
}
class Login extends StatelessWidget {
final Authorization _auth;
Login({#required Authorization auth})
: assert(auth != null),
_auth = auth;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[Text("Login"), RaisedButton(
onPressed: () {
_auth.send.add(true);
},
)],
),
);
}
}

Related

Could not find the correct Provider<TokenBloc> above this App Widget

I using BLoC. How to create it correctly, what would not arise due to the lack of widgets down the widget tree. Now I usually like this:
Widget build(BuildContext context) {
return MaterialApp(
// debugShowCheckedModeBanner: false,
theme: Styles.appTheme,
home: BlocProvider<TokenBloc>(
create: (context) => di.sl<TokenBloc>(),
child: _childTokenBloc,
),
);
}

Widget get _childTokenBloc {
return BlocBuilder<TokenBloc, TokenState>(builder: (context, state) {
if (state is TokenInitialState) {
context.read<TokenBloc>().add(TokenCheckEvent());
return const LogoImage();
}
if (state is TokenCheckState) {
return const LogoImage();
}
if (state is TokenOkState) {
return MainPageWidget();
}
if (state is TokenNoAuthorizationState) {
return const AuthorizationPageWidget();
}
return const LogoImage();
}
);
}
In AuthorizationPageWidget I do:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ConfirmAuthorizationPage()),
);
And from ConfirmAuthorizationPage I try to turn to TokenBloc:
context.read<TokenBloc>().add(TokenAddEvent());
but I get Error: Could not find the correct Provider above this App Widget
I thought that TokenBloc would be found in the widget tree, but is it not? And how to fix this problem? Need to use MultiBlocProvider in the build method of the ConfirmAuthorizationPage widget? It will be re-initialized, and the previous one will not be used.
Update 1:
Code AuthorizationPageWidget:
class AuthorizationPageWidget extends StatefulWidget {
const AuthorizationPageWidget({Key? key}) : super(key: key);
#override
_AuthorizationPageWidgetState createState() =>
_AuthorizationPageWidgetState();
}
class _AuthorizationPageWidgetState extends State<AuthorizationPageWidget> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider<AuthorizationBloc>(
create: (context) => sl<AuthorizationBloc>(),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_title,
_description,
Expanded(child: Align(alignment: FractionalOffset.bottomCenter, child: _bottomButton))
],
),
),
),
);
}
//......
void pushConfirmPage(String number) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ConfirmAuthorizationPage(number: number,)),
);
}
}
If you want to provide your Bloc in all your application, you have to write it in your MaterialApp like this, not in the body ;
return
BlocProvider<TokenBloc>( // like this
create: (context) => TokenBloc(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: _TokenHome(),
),
),
);
class _TokenHome extends StatelessWidget { // use a class instead of function
const _TokenHome({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocBuilder<TokenBloc, TokenState>(
builder: (context, state) {
if (state is TokenInitialState) {
context.read<TokenBloc>().add(TokenCheckEvent());
return const LogoImage();
}
if (state is TokenCheckState) {
return const LogoImage();
}
if (state is TokenOkState) {
return MainPageWidget();
}
if (state is TokenNoAuthorizationState) {
return const AuthorizationPageWidget();
}
return Container(
width: 50,
height: 50,
color: Colors.red,
); // use this if there is not a state
}
);
}
}
If for some reason it doesn't show anything anymore, then it's because some of your classes like AuthorizationPageWidget or LogoImage are wrong, check that.
-------- EDIT
Using BlocProvider on each page can be useful, but keep in mind that for example AuthorizationBloc will only work for its children, if you call it on another side of the screen it will not work, so it is highly recommended to use a MultiBlocProvider in MaterialApp to avoid future problems;
return MultiBlocProvider( // like this
providers: [
BlocProvider<TokenBloc>(
create: (context) => TokenBloc(),
),
BlocProvider<AuthorizationBloc>(
create: (context) => AuthorizationBloc(),
),
],
child: BlocBuilder<LanguageCubit, Locale?>(
builder: (context, lang) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
);
},
),
);
So all the other BlocProvider that you use to create, delete them, you do not need them, now if you use a BlocBuilder, BlocListeners of any Bloc, you would not have any inconvenience.

How to call a function in cubit class in initState?

So, I have made a function in Cubit Class it is to get data from API. For now I just can get the data if I pressed a button. I wanna make the function automatically called when the page/screen is open. For your information, this page is the first page that will be launched when user open the app. Here is some of my codes.
class UsersCubit extends Cubit<UsersState> {
UsersCubit() : super(UsersInitial());
UserRepository _userRepository = UserRepository();
void getAllUsers() async{
emit(UsersLoading());
try{
ResponseUsers _data = await _userRepository.getUsers();
emit(UsersSuccess(_data));
} catch(e){
emit(UsersError(e.toString()));
}
}
}
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
create: (context) => UsersCubit(),
child: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
),
),
);
}
}
I have tried to call the function directly in initState but when I run the app it returns an error.
#override
void initState() {
context.read<UsersCubit>().getAllUsers();
super.initState();
}
error:
Error: Could not find the correct Provider<UsersCubit> above this UsersPage Widget
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 UsersPage is under your MultiProvider/Provider<UsersCubit>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
...
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
...
consider using `builder` like so:
...
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
Is there any way to solve this case?
Can you try wrapp BlocConsumer inside a Builder ?
import 'package:flutter/material.dart';
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
create: (context) {
final cubit = UsersCubit();
cubit.getAllUsers();
return cubit;
},
child: Builder(builder: (context) {
return BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
);
}),
),
);
}
}
I have solved this case with help from #Ehsan Askari. He suggests me to provide the cubit above the MaterialApp, then I did it. Here is my code now
class AppWidget extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => UsersCubit(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: UsersPage(),
),
);
}
}
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
void initState() {
context.read<UsersCubit>().getAllUsers();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return (state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar();
},
),
);
}
}
you can call it from your Blocprovider by accessing your class like this :-
BlocProvider(
create: (context) => Usercubit()..getAllUsers(),
build : (context) => Scaffold() ........
You don't need to call a function inside initState when using Bloc or cubit, just call it when creating the Cubit inside BlocProvider like this >>.
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
/// after create the cubit you can call the method.
create: (context) => UsersCubit()..getAllUsers();,
child: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
),
),
);
}
}

How can I use the same bloc on two different screens in flutter?

I'm trying to pass the same pad that brings me a firebaseuser instance to two different screens using flutter_bloc, the home screen shows it to me successfully the problem this when I switch to the other screen this stays in waiting and does not go from there.
UserBloc Class
class UserBloc extends Bloc {
final _auth_repository = FirebaseAuthApiRespository();
Stream<FirebaseUser> streamFirebase = FirebaseAuth.instance.onAuthStateChanged;
Stream<FirebaseUser> get auhtStatus => streamFirebase;
Future<FirebaseUser> signIn() => _auth_repository.signInFirebase();
signOut() {
_auth_repository.signOut();
}
Future<FirebaseUser> signInFacebook() => _auth_repository.signInFacebook();
signOutFacebook(){
_auth_repository.signOutFacebook();
}
final _cloudFirestoreRepository = CloudFireStoreRepository();
void updateUserData(User user) => _cloudFirestoreRepository.updateUserDataFirestore(user);
#override
// TODO: implement initialState
get initialState => null;
#override
Stream mapEventToState(event) {
// TODO: implement mapEventToState
return null;
}
}
main.dart
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ManagmentStorage managmentStorage = ManagmentStorage();
#override
void initState() {
managmentStorage.createStragoe();
super.initState();
}
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CameraBloc>(create: (BuildContext context) => CameraBloc()),
BlocProvider<UserBloc>(create: (BuildContext context) => UserBloc()),
],
child: MaterialApp(
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark,
debugShowCheckedModeBanner: false,
home: SignInScreen(), //ProfileUserHome(), //
routes: <String, WidgetBuilder>{
'/adddocument': (BuildContext context) => ScreenAddPhoto(),
'/screenimagedetail': (BuildContext context) => ScreenImageDetail()
},
),
);
}
}
screen 1:
class ProfileUser extends StatelessWidget {
UserBloc userBloc;
#override
Widget build(BuildContext context) {
userBloc = BlocProvider.of<UserBloc>(context);
return StreamBuilder(
stream: userBloc.auhtStatus,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Center(
child: SpinKitSquareCircle(color: Colors.white),
);
break;
case ConnectionState.waiting:
return Center(
child: SpinKitSquareCircle(color: Colors.white),
);
break;
case ConnectionState.active:
return showPofileData(snapshot);
break;
case ConnectionState.done:
return showPofileData(snapshot);
break;
default:
}
},
);
}
Widget showPofileData(AsyncSnapshot snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return Stack(
children: <Widget>[
ListView(
children: <Widget>[Text("Usuario no logeado. Haz login")],
),
],
);
} else {
var user = User(
uid: snapshot.data.uid,
name: snapshot.data.displayName,
email: snapshot.data.email,
photoURL: snapshot.data.photoUrl,
);
return Stack(
children: <Widget>[
UserBody(),
Header(
user: user,
)
],
);
}
}
}
screen2:
class ProfileUserHome extends StatelessWidget {
UserBloc userBloc;
#override
Widget build(BuildContext context) {
userBloc = BlocProvider.of<UserBloc>(context);
return Scaffold(
body: StreamBuilder(
stream: userBloc.auhtStatus,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Center(
child: SpinKitSquareCircle(color: Colors.white),
);
break;
case ConnectionState.waiting:
return Center(
child: SpinKitSquareCircle(color: Colors.white),
);
break;
case ConnectionState.active:
return profileuserinfo(snapshot);
break;
case ConnectionState.done:
return profileuserinfo(snapshot);
break;
default:
}
},
),
);
}
Widget profileuserinfo(AsyncSnapshot snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return Center(child: Text("Usuario no logeado. Haz login"));
} else {
var user = User(
uid: snapshot.data.uid,
name: snapshot.data.displayName,
email: snapshot.data.email,
photoURL: snapshot.data.photoUrl,
);
return ListView(children: <Widget>[
HeaderProfileUserHome(user: user)
]);
}
}
}
BottomNavigationBar:
class NavBarUser extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _NavBarUser();
}
}
class _NavBarUser extends State<NavBarUser> {
int indexTap = 0;
#override
Widget build(BuildContext context) {
PageController controller = PageController(initialPage: 0, keepPage: true);
void onTapTapped(int index) {
setState(() {
indexTap = index;
controller.animateToPage(index,
duration: Duration(milliseconds: 200), curve: Curves.ease);
});
}
void pageChanged(int index) {
setState(() {
indexTap = index;
});
}
final pageView = PageView(
controller: controller,
onPageChanged: (index) {
pageChanged(index);
},
children: <Widget>[UserHome(), ProfileUserHome()],
);
return MultiBlocProvider(
providers: [
BlocProvider<CameraBloc>(create: (BuildContext context) => CameraBloc()),
BlocProvider<UserBloc>(create: ( BuildContext context) => UserBloc()),
],
child: Scaffold(
body: pageView,
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
canvasColor: Colors.black,
primaryColor: Color.fromARGB(230, 226, 80, 116)),
child: BottomNavigationBar(
onTap: (index) {
onTapTapped(index);
},
currentIndex: indexTap,
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text("")),
BottomNavigationBarItem(
icon: Icon(Icons.person), title: Text("")),
],
),
),
),
);
}
}
images of login succesful:
images screen 1
images screen2 profile and here is the problem :c
on the profile screen nothing appears to me and when I return home neither
You are doing it the wrong way, you are using the flutter_bloc library so:
You need to specify events and states.
you need to dispatch events to Bloc
handle events in mapEventToState
yield states
#override
Stream mapEventToState(event) {
// TODO: implement mapEventToState
return null;
}
And finally, listen to states using BlocBuilder widget by the flutter_bloc library instead of StreamBuilder
There is a well-written documentation&tutorial for the library you can check https://bloclibrary.dev/#/gettingstarted
Also you are providing BLOCS twice, in main.dart and NavBarUser widget.
Therefore remove it from NavBarUser
MultiBlocProvider(
providers: [
BlocProvider<CameraBloc>(create: (BuildContext context) => CameraBloc()),
BlocProvider<UserBloc>(create: ( BuildContext context) => UserBloc()),
],

Flutter problem with finding provider context

I have a problem with Flutter Provider pattern. After user is redirected to a new screen, the provider could not be found.
Following my previous question (Could not find the correct provider above this widget)
I wrote this code:
class NewRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Tap to select';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: NewRouteBody()
));
}
}
class NewRouteBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
var user = Provider.of<UserRepository>(context);
return ListView(...)
I did same thing but I get again the error which says that it could not find the correct provider above this widget (NewRouteBody).
Tried to fix it somehow, Googled the answer for a few hours but without success...
Any help is appreciated.
EDIT
This is UserRepository which contains pattern:
class UserRepository with ChangeNotifier {
User user;
Status _status = Status.Uninitialized;
Status get status => _status;
User get getUser => user;
...}
EDIT 2:
Code snippet with ChangeNotifier:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.red,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserRepository>(
builder: (context) => UserRepository.instance(),
child: Consumer<UserRepository>(
builder: (context, UserRepository userRepository, _) {
switch (userRepository.status) {
case Status.Uninitialized:
return Login();
case Status.Unauthenticated:
return Login();
case Status.Authenticating:
case Status.Authenticated:
if(userRepository.getUser.isPrefSet == 0){
return Selection();
}
return Dashboard();
}
},
),
);
}
}
The issue is:
Your ChangeNotifierProvider is located inside Home, but you are trying to access it outside Home.
Providers are scoped. Which means that if it's located inside a widget tree, only its descendants can access it. As such, in your code, only Home can read from the provider.
To fix that, move the provider above MaterialApp:
ChangeNotifierProvider<UserRepository> (
builder: (context) => UserRepository(),
child: MaterialApp(
home: Home(),
),
)
You first need to create the Provider and place in the tree above the usage.
for example, in your case:
Widget build(BuildContext context) {
final title = 'Tap to select';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Provider<UserRepository> (
builder: (context) => UserRepository(),
dispose: (context, val) => val.dispose(),
child: NewRouteBody())
));
}
When the application reports such an error, it can be from many reasons. In my case, I was trying to read data from a context that was not wrapped by the BlocProvider from its ancestor.
// In my Child Widget
Navigator.push(context, MaterialPageRoute(
builder: (_) => MultiBlocProvider(providers: [
BlocProvider.value(
value: SaveJobsCubit()),
BlocProvider.value(
value: context.read<OnlineCompaniesCubit>()),
BlocProvider.value(
value: context.read<ApplyJobsCubit>()),
],
child: AttractiveJobsScreen(),
)
// But in Parent Widget, I create MultiBlocProvider with case have access_token
AuthRepo.accessToken != null
? RepositoryProvider(
create: (context) => OnlineCompaniesRepo(),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SaveJobsCubit(),
),
BlocProvider(
create: (context) => OnlineCompaniesCubit(context.read<OnlineCompaniesRepo>()),
),
BlocProvider(
lazy: false,
create: (context) => ApplyJobsCubit(),
),
],
child: current,
),
)
: RepositoryProvider(
create: (context) => OnlineCompaniesRepo(),
child: BlocProvider(
create: (context) => OnlineCompaniesCubit(context.read<OnlineCompaniesRepo>()),
child: current,
),
);
This causes an error in case there is no access_token, then the child screen will not have SaveJobsCubit and cause the above error.
Hope this helps someone.

Inherited widgets and navigator

I found some answers about this [here, here] but none of them completely answer my question.
I'm going to be using the package provider to describe my question because it greatly reduces the boilerplate code.
What I want to do is to inject a dependency when (and only when) a route is called. I can achieve that by doing something like this on onGenerateRoute:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Routes Demo',
initialRoute: '/',
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => HomePage());
case '/firstPage':
return MaterialPageRoute(
builder: (_) => Provider(
builder: (_) => MyComplexClass(),
child: FirstPage(),
),
);
case '/secondPage':
return MaterialPageRoute(builder: (_) => SecondPage());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('Route does not exists'),
),
),
);
}
});
}
}
class MyComplexClass {
String message = 'UUUH I am so complex';
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
child: Text('Go to first page'),
onPressed: () {
Navigator.pushNamed(context, '/firstPage');
}),
],
),
));
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final myComplexClass = Provider.of<MyComplexClass>(context);
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Center(
child: Text(myComplexClass.message),
),
RaisedButton(
child: Text('Go to second page'),
onPressed: () {
Navigator.pushNamed(context, '/secondPage');
},
)
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final myComplexClass = Provider.of<MyComplexClass>(context);
return Scaffold(
body: Center(
child: Text(myComplexClass.message),
),
);
}
}
This works fine for '/firstPage', but as soon as I push another route from inside 'firstPage' the context is lost and I loose access to MyComplexClass, since the navigator stays at the top of the tree together with MaterialApp the next route will loose the context where MyComplexClass was injected, I cannot manage to find a elegant solution to this.
This is the navigator stack we end up with:
As we can see SecondPage is not a child of Provider, hence the problem.
I don't want to inject all dependencies I have all at once on top of MainApp, I want to inject them as they're needed.
I considered creating new navigators each time I need another "fold", but that seems to become really messy very quickly.
How do I solve this issue?
In the following examples, both the route / and /login can access Provider.of<int>, but the route /other can't.
There are two solutions:
A StatefulWidget combined with a Provider.value that wraps each route.
Example:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
int myState = 42;
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
'/': (_) => Provider.value(value: myState, child: Home()),
'/login': (_) => Provider.value(value: myState, child: Login()),
'/other': (_) => Other(),
},
);
}
}
A private placeholder type that wraps MaterialApp combined with ProxyProvider:
Example:
class _Scope<T> {
_Scope(this.value);
final T value;
}
// ...
Widget build(BuildContext context) {
Provider(
builder: (_) => _Scope(42),
child: MaterialApp(
routes: {
'/': (_) => ProxyProvider<_Scope<int>, int>(
builder: (_, scope, __) => scope.value, child: Home()),
'/login': (_) => ProxyProvider<_Scope<int>, int>(
builder: (_, scope, __) => scope.value, child: Login()),
'/other': (_) => Other(),
},
),
);
}