Flutter riverpod automatic state change based on event - flutter

I am new to riverpod and while the articles have been helpful to get started I am struggling with stale state.
When a user logs in I am setting some state. When the details of the state change in DB I want the rebuild to happen automatically. While I am able to get a stream from the DB I am unable to connect the DB change to the riverpod state.
The pattern is for collaboration. Where two users are working on the same part of the application on independent phones, tablets etc.
I am using document stream and collection streams from firecloudstore.
Any helpful articles or how you have solved for this with riverpod? Do I have to invest time in learning something like BLoC for this?

You definitely don't need to learn BLoC to accomplish what you're after.
You say you're using firebase streams, so here's an example of live rebuilding when your data changes.
First, your repository layer.
class YourRepository {
YourRepository(this._read);
static final provider = Provider((ref) => YourRepository(ref.read));
final Reader _read;
Stream<YourModel?> streamById(String id) {
final stream = FirebaseFirestore.instance.collection('YourCollection').doc(id).snapshots();
return stream.map((event) => event.exists ? YourModel.fromJson(event.data()!) : null);
}
}
Next, define a StreamProvider that reads the stream defined in your repository.
final streamYourModelById = StreamProvider.autoDispose.family<YourModel?, String>((ref, id) {
return ref.watch(YourRepository.provider).streamById(id);
});
Last, use the StreamProvider in a widget to reload when your data changes.
// Hooks
class YourWidget extends HookWidget {
const YourWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: useProvider(streamYourModelById('YourDataId')).when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text(err.toString())),
data: (yourData) => Center(child: Text(yourData?.toString() ?? 'Got Null')),
),
);
}
}
// Without Hooks
class YourWidget extends ConsumerWidget {
const YourWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
return Scaffold(
body: watch(streamYourModelById('YourDataId')).when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text(err.toString())),
data: (yourData) => Center(child: Text(yourData?.toString() ?? 'Got Null')),
),
);
}
}
You should be able to apply this pattern to accomplish whatever you need.

Related

Flutter + Firebase - Mutate in-memory data model or rely on stream for rebuilds

In the example below, CandidateData is deserialized from a Firebase Realtime database using a Stream. Properties from it are used in building a HiringManagerButton.
class CandidateData {
bool isEmployed;
String name;
}
class HiringManagerButton extends StatelessWidget {
final CandidateData data;
const HiringManagerButton({Key? key, required this.data}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text('${data.isEmployed ? 'Fire' : 'Hire'} ${data.name}'),
onPressed: onPressedFn
);
}
}
class SomeClass extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<CandidateData>(
stream: fromDatabaseStream,
builder: (context, state) => HiringManagerButton(data: state.data!));
}
}
How should onPressedFn operate? Should it mutate the CandidateData model by changing the value of isEmployed, which causes the Widget to instantly rebuild, then make the service call to update the backend? Or should it just make the service call to update the backend, and rely on the Stream to provide an updated value to rebuild the HiringManagerButton with?
EDIT: Code for the two options above:
Option 1:
onPressedFn = () async {
data.isEmployed = !data.isEmployed;
await context.read<DatabaseService>().update(data);
}
Option 2:
onPressedFn = () async {
await context.read<DatabaseService>().update(CandidateData(isEmployed: !data.isEmployed, name: data.name));
}
Decided to go with option 2 as the database is the source of truth. Moreover, Flutter Firebase seems to have some form of offline consistency, which results in performance being a non-issue - DB writes are reflected instantly locally.

What is the correct way to have a ChangeNotifier class depend on a Stream?

I have a class called MyChangeNotifier. This class is required to have a behaviour run every time an AppNotification class is published to a stream. I have this working, but I am not sure if it is the correct way to do this with provider library.
I provide the AppNotification stream using StreamProvider as follows...
MultiProvider(
providers: [
...,
StreamProvider<AppNotification?>.value(value: _notificationService.notificationsStream, initialData: null),
],
child: ...
),
Then down the widget tree I have a StatlessWidget whose job it is to specifically consume the AppNotification events from the stream, and pass them to the MyChangeNotifier class..
class AppNotificationConsumer extends StatelessWidget {
final Widget child;
const AppNotificationConsumer(this.child , {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<AppNotification?>(
builder: (context , notification , widget) {
if (notification != null) {
var model = Provider.of<MyChangeNotifer>(context , listen: false);
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
model.handleNotification(notification);
});
}
}
return child;
},
);
}
}
Here I have this logic wired up in the build method of a StatelessWidget. I have to use addPostFrameCallback because the handleNotification call, calls the overlay_support libray showNotification() which walks the widget tree (cannot be called in build).
Is there a way using a provider that I can bind the MyChangeNotifier and the AppNotification stream without using a StatelessWidget....or is this the correct approach?
There is an easier way to do it. Just use the ChangeNotifierProxyProvider
It would be like this:
MultiProvider(
providers: [
...,
StreamProvider<AppNotification?>.value(value: _notificationService.notificationsStream, initialData: null),
// Here ---v
ChangeNotifierProxyProvider<AppNotification?, MyChangeNotifer>(
create: (_) => MyChangeNotifer(),
update: (_, notification, myChangeNotifier) =>
myChangeNotifier!..handleNotification(notification),
),
],
child: ...
),
Every time there is a new AppNotification the update is called giving the chance to update other things. There is also ChangeNotifierProxyProvider2, ChangeNotifierProxyProvider3 (and so on) with 2 or 3 other dependencies respectively instead of just 1.

Read nested widget/class properties value in flutter

I'm building a simple app with lots of nested widgets/classes from different specialised files
list of files:
main.dart -> the menu file used to start the activity
"Activity()"
group_widgets.dart -> the file that contains the custom widget
"CustomWidget()"
file_a.dart -> the file that uses the custom widgets
inside the "Activity()"
other.dart -> other files that needs to manage data changed in CustomWidget()
inside main.dart:
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Activity(),
));
},
inside group_widgets.dart:
class CustomWidget extends StatefulWidget {
const CustomWidget({Key? key}) : super(key: key);
#override
State<CustomWidget> createState() => _CustomWidgetState();
}
class _CustomWidgetState extends State<CustomWidget> {
var _boolean = false;
bool switchBoolean(bool state) => !state;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => {
setState(() {
_boolean = switchBoolean(_boolean);
})
},
child: Container(
color: _boolean == true ? Colors.green : Colors.red,
),
);
}
}
inside file_a.dart
class Activity extends StatefulWidget {
const Activity({Key? key}) : super(key: key);
#override
State<Activity> createState() => _ActivityState();
}
class _ActivityState extends State<Activity> {
#override
Widget build(BuildContext context) {
bool boolean = true;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CustomWidget(),
Text('Here where to show the variable from CustomWidget'
'and prove I can retrieve it')
],
),
),
);
}
}
inside other.dart
if ( booleanFromCustomWidget == true) {
Something ...
}
What is the best practice to achieve it?
I've read a lot here but nothing seems to well fit my needing.
Just comment if my request is not as clear as it seems to me))
Please correct me if I am wrong, but if you want to access data from parent widgets from inside their descendants (children or even nested children) you can either pass them down via parameter arguments:
Child(int age, String name);
And then accept it in the new file, where the Child widget lives, via its constructor:
class Child {
String name;
int age;
// Constructor
Child(String passedName, int passedAge) {
this.name = passedName;
this.age = passedAge;
}
}
Inside the parent.dart you then have to import the children.dart to use it.
Or use a popular package like the provider package: https://pub.dev/packages/provider
This allows you to store data containers, which you can access basically anywhere in your code. Feel free to google it & watch some tutorials to get started, as it is the preferred approach to avoid passing data to widget which really do not care about the passed parameters.
Note: You can transfer the idea to output the String data like in your example code above.
you can use a state manager like provider, or bloc
At the top level, you set up the data services

How to make an object from a bloc available for all other bloc in Flutter

I am using Bloc for my Flutter project. I have created three blocs. These are AuthenticationBloc, FirebaseDatabaseBloc, and ChatMessagesBloc. When the user gets authenticated, AuthenticationBloc emits a state called authenticated with a user object.
I want to make this user object available inside FirebaseDatabaseBloc and ChatMessagesBloc. What is the clean way of doing this?
Well, This is year 2022 and a lot has changed. Bloc to Bloc to communication via the constructor is now considered a bad practice. Nobody said it won't work though but trust me, you'd end up tightly coupling your code.
Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs, as it creates tight-coupling which is hard to maintain. Since blocs reside in the business logic architectural layer, no bloc should know about any other bloc.
documentation.
You should rather try this:
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}
I hope it helps!
This is achievable by BLoC-to-BLoC communication. The simplest way is to pass your BLoC reference by the other's constructor and subscribe to BLoC changes:
#override
Widget build(BuildContext context) {
final authenticationBloc = AuthenticationBloc();
return MultiBlocProvider(
providers: [
BlocProvider<AuthenticationBloc>.value(value: authenticationBloc),
BlocProvider<FirebaseDatabaseBloc>(
create: (_) => FirebaseDatabaseBloc(
authenticationBloc: authenticationBloc,
),
),
],
child: ...,
);
}
Then, inside the FirebaseDatabaseBloc you can subscribe to changes:
class FirebaseDatabaseBloc extends Bloc<FirebaseDatabaseEvent, FirebaseDatabaseBloc> {
final AuthenticationBloc authenticationBloc;
StreamSubscription<AuthenticationState> _authenticationStateStreamSubscription;
FirebaseDatabaseBloc({
#required this.authenticationBloc,
}) : super(...) {
_authenticationStateStreamSubscription = authenticationBloc.listen(_onAuthenticationBlocStateChange);
}
#override
Future<void> close() async {
_authenticationStateStreamSubscription.cancel();
return super.close();
}
void _onAuthenticationBlocStateChange(AuthenticationState authState) {
// Do whatever you want with the auth state
}
}
For more info, you can check this video: https://www.youtube.com/watch?v=ricBLKHeubM

How to access data in Bloc's state from another bloc

I am developing a Flutter application using Bloc pattern. After success authentication, UserSate has User object. In all other Blocs, I need to access User object in UserState. I tried with getting UserBloc on other Bloc's constructor parameters and accessing User object. But it shows that User object is null. Anyone have a better solution?
class SectorHomeBloc extends Bloc<SectorHomeEvent, SectorHomeState> {
final OutletRepository outletRepository;
UserBloc userBloc;
final ProductRepository productRepository;
final ProductSubCategoryRepository productSubCategoryRepository;
final PromotionRepository promotionRepository;
final ProductMainCategoryRepository mainCategoryRepository;
SectorHomeBloc({
#required this.outletRepository,
#required this.userBloc,
#required this.productSubCategoryRepository,
#required this.productRepository,
#required this.promotionRepository,
#required this.mainCategoryRepository,
});
#override
SectorHomeState get initialState => SectorHomeLoadingState();
#override
Stream<SectorHomeState> mapEventToState(SectorHomeEvent event) async* {
try {
print(userBloc.state.toString());
LatLng _location = LatLng(
userBloc.state.user.defaultLocation.coordinate.latitude,
userBloc.state.user.defaultLocation.coordinate.longitude);
String _token = userBloc.state.user.token;
if (event is GetAllDataEvent) {
yield SectorHomeLoadingState();
List<Outlet> _previousOrderedOutlets =
await outletRepository.getPreviousOrderedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _featuredOutlets =
await outletRepository.getFeaturedOutlets(
_token, _location, event.orderType, event.sectorId);
List<Outlet> _nearestOutlets = await outletRepository.getOutletsNearYou(
_token, _location, event.orderType, event.sectorId);
List<Product> _newProducts = await productRepository.getNewItems(
_token, _location, event.orderType, event.sectorId);
List<Product> _trendingProducts =
await productRepository.getTrendingItems(
_token, _location, event.orderType, event.sectorId);
List<Promotion> _promotions = await promotionRepository
.getVendorPromotions(_token, event.sectorId);
yield SectorHomeState(
previousOrderedOutlets: _previousOrderedOutlets,
featuredOutlets: _featuredOutlets,
nearByOutlets: _nearestOutlets,
newItems: _newProducts,
trendingItems: _trendingProducts,
promotions: _promotions,
);
}
} on SocketException {
yield SectorHomeLoadingErrorState('could not connect to server');
} catch (e) {
print(e);
yield SectorHomeLoadingErrorState('Error');
}
}
}
The print statement [print(userBloc.state.toString());] in mapEventToState method shows the initial state of UserSate.
But, at the time of this code executing UserState is in UserLoggedInState.
UPDATE (Best Practice):
please refer to the answer here enter link description here
so the best way for that is to hear the changes of another bloc inside the widget you are in, and fire the event based on that.
so what you will do is wrap your widget in a bloc listener and listen to the bloc you want.
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<FirstBloc, FirstBlocState>(
listener: (context, state) {
if(state is StateFromFirstBloc){
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());}//or whatever you want
},
child: ElevatedButton(
child: Text('THIS IS NEW SCREEN'),
onPressed: () {
BlocProvider.of<SecondBloC>(context).add(SecondBloCEvent());
},
),
);
}
}
the lovely thing about listener is that you can listen anywhere to any bloc and do whatever you want
here is the official documentation for it
OLD WAY (NOT Recommended)
there is an official way to do this as in the documentation, called Bloc-to-Bloc Communication
and here is the example for this as in the documentation
class MyBloc extends Bloc {
final OtherBloc otherBloc;
StreamSubscription otherBlocSubscription;
MyBloc(this.otherBloc) {
otherBlocSubscription = otherBloc.listen((state) {
// React to state changes here.
// Add events here to trigger changes in MyBloc.
});
}
#override
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}
sorry for the late update for this answer and thanks to #MJ studio
The accepted answer actually has a comment in the above example in the official docs saying "No matter how much you are tempted to do this, you should not do this! Keep reading for better alternatives!"!!!
Here's the official doc link, ultimately one bloc should not know about any other blocs, add methods to update your bloc and these can be triggered from blocListeners which listen to changes in your other blocs: https://bloclibrary.dev/#/architecture?id=connecting-blocs-through-domain
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}