How to get StreamProvider data in children with new routes in Flutter (Dart) - flutter

I am using the StreamProvider method to wrap my widgets with certain data, such as Auth (which is working anywhere in my app) from Firebase Auth. I want to do the same with a Firestore value but it only seems to work one level deep.
I have a database call that finds an employees profile once the auth check is done. When I try get the employee from my Home() widget with Provider.of(context) it works great:
This is my wrapper widget (which is my main file's home: widget)
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print(user.uid);
// Return either home or authenticate widget
if (user == null) {
return Authenticate();
}
else {
return StreamProvider<Employee>.value(
value: DatabaseService().linkedEmployee(user.uid),
child: Home(),
);
}
}
}
The Database Service function from DatabaseService():
// Get Linked Employee
Stream<Employee> linkedEmployee(String uid) {
return employeesCollection.where("linkedUser", isEqualTo: uid).snapshots().map(_linkedEmployeeFromSnapShot);
}
Employee _linkedEmployeeFromSnapShot(QuerySnapshot snapshot) {
final doc = snapshot.documents[0];
return Employee(
eId: doc.data["eId"],
employeeCode: doc.data["employeeCode"],
fName: doc.data["fName"],
lName: doc.data["lName"],
docId: doc.documentID
);
}
I can access Provider.of<User>(context) from any widget anywhere in my tree. So why can't I do the same for Provider.of<Employee>(context) ?
When I try that in any widget other than Home() I get the error:
Error: Could not find the correct Provider above this Vehicles Widget
For example, in my widget Vehicles:
class Vehicles extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
final employee = Provider.of<Employee>(context);
...
The User Provider works fine, I can print it out, but the employee provider does not work.
Is it something to do with context? Thanks, any advice would be appreciated.
How I'm navigating to the Vehicles() widget from Home() with a raised button with this event :
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Vehicles())
);
},

Here is a more explained reply hence I think some encounter this issue and I also think it's a bit tricky to get the head around it, especially when you have rules in your Firestore that requires a user to be authorized to access the database.
But generally, you want to wrap providers (that you want to access around all of the app) around MaterialApp().
So I'll show you a simple example to easier understand it.
//The App() handles makes the providers globally accessible
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FirebaseAuthProviderLayer(
child: AuthorizedProviderLayer(
authorizedChild: MatApp(child: StartSwitch()),
unAuthorizedChild: MatApp(child: SignInScreen()),
),
);
}
}
//The MaterialApp Wrapped so that it not has to be rewritten
class MatApp extends StatelessWidget {
Widget child;
MatApp({this.child});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App',
home: child,
);
}
}
class FirebaseAuthProviderLayer extends StatelessWidget {
final Widget child;
FirebaseAuthProviderLayer({this.child});
#override
Widget build(BuildContext context) {
return StreamProvider<User>.value(
value: FirebaseAuth.instance.authStateChanges(),
child: child,
);
}
}
//And the layer that decides either or not we should attach all the providers that requires the user to be authorized.
class AuthorizedProviderLayer extends StatelessWidget {
Widget authorizedChild;
Widget unAuthorizedChild;
AuthorizedProviderLayer({this.unAuthorizedChild, this.authorizedChild});
User user;
final FirestoreService firestoreService =
FirestoreService(); //The Service made to access Firestore
#override
Widget build(BuildContext context) {
user = Provider.of<User>(context);
if (user is User)
return MultiProvider(
providers: [
StreamProvider<FirestoreUserData>.value(
value: firestoreService.streamUser(),
),
StreamProvider<AppSettings>.value(
value: firestoreService.streamSettings(),
initialData: null,
)
],
child: authorizedChild,
);
return unAuthorizedChild;
}
}

Related

Provider to be initialized asynchronously from `initState()` but get `could not find the correct Provider`

I develop an ad app, with a message button on the detailed view.
When the user tap on it, the chats view (stateful widget) is pushed to the screen.
The initState() is there to call the asyncInitMessages() which asynchronously fetches the chats and related message from the distant database. The asyncInitMessages() belongs to the Chats class which extends ChangeNotifier.
/// A chat conversation
class Chats extends ChangeNotifier {
/// Internal, private state of the chat.
void asyncInitMessages(
{required ClassifiedAd ad,
required String watchingUserId,
required bool isOwner}) async {
// blah blah
}
}
The ClassifiedAdMessagesViewstateful widget class implementation is as follows (snipet):
#override
void initState() {
// == Fetch conversation and messages
asyncInitMessages();
}
void asyncInitMessages() async {
// === Update all messages
try {
Provider.of<Chats>(context, listen: false).asyncInitMessages(
ad: widget.ad,
watchingUserId: widget.watchingUser!.uid,
isOwner: _isOwner);
} catch (e) {
if (mounted) {
setState(() {
_error = "$e";
_ready = true;
});
}
}
}
#override
Widget build(BuildContext context) {
// <<<<<<<<<<< The exception fires at the Consumer line right below
return Consumer<Chats>(builder: (context, chats, child) {
return Scaffold(
// ... blah blah
Finally, when running ll that, I got the exception in the build at the Consumer line:
could not find the correct Provider<chats>
Help greatly appreciated.
[UPDATED]
Here is the main (very far up from the messages screen)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// } else {
// Firebase.app(); // if already initialized, use that one
// }
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(RootRestorationScope(
restorationId: 'root',
child: ChangeNotifierProvider(
create: (context) => StateModel(),
child: const App())));
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return PersistedAppState(
storage: const JsonFileStorage(),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ThemeModel>.value(value: _themeModel),
//ChangeNotifierProvider<AuthModel>.value(value: _auth),
],
child: Consumer<ThemeModel>(
builder: (context, themeModel, child) => MaterialApp(
// blah blah
}
}
}
And the component just on top of the
/// Classified ad detail view
class ClassifiedAdDetailView extends StatefulWidget {
final User? watchingUser;
final ClassifiedAd ad;
const ClassifiedAdDetailView(
{Key? key, required this.watchingUser, required this.ad})
: super(key: key);
#override
State<ClassifiedAdDetailView> createState() => _ClassifiedAdDetailViewState();
}
class _ClassifiedAdDetailViewState extends State<ClassifiedAdDetailView>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Chats(),
builder: ((context, child) => Scaffold(
// blah blah
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ClassifiedAdMessagesView(
ad: ad,
watchingUser: widget.watchingUser)));
}),
Providers must be located in the widget tree above the widget where you want to use them with Consumer or Provider.of. When you push a new route with Navigator, it won't be add the pushed route below the widget from where you push, it will add it at the same level where home of MaterialApp is located.
(I think the error message you get also states that you can't access the providers between routes.)
In general the tree will look like this if you push some routes (check it with the Flutter Widget Inspector):
MaterialApp
home
widget1
widget2
widget21
widget22
page1
widget1
widget2
page2
page3
In your code you create the provider in ClassifiedAdDetailView and then push
ClassifiedAdMessagesView from this in the onPressed method. You won't be access this provider from ClassifiedAdMessagesView because the tree will be like (simplified):
MaterialApp
home
ClassifiedAdDetailView
ClassifiedAdMessagesView
The solution is to "lift the state up" and place the provider above every widget from where you need to access it. It can be a part of your existing Multiprovider above MaterialApp but if it is too far, you need to find a proper place that is above both ClassifiedAdDetailView and ClassifiedAdMessagesView.

Flutter - Using GetIt with BuildContext

I'm using Localizations in my app based on the flutter documentation.
See here: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
I use get_it package (version 4.0.4) to retrieve singleton objects like the Localization delegate. Unfortunately it needs a BuildContext property. Sometimes in my app I don't have the context reference so it would be nice if it would work like this: GetIt.I<AppLocalizations>() instead of this: AppLocalizations.of(context). It still can be achieved without a problem if you setup get_it like this: GetIt.I.registerLazySingleton(() => AppLocalizations.of(context)); The problem is that you need the context at least once to make it work. Moreover if you would like to display a localized text instantly in your initial route it's more difficult to get a properly initialized BuildContext at a time when you need it.
It's a little hard for me to explain it properly so I recreated the issue in a minimal example.
I commented out some code that would cause compile time errors, but it shows how I imagined it to be done.
main.dart
GetIt getIt = GetIt.instance;
void setupGetIt() {
// How to get BuildContext properly if no context is available yet?
// Compile time error.
// getIt.registerLazySingleton(() => AppLocalizations.of(context));
}
void main() {
setupGetIt();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// The above line also won't work. It has BuildContext but Applocalizations.of(context) won't work
// because it's above in the Widget tree and not yet setted up.
getIt.registerLazySingleton(() => AppLocalizations.of(context));
return MaterialApp(
supportedLocales: const [
Locale('en', 'US'),
Locale('hu', 'HU'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localeResolutionCallback: (locale, supportedLocales) {
// check if locale is supported
for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale?.languageCode &&
supportedLocale.countryCode == locale?.countryCode) {
return supportedLocale;
}
}
// if locale is not supported then return the first (default) one
return supportedLocales.first;
},
// You may pass the BuildContext here for Page1 in it's constructor
// but in a more advanced routing case it's not a maintanable solution.
home: Page1(),
);
}
}
Initial route
class PageBase extends StatelessWidget {
final String title;
final Widget content;
PageBase(this.title, this.content);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: content,
);
}
}
class Page1 extends PageBase {
// It won't run because I need the context but clearly I don't have it.
// And in a real app you also don't want to pass the context all over the place
if you have many routes to manage.
Page1(String title)
: super(AppLocalizations.of(context).title, Center(child: Text('Hello')));
// Intended solution
// I don't know how to properly initialize getIt AppLocalizations singleton by the time
// it tries to retrieve it
Page1.withGetIt(String title)
: super(getIt<AppLocalizations>().title, Center(child: Text('Hello')));
}
locales.dart
String globalLocaleName;
class AppLocalizations {
//AppLocalizations(this.localeName);
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static Future<AppLocalizations> load(Locale locale) async {
final String name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
globalLocaleName = localeName;
return AppLocalizations();
});
}
String get title => Intl.message(
'This is the title.',
name: 'title',
);
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
// This delegate instance will never change (it doesn't even have fields!)
// It can provide a constant constructor.
const _AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) {
return ['en', 'hu'].contains(locale.languageCode);
}
#override
Future<AppLocalizations> load(Locale locale) => AppLocalizations.load(locale);
#override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
And some intl generated dart code and .arb files that is not so important to illustrate the problem.
So all in all, how can I achive to use my AppLocalizations class as a singleton without using a context for example in a situation like this? Maybe my initial approach is bad and it can be done in other ways that I represented. Please let me know if you have a solution.
Thank you.
To achieve what you have described you need to first make the navigation service using get_it. Follow these steps to achieve the result :
1. Create a navigation service
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!
.push(routeName);
}
goBack() {
return navigatorKey.currentState!.pop();
}
}
This allows you to navigate anywhere from any point throughout the app without build context. This navigator key is what you can use to achieve the AppLocalization instance for the current context.
Refer to the FilledStacks tutorials for this method of navigating without build context.
https://www.filledstacks.com/post/navigate-without-build-context-in-flutter-using-a-navigation-service/
2. Register
GetIt locator = GetIt.instance;
void setupLocator() {
...
locator.registerLazySingleton(() => NavigationService());
...
}
3. Assign the navigator key in the material app
return MaterialApp(
...
navigatorKey: navigationService.navigatorKey,
...
),
3. Create an instance for the AppLocalizations and import it wherever you want to use
localeInstance() => AppLocalizations.of(locator<NavigationService>().navigatorKey.currentContext!)!;
3. The actual use case
import 'package:{your_app_name}/{location_to_this_instace}/{file_name}.dart';
localeInstance().your_localization_variable
You can add a builder to your MaterialApp and setup the service locator inside it with the context available. Example:
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, widget) {
setUpServiceLocator(context);
return FutureBuilder(
future: getIt.allReady(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return widget;
} else {
return Container(color: Colors.white);
}
});
},
);
}
Service Locator Setup:
void setUpServiceLocator(BuildContext context) {
getIt.registerSingleton<AppLocalizations>(AppLocalizations.of(context));
}
You could use some non-localizable splash screen with FutureBuilder and getIt.allReady().
Something like:
class SplashScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// Navigate to main page (with replace)
} else if (snapshot.hasError) {
// Error handling
} else {
// Some pretty loading indicator
}
},
);
}
I'd like to recommend the injectable package for dealing with get_it also.

How to create a StreamProvider and subscribe to it later, Flutter

I have an issue where I want to create a MultiProvider with some different providers, the problem is that two of those are StreamProviders that require first to have a firebase auth login, and after that subscribe to a Stream in firebase based on the result of the user logged in.
So if I cannot launch the StreamProvider before the login at the top of my MaterialApp.
If I declare those providers after the login is complete I get an error that the provider is not on the correct route because I need the data in several routes around all my app.
Here is my code:
class Neybor extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textTheme = GoogleFonts.nunito;
return MultiProvider(
providers: [
ChangeNotifierProvider<Data>(create: (context) => new Data()),
/// Settings Stream
StreamProvider<SettingsDataModel>.value(
value: Globals.firebaseCaller.settings(),
),
/// Plans Stream
StreamProvider<PlansDataModel>.value(
value: Globals.firebaseCaller.plans(),
),
],
child: MaterialApp(
...
}
For Globals.firebaseCaller.settings() and Globals.firebaseCaller.plans() I use the register user uid
Is there a way to declare a StreamProvider and subscribe to it later on my code?
Thanks in advance
Use create parameter in the StreamProvider to pass your stream and subscribe to it using Provider.of<T>(context)
class Neybor extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textTheme = GoogleFonts.nunito;
return MultiProvider(
providers: [
/// Settings Stream
/// Globals.firebaseCaller.settings() should returns a Stream<SettingsDataModel>
StreamProvider<SettingsDataModel>(create: (context) =>
Globals.firebaseCaller.settings(),
),
],
child: HomeView()
..
then in the HomeView()
import 'package:provider/provider.dart';
class HomeView extends StatelessWidget {
#override
Widget build(BuildContext context) {
SettingsDataModel settings = Provider.of<SettingsDataModel>(context);
if (settings == null) {
return Align(child: new CircularProgressIndicator());
} else {
// your code
...
}
...

Best way to pass data from a root to deeper child widget in Flutter

I am new to flutter. My flutter widget tree is getting deeper and deeper, I want to know which is the best method to pass data from a root to a widget which is much deeper from it. I'm currently passing it as a constructor from widget to widget.
My current implementation is given below
Level1(data: data)
Level2(data: data)
Level3(data: data)
Level4(data: data)
suppose my data is retrieved from DB in level1 widget and it is used in level4 widget. As we see, my current implementation is considerably messy. How this is generally done? what is the best practice?
You might like to use Provider. You can find more about it here.
Basically, you create provider of the data at the top-most level like:
class Level1 {
#override
Widget build(BuildContext context) {
Provider<Data>(
create: (_) => Something(),
child: Level2 (
// stuff of level 2
),
),
}
}
Something in this case bight be a change notifier.
You can then access it at a lower level with:
final provider = Provider.of<Something>(context);
Inherited widget - If you want to avoid using any third party library..
More can be found here - https://medium.com/#mehmetf_71205/inheriting-widgets-b7ac56dbbeb1
and here https://www.youtube.com/watch?v=Zbm3hjPjQMk
class MyInheritedWidget extends InheritedWidget {
final int accountId;
final int scopeId;
MyInheritedWidget(accountId, scopeId, child): super(child);
#override
bool updateShouldNotify(MyInheritedWidget old) =>
accountId != old.accountId || scopeId != old.scopeId;
}
class MyPage extends StatelessWidget {
final int accountId;
final int scopeId;
MyPage(this.accountId, this.scopeId);
Widget build(BuildContext context) {
return new MyInheritedWidget(
accountId,
scopeId,
const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget();
Widget build(BuildContext context) {
// somewhere down the line
const MyOtherWidget();
...
}
}
class MyOtherWidget extends StatelessWidget {
const MyOtherWidget();
Widget build(BuildContext context) {
final myInheritedWidget = MyInheritedWidget.of(context);
print(myInheritedWidget.scopeId);
print(myInheritedWidget.accountId);
...

Handling variables for stateful widget

I have ListView widget whose contents are loaded dynamically.
So I decided to make myStatelessWidget.
My basic ideas are
Keep variable articles to be shown on ListView in the StatefulWidget or State.
Pass the contents from outside.
So for now, I write like this, but it has error.
Is my basic idea is correct? or where should I fix?
//// to pass the argument from outside.
new BodyLayout(articles: myarticles),
////
class BodyLayout extends StatefulWidget {
// List<Article> articles // ???I should put here/?
BodyLayout({articles});
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
// List<Article> articles // ???I should put here/?
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.articles.length, // the getter 'articles' is not defined error....
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.articles[index].title),
onTap: () => onTapped(context,widget.articles[index].url),
);
},
);
}
}
You only need to use a stateful widget if you are going to call the setState() method to rebuild the widget with some new state. One case in which you might do that, if you need to retrieve the list of articles from some api or database call, is to have the widget return a loading indicator if the articles list is null, make the async call to retrieve the articles in the state class's initState() method, and when it is returned, rebuild the widget by calling setState() with the retrieved list of articles. Like this, maybe:
/// to pass the argument from outside.
new BodyLayout(),
///
class BodyLayout extends StatefulWidget {
BodyLayout();
#override
_BodyLayoutState createState() => _BodyLayoutState();
}
class _BodyLayoutState extends State<BodyLayout>{
List<Article> articles;
bool loading = true;
#override
void initState(){
_getArticles();
}
void getArticles() async {
articles = await Repository.instance.getArticles(); //some async method to retrieve the articles
setState((){
loading = false;
}); // after the articles are retrieved you can call setState to rebuild the widget
}
#override
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
onTap: () => onTapped(context, articles[index].url),
);
},
);
}
}
If you have the list of articles to begin with and don't need to rebuild the list, you can just make that a stateless widget and pass in the list of articles.
The error you indicated that you got, seems to be because articles is not actually defined as a variable for that class. Dart supports multiple syntax options for passing instance variables like this but this is how I would define that variable and ensure that it is being passed in when the widget is created (could be stateless or stateful widget):
//// to pass the argument from outside.
new BodyLayout(articles: myarticles),
////
class BodyLayout extends StatelessWidget {
final List<Article> articles
BodyLayout({this.articles}) : assert(articles != null);
#override
Widget build(BuildContext context){ ... };
}
If you want to convert your widget to a StatelessWidget, then you can just delete the createState and move the stuff in the build method of the state class into the widget class. This works just fine if your widget doesn't maintain an internal state, but if it has interactive elements (like buttons or such) you will want to delegate them to the parent widget caller via a callback.
To define properties for your custom widget, define the fields as final and instantiate them in the class constructor with this.fieldName. For example:
class BodyLayout extends StatefulWidget {
BodyLayout({
this.articles,
this.onArticleTapped,
});
final List<Article> articles; // Defining the articles property
final void Function(String) onArticleTapped; // Defining the on-tapped callback
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(widget.articles[index].title),
onTap: () => onArticleTapped(widget.articles[index].url),
);
},
);
}
}
You can then use it like such:
...
BodyLayout(
articles: [some list of articles],
onArticleTapped: (url) => <do something with url>
),