Closed rivella50 closed 2 years ago
Hello!
You can wrap ProviderScope
inside CreatorGraph
or vice versa. There is no side effect for that since they are just simple widgets underhood.
The names creator/emitter/watcher are different from providers, so there are minimum name conflicts. However, the name Ref
is the same in both packages. That means if you use both provider and creator in the same file, you will likely need to hide Ref
or rename one of the packages in import statements.
In a summary, use Emitter for anything with async
and use Creator for the rest. Full list below:
Riverpod Provider | Creator |
---|---|
Provider | Creator |
StateProvider | Creator |
FutureProvider | Emitter |
StreamProvider | Emitter.stream |
StateNotifierProvider | Creator with a file-level private variable (example) or a class-level private variable |
ChangeNotifierProvider | Not supported. Note ChangeNotifierProvider is not encouraged in riverpod |
Firstly, use the extension methods. Since there are only two types of creators, extension methods (map
, where
, reduce
, and change
, asyncData
) are possible. They all return normal creators (unlike select
future
in riverpod, which returns a special internal provider).
Second, compose multiple creators with set
emit
to express complex logic concisely. Sometimes, you don't need to put fields inside an immutable class, instead, just define creators for each field. See this example.
Let me know if you have any questions or thoughts! Thanks!
To be honest, I migrated my own project from riverpod to creator in a few hours, but it was a full migration, so I didn't have an experience that they co-exist in a project. Let me know if you see issues.
Thank you very much for your explanations. I'll let you know about my impressions.
One thing I forgot to mention is that remember riverpod is default to keep alive while creator is default to auto dispose.
I've seen that. That's why i use Riverpod's autoDispose feature for all my stream providers and see some issues there with disposing and relistening.
In my project my view models have quite some provider listeners like this:
class ViewModel {
ViewModel(this._ref, this.contractId) {
_ref.listen<AsyncValue<List<Account>>>(
contractorAccountsStreamProvider('contractorId'), (previous, next) {
/// ...
}
);
}
final AutoDisposeChangeNotifierProviderRef _ref;
final String contractId;
}
Passing a Ref instance is no problem, but how do i listen to a stream provider in there? You only recommend read and set, but not watch.
(typing on phone with wine) I guess the answer depends on what do you do inside those listener. If it has side effects which doesn't change the view model property, I think you can pull the logic out and create a separate Creator<void>
which watch stream_creator.change
, then watch this new creator in a Watcher listener or the place you watch the view model.
Hmmmm, but using a Watcher means we are in the view. In the view model i currently listen to some stream providers and have to use some logic on incoming change events before the corresponding models and view are updated. So with this code:
ViewModel(this._ref, this.contractId) {
_ref.watch(contractorAccountsStreamProviderCreator.change);
}
final contractorAccountsStreamProviderCreator = Emitter.stream((ref) {
return FirestoreDatabase(uid: 'dfdf').contractorAccountsStream('contractId');
});
How can i react to incoming stream changes in the view model? That's where Riverpod's .listen comes in quite handy since it allows listening to providers outside of a view. Is that also possible with Creator?
Sorry if I was clear earlier.
I'm assuming you want to do something with the previous and current value of contractorAccountsStreamProviderCreator
. Let's say you want to print them. Then this will be the logic layer:
final contractorAccountsStreamCreator = Emitter.stream((ref) {
return FirestoreDatabase(uid: 'dfdf').contractorAccountsStream('contractId');
});
final doSomethingCreator = Creator<void>( (ref) {
final change = ref.watch(contractorAccountsStreamCreator.change);
print('changed from ${change.before} to ${change.after}');
});
Then you just need to watch the doSomethingCreator
at some place, so it does the work. I guess the best place is where you watch contractorAccountsStreamCreator
. You can do:
Watcher( (context, ref, _) {
ref.watch(doSomethingCreator);
final contractor = ref.wath(contractorAccountsStreamCreator);
return ContractorWidget(contractor);
});
Or add a Watcher at somewhere:
Watcher(null, listener: (ref) => ref.watch(doSomethingCreator), child: SomeChildWidget());
Hope this makes more sense.
That makes all sense, yes, but i still don't know how to listen to the stream creator within my view model and react to changes in there. Your mentioned Watcher is again acting in a connected view. Let's leave the view aside for a moment. My question is just how i can imitate Riverpod's .listen feature with Creator like this in the view model's constructor:
ViewModel(this._ref, this.contractId) {
_ref.listen<AsyncValue<List<Account>>>(
contractorAccountsStreamProvider('contractorId'), (previous, next) {
if (next != null && next.hasValue) {
_handleContractorAccountsUpdate(next.value!);
}
}
);
}
I.e. directly use the passed Ref object and listen to a stream provider and use a callback to handle the updated data in the next object.
Creator doesn't have a listen
syntax, jobs having side effects should be in their own creators. Then, you can watch a Creator<void>
inside ViewModel, since void
never changes, your ViewModel will not rebuild.
My point was if _handleContractorAccountsUpdate
doesn't update the ViewModel, pull the logic out of the ViewModel into doSomethingCreator
.
If _handleContractorAccountsUpdate
updates the ViewModel, then who is watching ViewModel? I'm assuming you have a final viewModelProvider = Provider.family<ViewModel, String>( (ref, contractorId) => ViewModel(ref, contractorId));
and someone watches it? If _handleContractorAccountsUpdate
update view model, how do you propagate the change reactively?
_handleContractorAccountsUpdate does update a property in ViewModel and a corresponding view is watching it as a ConsumerWidget - and yes, ViewModel uses ChangeNotifier, the one that is not supported with Creator and not encouraged to use with Riverpod. But for my needs it works best. This is what i have now, but change.after is always null (i guess this is because of FutureOr):
final contractDetailsModelProvider =
Creator((ref) => ViewModel(ref, 'contractId'));
final doSomethingCreator = Creator<void>( (ref) async {
final change = await ref.watch(contractorAccountsStreamProviderCreator.change);
print('changed from ${change.after}');
});
class ViewModel with ChangeNotifier {
ViewModel(this._ref, this.contractId) {
_ref.watch(doSomethingCreator);
}
final creator.Ref _ref;
final String contractId;
}
final contractorAccountsStreamProviderCreator = Emitter.stream((ref) {
return contractorAccountsStream('contractorId');
});
FutureOr<Stream<dynamic>> contractorAccountsStream(String contractorId) =>
_service.collectionStreamChanges(
path: FirestorePath.account(),
builder: Account.fromMap,
queryBuilder: (query) => query
.where('contractorId', isEqualTo: contractorId)
.where('isDeleted', isEqualTo: false),
);
FutureOr<Stream<dynamic>> collectionStreamChanges<T>({
required String path,
required T Function(
Map<String, dynamic>? data,
String documentID,
String changeType,) builder,
Query<Map<String, dynamic>>? Function(Query<Map<String, dynamic>> query)?
queryBuilder,
int Function(T lhs, T rhs)? sort,
}) {
Query<Map<String, dynamic>> query =
FirebaseFirestore.instance.collection(path);
if (queryBuilder != null) {
query = queryBuilder(query)!;
}
final Stream<QuerySnapshot<Map<String, dynamic>>> snapshots =
query.snapshots();
return snapshots.map((snapshot) {
final result = snapshot.docChanges
.map((element) => builder(
element.doc.data(),
element.doc.id,
element.type.name,),)
.where((value) => value != null)
.toList();
if (sort != null) {
result.sort(sort);
}
return result;
});
}
Try something like this. I think it is because doSomethingCreator
's state is a Future
final contractDetailsModelProvider =
Creator((ref) => ViewModel(ref, 'contractId'));
class ViewModel with ChangeNotifier {
ViewModel(this._ref, this.contractId) {
_ref.watch(doSomethingCreator);
}
final creator.Ref _ref;
final String contracted;
final doSomethingCreator = Emitter<void>( (ref, emit) async {
final change = await ref.watch(contractorAccountsStreamProviderCreator.change);
print('changed from ${change.after}');
// ... make changes to view model (update: seems doesn't work, cannot access view model's field.)
notifyListeners();
});
}
This should work:
final streamCreator = Emitter.stream((ref) => Stream.fromIterable([1, 2, 3]));
class ViewModel {
ViewModel(this._ref, this.contractId) {
_ref.watch(doSomethingCreator);
}
final Ref _ref;
final String contractId;
int foo = 0;
}
final contractDetailsModelProvider =
Creator((ref) => ViewModel(ref, 'contractId'));
final doSomethingCreator = Emitter<void>((ref, emit) async {
final change = await ref.watch(streamCreator.change);
print('changed from ${change.after}');
ref.read(contractDetailsModelProvider).foo = change.after;
// ref.read(contractDetailsModelProvider).notifyListeners();
});
Future<void> main(List<String> args) async {
final ref = Ref();
ref.watch(contractDetailsModelProvider);
await Future.delayed(const Duration());
print('foo: ${ref.read(contractDetailsModelProvider).foo}');
}
Prints:
changed from 1
changed from 2
changed from 3
foo: 3
Yep that works fine now. Thank you so much for your support! One last question: I switch between 3 screens in my test app, and one of them watches the view model provider:
@override
Widget build(BuildContext context) {
context.ref.watch(contractDetailsModelProvider);
When switching to another view i would assume that all used providers are disposed (what your doc also says), but this is not the case. Everything seems to work as if i never had left the first view, i.e. there is also no print output in doSomethingCreator. According to your last example code above how would the correct disposal work in order that all watchers need to start again when coming back to the first view?
Ok great, this seems to do the trick:
class Screen1 extends StatefulWidget {
const Screen1({Key? key}) : super(key: key);
@override
State<Screen1> createState() => _Screen1State();
}
class _Screen1State extends State<Screen1> {
creator.Ref? ref;
@override
Widget build(BuildContext context) {
ref = context.ref;
final prov = ref!.watch(contractDetailsModelProvider);
return Column(
children: const [
Text('Screen 1'),
],
);
}
@override
void dispose() {
ref?.dispose(contractDetailsModelProvider);
super.dispose();
}
}
That works, but I would do this, which is simpler and can use StatelessWidget.
@override
Widget build(BuildContext context) {
return Watcher(null, listener: (ref) =>
ref.watch(contractDetailsModelProvider)),
child: Column(
children: const [
Text('Screen 1'),
],
));
}
Even better :-) Thanks a lot, i've learned a lot today.
So have you finished migration, how is your experience with creator so far? If there is no other issue, I will close this.
Well it was just a small part of my current mobile app to see if the migration would work. I had some problems with the naming, but that's normal i guess. Overall still looks promising to me. And yes, you can close the ticket. Have a nice evening.
Hi there, i'm looking forward to test Creator in a new mobile app. Since the naming is quite different and there are actually only Creator and Emitter around i wonder if this package covers all different types of Providers and cases which Riverpod offers? Since the app has to be wrapped with CreatorGraph i have my doubts that Riverpod and Creator can be used side by side in the same app. If that's the case it would be interesting to see a section in your documentation "Migrating from Riverpod" which shows how the various provider usages would look with Creator. Thanks for offering the package. Making state management easier is always a good task - and i've also discovered some flaws when using Riverpod.