I am still learning flutter/Dart and creating an app in the process.
Now, I am looking at my code and thinking that I am working too hard by not using some StreamProviders I create in my main.dart file. I did this at the very beginning of my journey so I really didn't know what I was doing. I was just following a tutorial.
So, in my main.dart I created a multiprovider and some StreamProviders inside that.
Widget build(BuildContext context) {
final firestoreService = FirestoreService();
Provider.debugCheckInvalidValueType = null;
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AgentProvider()),
ChangeNotifierProvider(create: (context) => AgencyProvider()),
ChangeNotifierProvider(create: (context) => TrxnProvider()),
ChangeNotifierProvider(create: (context) => EventProvider()),
// The following providers are for retrieving the data for editing purposes
// The stream providers are only called once but they can be accessed
// anywhere in the app.
StreamProvider(create: (context) => firestoreService.getAgents(), initialData: []),
StreamProvider(create: (context) => firestoreService.getAgency(), initialData: []),
StreamProvider(
create: (context) => firestoreService.getAgencyTrxns(context), initialData: []),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginScreen(),
),
);
}
As you can see, I added notes stating that I can access the streams anywhere in my code but I have been creating new streams throughout. Yes, this is embarrassing but I am learning.
Here are the 3 functions used to create the StreamProviders above
Stream<List<Agents>> getAgents() {
return _db.collection('agents').snapshots().map((snapshot) => snapshot.docs
.map((document) => Agents.fromFirestore(document.data()))
.toList());
}
Stream<List<Agency>> getAgency() {
return _db.collection('agency').snapshots().map((snapshot) => snapshot.docs
.map((document) => Agency.fromFirestore(document.data()))
.toList());
}
Stream<QuerySnapshot> getAgencyTrxns(BuildContext context) async* {
yield* FirebaseFirestore.instance
.collection('agency').doc(globals.agencyId)
.collection('trxns')
.where('trxnStatus', isNotEqualTo: 'Closed')
.snapshots();
}
Had I realized what I have done it would have saved me a lot of work...I think.
Now, I am trying to learn how to use these StreamProviders in my code. How do I use them in a StreamBuilder? Currently, this is what I am doing and I don't think I am using the StreamProviders I create in my main.dart.
child: StreamBuilder(
stream: _db.collection('agency').snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
} else {
I think I need to use consume. I am watching a YouTube video but it is not very clear. I would really like to use the StreamProviders I already have rather than create new streams and use memory.
Any help you can provide or if you can point me to a great tutorial that would be even better.
Related
I'm currently working on a Flutter app and I have some issue to write tests.
When I run my test I have the following exception :
"The following ProviderNotFoundException was thrown building Consumer(dirty):
Error: Could not find the correct Provider above this Consumer
Widget"
This is a piece of my main.dart
Widget build(BuildContext context) {
themeData = Theme.of(context);
return Consumer<AppThemeNotifier>(
builder: (BuildContext context, AppThemeNotifier value, Widget? child) {
customAppTheme = AppTheme.getCustomAppTheme(value.themeMode());
return MultiBlocProvider(
providers: [
BlocProvider<AuthentificationBloc>(
create: (context) => AuthentificationBloc(),
),
BlocProvider<RegisterBloc>(
create: (context) => RegisterBloc(),
),
BlocProvider<EventsBloc>(
create: (context) => EventsBloc(),
),
BlocProvider<UsersBloc>(
create: (context) => UsersBloc(),
),
],
And this is the test that I try to run
void main() {
Widget skeleton(widgetToTest)
{
return Consumer<AppThemeNotifier>(
builder: (BuildContext context, AppThemeNotifier value, Widget? child) {
return widgetToTest;
});
}
testWidgets('My login page contains two textfield to enter my credentials',
(WidgetTester tester) async {
await tester.pumpWidget(skeleton(Login2Screen()));
final textFormFieldFinder = find.byElementType(TextFormField);
expect(textFormFieldFinder, findsNWidgets(2));
});
}
I tried to create the "skeleton" function to add the Provider but it doesn't work...
Any help is really appreciate :)
You need to load AppThemeNotifier via ChangeNotifierProvider or any other suitable provider.
await tester.pumpWidget(
ChangeNotifierProvider<AppThemeProvider>(
create: (context) => AppThemeProvider(),
child: skeleton(Login2Screen()),
),
);
I have an app where I use StreamProvider to fetch data from Firestore & pass it around in the app. While it works as I want, the problem arises when I log out & sign in again from a different user ID, it still shows the data from the first user. What do I do to make sure the StreamProvider disposes the current value when a user logs out, and refetches the proper user data when a new user signs in? Here's my code
main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final db = Database();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AuthenticationService>(
create: (_) => AuthenticationService(FirebaseAuth.instance),
),
StreamProvider(
// key: ObjectKey(FirebaseAuth.instance.currentUser.uid),
create: (context) =>
context.read<AuthenticationService>().authStateChanges,
),
ChangeNotifierProvider<AppointmentProvider>(
create: (_) => AppointmentProvider(),
),
StreamProvider(
catchError: (context, error) {
print(error);
},
create: (BuildContext context) => db.getAppointments(),
),
],
child: MaterialApp(
key: GlobalObjectKey(context),
debugShowCheckedModeBanner: false,
title: 'Hospital App',
theme: appTheme,
home: AuthenticationWrapper(),
),
);
}
}
db.getAppointments() method
Stream<List<AppointmentModel>> getAppointments() {
return _firebaseFirestore
.collection("DoctorAppointments")
.doc(uid)
.collection("DoctorAppointmentsList")
.snapshots()
.map((snapShot) => snapShot.docs
.map((document) => AppointmentModel.fromJson(document.data()))
.toList());
}
The AddWorkoutEvent is dispatched correctly from the PageCreateWorkout with a DbMWorkout.
This will be inserted in the correct table of the DB.
The PageCreateWorkout will be notified with the WorkoutAddedState to go to PageWorkoutDetail with the given workoutId. The routes are stacked on the PageSelectWorkout, in which the WorkoutBloc is mainly used, to show all workouts. In there the PageSelectWorkout shall be refreshed with the new WorkoutLoadedState with the newly given workoutList. (The WorkoutList contains the added Workout, which I checked in the logger; but the State won't be yielded. Note that I am extending equatable to WorkoutStates.)
else if (event is AddWorkoutEvent) {
logger.i("AddWorkoutEvent | workout: ${event.workout}");
yield WorkoutLoadingState();
try {
DbMWorkout workout = await RepositoryWorkout.repo.add(event.workout);
yield WorkoutAddedState(id: workout.id);
List<DbMWorkout> workoutList = await RepositoryWorkout.repo.getAll();
logger.i("AddWorkoutEvent | workoutListLength: ${workoutList.length}");
yield WorkoutLoadedState(workoutList: workoutList); // <-- this state
} catch (e) {
yield WorkoutErrorState(message: e.toString());
}
}
The PageSelectWorkout is the initialPage of a Navigator in an indexedStack:
IndexedStack(
sizing: StackFit.expand,
index: _currentIndex,
children: <Widget>[
Navigator(
key: _pageOverview,
initialRoute: PageOverview.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageSelectWorkoutNew,
initialRoute: PageSelectWorkout.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageLog,
initialRoute: PageLog.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route)),
Navigator(
key: _pageSettings,
initialRoute: PageSettings.routeName,
onGenerateRoute: (route) =>
RouteGenerator.generateRoute(route))
]),
The named Route to the SelectWorkout is wrapped with the correct Bloc in a BlocProvider:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) {
return BlocProvider(
create: (context) => WorkoutBloc(),
child: PageSelectWorkout());
});
Note: In other Events like DeleteWorkoutEvent , which is happening without navigating to another Page, the updated State gets yielded correctly.
I found the answer after checking out some GitHub Issues and it's rather simple:
Since I want to access the same Bloc on 2 different Pages I can not just wrap a new Instance of the Bloc to each of the Page.
Instead I should wrap the BlocProvider with the WorkoutBloc higher in the WidgetTree Hierarchy for example in the main.dart
Before:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => BlocProvider(
create: (context) => WorkoutBloc(), // <-- Instance of WorkoutBloc
child: PageSelectWorkout()));
case PageCreateWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => BlocProvider(
create: (context) => WorkoutBloc(), // <-- Instance of WorkoutBloc
child: PageCreateWorkout(
workoutIndex: arguments["workoutIndex"],
workoutPosition: arguments["workoutPosition"],
),
));
After:
case PageSelectWorkout.routeName:
return CupertinoPageRoute(
settings: settings, builder: (context) => PageSelectWorkout());
case PageCreateWorkout.routeName:
return CupertinoPageRoute(
settings: settings,
builder: (context) => PageCreateWorkout(
workoutIndex: arguments["workoutIndex"],
workoutPosition: arguments["workoutPosition"],
),
);
and higher in the Inheritance/WidgetTree Hierarchy e.g. main.dart :
return MaterialApp(
debugShowCheckedModeBanner: false,
title: Strings.appTitle,
theme: AppTheme.darkTheme,
home: MultiBlocProvider(providers: [
BlocProvider<WorkoutBloc>(
create: (context) => WorkoutBloc(), // <-- put the Instance higher
),
BlocProvider<NavigationBloc>(
create: (context) => NavigationBloc(),
),
], child: BottomNavigationController()));
I have a streamProvider that contains all of the documents from a collection.
I want a StreamProvider that contains only certain documents. Can I use .where('agency', isEqualTo: 'some name'? Can this be done and if so how do I do it? Below is my current code:
'''
// From the main.dart
return MultiProvider(
providers: [
StreamProvider(create: (context) => firestoreService.getTrxns()),
],
child: MaterialApp(
initialRoute: WelcomeScreen.id,
routes: {
WelcomeScreen.id: (context) => WelcomeScreen(),
Home_Screen.id: (context) => Home_Screen(),
},
/// This is from a Services.dart file
Stream<List<Trxns>> getTrxns() {
return _db.collection('trxns').snapshots().map((snapshot) => snapshot.docs
.map((document) => Trxns.fromFirestore(document.data()))
.toList());
}
'''
That's exactly how you would do it:
_db.collection('trxns').where('agency', isEqualTo:
'some name').snapshots().map()
How to fix this issue? I tried to add create and How do I pass these parameters?
ProxyProvider<YelloChatDb, UserDao>(
builder: (context, yelloChatDb, userdAO) => UserDao(yelloChatDb),
),
ProxyProvider<YelloVendorClient, VendorService>(
builder: (context, yelloVendorClient, categoryService) =>
VendorService.create(yelloVendorClient.chopperClient),
dispose: (context, categoryService) => categoryService.client.dispose()),
Dao class
#UseDao(tables: [Users])
class UserDao extends DatabaseAccessor<YelloChatDb> with _$UserDaoMixin {
UserDao(YelloChatDb db) : super(db);
...
}
Change log 3.2.0 https://pub.dev/packages/provider#320
Deprecated "builder" of providers in favor to "create"
You can use create instead of builder
example from https://pub.dev/packages/provider#proxyprovider
code snippet
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
ProxyProvider<Counter, Translations>(
create: (_, counter, __) => Translations(counter.value),
),
],
child: Foo(),
);
}