Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coverage of Riverpod's Providers #3

Closed
rivella50 opened this issue Jun 17, 2022 · 21 comments
Closed

Coverage of Riverpod's Providers #3

rivella50 opened this issue Jun 17, 2022 · 21 comments

Comments

@rivella50
Copy link

rivella50 commented Jun 17, 2022

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.

@terryl1900
Copy link
Collaborator

terryl1900 commented Jun 17, 2022

Hello!

  1. You can use Creator and Riverpod in the same project.

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.

  1. Creator covers all riverpod providers except for ChangeNotifierProvider (which is for migrating from the provider package).

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
  1. Once you migrate, leverage the creator's unique power.

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!

@terryl1900
Copy link
Collaborator

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.

@rivella50
Copy link
Author

Thank you very much for your explanations.
I'll let you know about my impressions.

@terryl1900
Copy link
Collaborator

One thing I forgot to mention is that remember riverpod is default to keep alive while creator is default to auto dispose.

@rivella50
Copy link
Author

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.

@rivella50
Copy link
Author

rivella50 commented Jun 18, 2022

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.

@terryl1900
Copy link
Collaborator

terryl1900 commented Jun 18, 2022

(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.

@rivella50
Copy link
Author

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?

@terryl1900
Copy link
Collaborator

terryl1900 commented Jun 19, 2022

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.

@rivella50
Copy link
Author

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.

@terryl1900
Copy link
Collaborator

terryl1900 commented Jun 19, 2022

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?

@rivella50
Copy link
Author

_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;
    });
  }

@terryl1900
Copy link
Collaborator

terryl1900 commented Jun 19, 2022

Try something like this. I think it is because doSomethingCreator's state is a Future, void is the same, but future will be a different object every time.

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();
  });
}

@terryl1900
Copy link
Collaborator

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

@rivella50
Copy link
Author

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?

@terryl1900
Copy link
Collaborator

Please read this and this.

You need to wrap it inside a Watcher and use the ref provided by Watcher. This way, when Watcher is disposed, its dependencies are also disposed.

If you use context.ref.watch, you need to manually do ref.dispose somewhere in the widget lifecycle.

@rivella50
Copy link
Author

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();
  }
}

@terryl1900
Copy link
Collaborator

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'),
        ],
      ));
  }

@rivella50
Copy link
Author

Even better :-)
Thanks a lot, i've learned a lot today.

@terryl1900
Copy link
Collaborator

So have you finished migration, how is your experience with creator so far? If there is no other issue, I will close this.

@rivella50
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants