I have a a StreamProvider that exposes the User. I want to run logic in the background based on the provider. I don't want to consume the providers in my Widget tree. I'm interested in:
Where to initialise the logic - is main.dart appropriate?
If the logic isn't related to a ConsumerWidget, where would I access my WidgetRef?
How should I encapsulate my logic? Ie: Which classes or methods should I use?
final currentUserProvider = StreamProvider<User?>((ref){
return CurrentUserStream(); // The details don't matter
});
Below I have a couple of simplified use-cases.
Use-Case 1: Apply my userId somewhere
I want to make sure the userId is used for my analytics service
final User? user = ref.watch(currentUserProvider);
if (user != null) {
// Update analytics to track the userId
}
Use-Case 2: Manage Streams for the user
In this case, it's critical that I have a dispose method to tidy up after the user changes. This would be triggered if the user logs out / switches account.
final User? user = ref.watch(currentUserProvider);
/// Streams related to the user
List<StreamSubscription> userStreamSubscriptions;
if (user != null) {
// The details don't matter
}
/// Cancels the user Streams when the user changes
#override
void dispose() {
for (var subscription in userStreamSubscriptions) {
subscription.cancel();
}
super.dispose();
}
I don`t know can that help you and will actual for you, but anyway after little research I understand one way. For background logic I was used ConsumerStatefulWidget with his state. In state class we need declare login in initState method, because the state class have a ref field.
My example app (plus flutter_hooks, but not a lot) https://github.com/nicourrrn/flutter-state-managment
Related
I'm currently developing an app with thousands of items that users can buy.
What I'd like to do
There are a large number of items and all of them have the same price, so I think it'd be better to use only one IAP product for those different items.
What I won't do
Another solution is to sell credits used to exchange for items.
I won't choose this solution unless it is the only option because I doubt it gives a good user experience.
What I already know
The items are actually not consumed (they remain in the app forever), but because a non-consumable product can be purchased only once, a consumable needs to be used instead.
The purchased items must be stored in a server to be shared across devices since Google and Apple don't provide the mechanism for restoring consumables.
Question
Below is a simplified version of the code from the Codelab.
The data of the item that a user is about to purchase has to be passed from [a] to [b], but it looks impossible. How can I do it instead?
Is it only that the Flutter package lacks the feature while the native side has a support for it? (Should I file an issue about it on GitHub?)
class DashPurchases {
/// A method that is called when a user presses a purchase button.
Future<void> buy(PurchasableProduct product) async {
// [a]
// Neither PurchaseParam nor buyConsumable() has a parameter/property
// to hold additional data like the item that a user is about to buy.
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
}
/// A callback function that is called when the purchase is finished, cancelled, etc.
Future<void> _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
final validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// [b]
// The item should be given to the user here, but I have
// no idea how to identify the item that the user purchased.
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
}
I came up with the idea of adding the item to a queue (local DB or somewhere) at [a] and taking it out at [b].
However, it has several problems as follows.
It is quite cumbersome to track which item is pending or make the code robust. The purchase ID has to be added to the stored data to make it associated with the item as soon as _handlePurchase() is called so that the function can be retried after some failure in the backend.
purchaseDetails has no purchase ID on PurchaseStatus.error. It is difficult to tell whether an item with no associated ID is the result of an error or it is still just pending.
Multiple purchase details may be passed to _onPurchaseUpdate() in a different order than the actual purchases.
Resolving these will lead to very complicated code, and it doesn't seem very safe either.
I have a flutter app, which fetches a list of products from the server (product are related to the user, so the list doesn't change that much) so I'm saving the product lists locally. I'm using moor as a local database inside the application.
when the user opens the products list page:
I get the latest product creation date.
Request data created after that date from the server async.
I open a stream from the database to load the local data.
When the the server response arrives with new data, I save it to the database, then the database stream will provide that data.
the code:
Stream<List<Product>> getAllProducts() async* {
this
.fetchProductsOnline() // request data from server
.then((value) => this.insertMultipleProducts(value)); // then save the received data locally
yield* getLocalProductsList(); // meanwhile get the local data, and listen to new changes.
}
then inside my BLoC I have to listen to that stream of data, and then emit states containing the data.
but I don't seem to find the best/proper way to do that.
what I tried:
yield the stream of data after mapping it, and wrapping its items inside the state:
Stream<ProductState> mapProductListPageOpenedEventToState() async* {
yield ProductListLoading();
inProgress(); // enters the ui into a progress state
yield* this
.productRepository
.getAllProducts()
.map((event) => ProductListLoadedSuccess(event));
await outProgress(); // exists the ui from the progress state
}
The problems I faced here are:
- the code right under the stream yield, never gets executed (idk why).
- it only work once, means if I exit that page and reopen it (trigger the load product even again), nothing would happen (no state change, no event triggered).
listen to the stream of data, and use the emit() method to emit new states.
Stream<ProductState> mapProductListPageOpenedEventToState() async* {
yield ProductListLoading();
inProgress(); // enters the ui into a progress state
this.productRepository.getAllProducts().listen((event) {
if (event.isNotEmpty) {
emit(ProductListLoadedSuccess(event));
outProgress();
}
});
}
this approach worked properly, I achieved what I wanted, but the docs say that the emit() method, should be used for test purposes only, and that states from bloc should be yield only.
I'd appreciate any other solution that satisfies my needs, and that doesn't break any rules set by the docs.
I'm looking for suggestions on how to handle loading things after login, and combine data from multiple endpoints without mixing too much stuff, specifically the following two things:
1) Login flow
After I get a successful login response with userId, I need to push HomeScreen() and load some initial data, from various providers.
Example:
// home_screen.dart
initState() {
super.initState();
initializeData();
}
Future <void> initializeData() {
var authenticationProvider = Provider.of<AuthenticationProvider>(context);
var accountProvider = Provider.of<AccountProvider>(context);
var albumProvider = Provider.of<AlbumProvider>(context);
var songProvider = Provider.of<SongProvider>(context);
await accountProvider.loadAccount(authenticationProvider.getLoggedInUser().id);
await albumProvider.loadAlbums();
await songProvider.loadSongsForAlbums(albumProvider.getAlbums())
}
This works, but feels ugly?
2) Cleaner data sharing
Imagine the API like this:
api/albums (model contains info about album)
api/albumpurchases (model contains which albumId and userId)
What would be the best way to get purchased albums of the logged in user?
I can think of 3 different ways, none of which seem good:
AlbumProvider having two arrays, albums[], and purchases[], and a method getAlbumByPurchase (String purchaseId) or getPurchaseForAlbum(String albumId) which then does .where() by Id and returns the item.
Having AlbumProvider and AlbumPurchaseProvider, then using ProxyProvider to combine the two.
I'm not sure how exactly would that be implemented, an example would be very appreciated!
Adding purchase property to an Album, then manually mapping it similarly to way #1
.
I've used this so far, and it seems great in the beginning but gets very ugly very quick since your subsequent Album HTTP responses everywhere would be lacking that purchase property, so I either need to get the purchase again, or I need to get it an Album object with the purchase from AlbumProvider based on an Id in response I got.
I am currently working on a flutter app using firestore and mobx. I use mobx to keep the UI up to date, and make api calls to firestore (so the data flow is firestore -> mobx store -> UI). I want to set up listeners to listen for real-time changes in a firestore collection. Ideally, I would like to set up this listener in mobx, but I'm not sure how this would work - is a mobx store the right place to listen for firestore changes? One thing I'm worried about is that there is no dispose method in the mobx store in which I can detach listeners. I am wondering if this is an acceptable way to update variables in my store (and thus the UI, indirectly), or if I need to switch to BLoC/stream model. Any general advice about this issue (i.e. the best way to listen for real-time firestore updates and propagate the changes to the UI) would be appreciated!
I am not using flutter but I guess it shouldn't be very different.
Here is an example of how I listen to a user profile changes in my app.
class UserModel {
#observable id = ''
updateDetails (userUpdate) {
// update observable properties
}
destroy () {
// Call destroy to remove listener
if (this.stopWatch) {
this.stopWatch()
}
}
init () {
// firestore onSnapshot returns a disposer, keep it on the instance
this.stopWatch = fdb.collection('users').doc(this.id).onSnapshot((doc) => {
if (doc.exists) {
this.updateMyDetails(doc.data())
}
})
}
constructor ({id}) {
// ...
this.id = id
}
}
const user = new UserModel({id: 'firestoreId')})
user.init()
// then any observer, like your UI, is listening to changes of the userModel data
//...
user.destroy() // for example when the user signs out.
Note that you could listen to changes outside of the model if you want to keep these concerns separated, instead of having this init function.
If you are wondering why I check if (doc.exists) it's because Firestore won't send you any error if the document does not exist. (like an http 404). You need to handle it yourself.
I am using flutter to exchange firestore data from few devices.
If I use StreamBuilder everything works fine, but I do not like mixing business logic with UI. I would prefer using BLOC as pattern using flutter_bloc plugin.
But flutter_bloc works in this way:
Steps:
Event ------------------------> New data BUT NO NEW UI EVENT
Async Request
Async Response
State (mapEventToState)-------> ¿How do I get the new state?
As far as I do not have "UI Event" because firestore data is being updated from another device, I can not update State.
I could use something like this on the bloc constructor:
Stream<QuerySnapshot> query;
QuedadaBloc(){
query = Firestore.instance.collection('my_collection').snapshots();
query.listen((datos){
dispatch(Fetch()); // send fictitious UI event
});
}
But I think this is not the right way.
¿Any suggestion?
Many thanks.
J. Pablo.
The recommended way while using Flutter, Bloc and Firestore is to have the repository layer provide a stream of data from Firestore which can be subscribed by the Bloc in Bloc Constructor (or any other function; see this example).
Then, based on the changes in the stream, dispatch events when you receive new data from Firestore in the stream. The Bloc can handle the triggered dispatch event to change the State of the Application in a similar way when the changes in UI trigger the state change.
class SampleBloc extends Bloc<SampleEvent, SampleState> {
final FirestoreRepo _firestoreRepo;
StreamSubscription<?> _firestoreStreamSubscription;
SampleBloc({#required FirestoreData firestoreData})
: assert(firestoreRepo != null),
_firestoreRepo = firestoreRepo;
// instead of '_mapXEventToState', the subscription can also be wired in 'SampleBloc' constructor function.
Stream<TimerState> _mapXEventToState(XEvent xEvent) async* {
// Runs a dispatch event on every data change in Firestore
_firestoreStreamSubscription = _firestoreRepo.getDataStream().listen(
(data) {
dispatch(YEvent(data: data));
},
);
}
References:
Comment 1 and Comment 2 by Felix Angelov (felangel), flutter_bloc library creator in Bloc Gitter Chat