rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.28k stars 958 forks source link

A better tutorial for the basic ChangeNotifier workflow #130

Closed esDotDev closed 2 years ago

esDotDev commented 4 years ago

Following the usage examples of riverpod are hard because it references a lot of advanced use cases, like mixing with hooks, or the state_notifier package, it also adds more ambiguity because of it's discussion about usage outside of Flutter.

As a Flutter dev using Provider, looking to migrate, I would just like to see a more flutter focused / classic usage example:

  1. A ChangeNotifier model with multiple properties
  2. Views that interact/bind to that model in the most succinct way (read, watch, select)
  3. Lookup of that model from an out-of-tree layer (no context lookup)

Is there such an end-to-end example out there? This shgould be like <20 lines of code to demonstrate? I also think the main doc page on pub.dev could benefit from highliting this as it's initial example. And then subsequently, dive into more esoteric use cases.

Generally the docs feel like the author was really excited to talk about all the advanced use cases that are enabled, but vaults over the primary use case that devs want with riverpod, which is just decoupling from the context for reads.

esDotDev commented 4 years ago

Some other feedback, is I don't think this is a very good code snippet: http://screens.gskinner.com/shawn/chrome_2020-09-16_10-23-13.png

It assumes a bunch of pre-knowledge about what StateNotifier is, and does not even include the import. This leads one to assume this package is part of riverpod, and be confused when the class can not be found.

I would think if you want to use this as a primary snippet in the header of the website, this package needs to be there out of the box? When users are coming to a site to learn a specific thing, immediately throwing them a curveball is not nice :p

joanofdart commented 4 years ago

Hey @esDotDev

I had some time to work this one out and I can tell you... been there done that! :P

This is how I've been using it so far, do let me know if I can be of any more service! I'm mostly using Hooks, but you can just switch it up with a consumer widget or a consumer builder!

Using hooks:

some_view.dart

final someViewProvider = Provider.autoDipose<SomeViewModel>((ref) => SomeViewModel());

class SomeView extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final _someViewModel = useProvider(someViewProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('SomeView!'),
      ),
      body: RaisedButton(
          onPressed: _someViewModel.updateTitle,
          child: Text(_someViewModel.title),
       ),
    );
  }
}

some_view_model.dart

class SomeViewModel extends ChangeNotifier {

  final title = 'SomeView!!! Yikes :)';

  void changeTitle() {
    title = 'New title!';
    notifyListeners();
  }

}

If you don't wanna use hooks, then you'd basically change it to be something like this!

some_view.dart

final someViewProvider = Provider.autoDipose<SomeViewModel>((ref) => SomeViewModel());

class SomeView extends ConsumerWidget{
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final _someViewModel = watch(someViewProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('SomeView!'),
      ),
      body: RaisedButton(
          onPressed: _someViewModel.updateTitle,
          child: Text(_someViewModel.title),
       ),
    );
  }
}
joanofdart commented 4 years ago

This is from another issue I posted (mostly when i was learning about riverpod), you might find it useful! This one is using StateNotifier.


final authenticationServiceProvider = Provider<AuthenticationService>((ref) => AuthenticationService());
final firestoreServiceProvider = Provider<FirestoreService>((ref) => FirestoreService());

class ProfileStateNotifier extends StateNotifier<AsyncValue<AppUser>> {
  final Reader _reader;

  ProfileStateNotifier() : super(const AsyncValue.loading()) {
    _fetch();
  }

  _fetch() {
    final userId = _reader(authenticationServiceProvider).currentUser.uid;
    _reader(firestoreServiceProvider).getUserRecordAsStream(userId).listen((event) {
      state = AsyncValue.data(event);
    });
  }

  void updateUser(AppUser updatedUser) async {
    state = AsyncValue.data(updatedUser);
    await _reader(firestoreServiceProvider).updateUserData(updatedUser);
  }
}

then I'm calling this through a statenotifierProvider in the consumerWidget like this:

final profileStateNotifierProvider = StateNotifierProvider((ref) => ProfileStateNotifier());

class ProfileView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final _profileModel = watch(profileStateNotifierProvider.state);

    return _profileModel .when(
      data: (user) {
        return Scaffold(
          backgroundColor: Colors.lightBlue[900],
          appBar: AppBar(
            actions: [
              IconButton(
                icon: Icon(Icons.search),
                color: Colors.white,
                onPressed: () {},
              )
            ],
            backgroundColor: Colors.lightBlue[900],
            title: Text('Profile'),
            centerTitle: true,
            textTheme: TextTheme(
              headline6: TextStyle(
                color: Colors.white,
              ),
            ),
            elevation: 4,
          ),
          body: ListView(
            children: [
              CircleAvatar(
                backgroundColor: Colors.black87,
                radius: 101,
                child: CircleAvatar(
                  radius: 100,
                  backgroundImage: AssetImage(user.photoURL),
                ),
              ),
              Center(child: Text(user.displayName)),
              RaisedButton(
                color: Colors.lightBlue[900],
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  'Read More',
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
                onPressed: () {
                  final modifiedUser =
                      user.copyWith(aboutMe: 'testing if this updates using Riverpod');
                  _profileModel.updateUser(modifiedUser);
                },
              ),
            ],
          ),
        );
      },
      loading: () => Center(
        child: CircularProgressIndicator(),
      ),
      error: (error, stack) => Center(
        child: Text('Error $error'),
      ),
    );
  }
}

Do bare in mind I just edited this here BUT it should be close if not, working : )

esDotDev commented 4 years ago

Interesting, thanks for the examples.

So if I'm reading this right...

rrousselGit commented 4 years ago

That "reader" object is just ref.read. It's not an object but a typedef

select isn't implemented yet for ConsumerWidget/Consumer.

coyksdev commented 4 years ago

This is from another issue I posted (mostly when i was learning about riverpod), you might find it useful! This one is using StateNotifier.

final authenticationServiceProvider = Provider<AuthenticationService>((ref) => AuthenticationService());
final firestoreServiceProvider = Provider<FirestoreService>((ref) => FirestoreService());

class ProfileStateNotifier extends StateNotifier<AsyncValue<AppUser>> {
  final Reader _reader;

  ProfileStateNotifier() : super(const AsyncValue.loading()) {
    _fetch();
  }

  _fetch() {
    final userId = _reader(authenticationServiceProvider).currentUser.uid;
    _reader(firestoreServiceProvider).getUserRecordAsStream(userId).listen((event) {
      state = AsyncValue.data(event);
    });
  }

  void updateUser(AppUser updatedUser) async {
    state = AsyncValue.data(updatedUser);
    await _reader(firestoreServiceProvider).updateUserData(updatedUser);
  }
}

then I'm calling this through a statenotifierProvider in the consumerWidget like this:

final profileStateNotifierProvider = StateNotifierProvider((ref) => ProfileStateNotifier());

class ProfileView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final _profileModel = watch(profileStateNotifierProvider.state);

    return _profileModel .when(
      data: (user) {
        return Scaffold(
          backgroundColor: Colors.lightBlue[900],
          appBar: AppBar(
            actions: [
              IconButton(
                icon: Icon(Icons.search),
                color: Colors.white,
                onPressed: () {},
              )
            ],
            backgroundColor: Colors.lightBlue[900],
            title: Text('Profile'),
            centerTitle: true,
            textTheme: TextTheme(
              headline6: TextStyle(
                color: Colors.white,
              ),
            ),
            elevation: 4,
          ),
          body: ListView(
            children: [
              CircleAvatar(
                backgroundColor: Colors.black87,
                radius: 101,
                child: CircleAvatar(
                  radius: 100,
                  backgroundImage: AssetImage(user.photoURL),
                ),
              ),
              Center(child: Text(user.displayName)),
              RaisedButton(
                color: Colors.lightBlue[900],
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  'Read More',
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
                onPressed: () {
                  final modifiedUser =
                      user.copyWith(aboutMe: 'testing if this updates using Riverpod');
                  _profileModel.updateUser(modifiedUser);
                },
              ),
            ],
          ),
        );
      },
      loading: () => Center(
        child: CircularProgressIndicator(),
      ),
      error: (error, stack) => Center(
        child: Text('Error $error'),
      ),
    );
  }
}

Do bare in mind I just edited this here BUT it should be close if not, working : )

I could not see where you initialize the _read variable. hehehe! It could be like this right?

    class ProfileStateNotifier extends StateNotifier<AsyncValue<AppUser>> {
      final Reader _reader;

      ProfileStateNotifier(Reader read) : super(const AsyncValue.loading()) {
        _reader = read; 
        _fetch();
      }

      ...
    }
yaymalaga commented 4 years ago

I would like to see more complete examples using StateNotifierProvider, as the one from @joanofdart which includes network calls. I wasn't really sure if it was better to create a FutureProvider for doing the http calls and then watch it inside the StateNotifierProvider.

The same goes for reading information from the local storage as example, so I guess the missing tutorials are how to integrate services. Moreover, the only example right now using the StateNotifierProvider is the todo-list.

joanofdart commented 4 years ago

Hey 👋🏼

To initialize the Reader variable, you pass it through the provider builder, e.g:

final someProvider = Provider<SomeModel>((ref) => SomeModel(ref.read));

Then in the class itself you'll receive

class SomeModel {
  final Reader _reader;

  SomeModel(this._reader);
}

As @rrousselGit stated above, its just a typedef.

@yaymalaga perhaps I can do a simple/advanced example using network calls. Let me know what do you guys think?

yaymalaga commented 4 years ago

@joanofdart Sound great! I was also taking a look to the bloc examples and this one seems a perfect candidate https://bloclibrary.dev/#/flutterweathertutorial as it is using two endpoints and one depends on the other.

For example in that case, I was thinking about using a FutureProvider for getting the location, then another FutureProvider watching that location for getting the forecast, so we could finally watch it in a StateNotiferProvider to handle the state using freezed unions (same behavior as using bloc+cubits). Maybe this is overkill and you should just do everything inside the StateNotiferProvider or handle directly the state using the FutureProviders (like in the Marvel example).

esDotDev commented 3 years ago

So I've circled back on this about a year later, and have put together what I think is a good classic example for ChangeNotifier. It's inspired by the current TodoExample, but with a different flavor.

https://user-images.githubusercontent.com/736973/138825230-20a70eb7-777d-4ba4-a620-b96474be6eba.mp4

It retains the classic structure you might have used in the past, with a complex ChangeNotifier, whos properties can easily be bound to using .select. Really the only thing new to learn, if coming from Provider, is the Consumer/ConsumerWidget/ConsumerState classes, and how to declare the new style of providers.

todo_model.dart

class TodoItem {
  TodoItem(this.id, {required this.text, this.isCompleted = false});
  final String id;
  String text;
  bool isCompleted;
}

class TodoModel extends ChangeNotifier {
  TodoModel(TodoService service) {
    _service = service;
    loadItems();
  }
  late final TodoService _service;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  List<TodoItem> _all = [];
  List<TodoItem> get all => _all;
  EquatableList<TodoItem> get completed => EquatableList(_all.where((i) => i.isCompleted).toList());
  EquatableList<TodoItem> get active => EquatableList(_all.where((i) => !i.isCompleted).toList());

  void addItem(TodoItem value) {
    _all.add(value);
    _service.save(_all);
    notifyListeners();
  }

  void removeItem(TodoItem value) {
    _all.removeWhere((item) => item.id == value.id);
    _service.save(_all);
    notifyListeners();
  }

  void updateItem(TodoItem value) {
    for (var i = 0; i < _all.length; i++) {
      if (_all[i].id == value.id) _all[i] = value;
    }
    _service.save(_all);
    notifyListeners();
  }

  void loadItems() async {
    _isLoading = true;
    notifyListeners();
    _all = await _service.load();
    _isLoading = false;
    notifyListeners();
  }
}
// Enables deep equality check on lists, so we can more easily optimize rebuilds, requires `equatable` package
class EquatableList<T> with EquatableMixin {
  EquatableList(this.items);
  final List<T> items;

  @override
  List<Object?> get props => items;
}

todo_service.dart - A mock service that pretends to load/save data

/// Todo Service, saves and loads the app state
class TodoService {
  Future<List<TodoItem>> load() async {
    debugPrint("TODO: Actually load the data");
    await Future.delayed(const Duration(seconds: 2));
    return [
      TodoItem('todo-0', text: 'hi'),
      TodoItem('todo-1', text: 'hello'),
      TodoItem('todo-2', text: 'bonjour'),
    ];
  }

  Future<bool> save(List<TodoItem> items) async {
    debugPrint("TODO: Actually save the data");
    return true;
  }
}

providers.dart

class AppProviders {
  static final todoService = Provider((_) => TodoService());

  static final todoModel = ChangeNotifierProvider((ref) {
    // Inject the todo service into the model
    TodoService service = ref.watch(todoService);
    return TodoModel(service);
  });
}

Views can then bind to model fields using ref.watch(modelProvider.select((m) => m.foo)) or call methods on the models with ref.read(modelProvider).doFoo()

// Rebuild when the "active" set of todo items change
Widget build(BuildContext context, WidgetRef ref){
    List<TodoItem> items = ref.watch(AppProviders.todoModel.select((m) => m.active)).items;
    return _TodoListView(items);
}

// Make a call on the model to do something
void _handleAddItemPressed() {
  TodoModel model = ref.read(AppProviders.todoModel);
  model.addItem(TodoItem(...));
}
TimWhiting commented 3 years ago

@esDotDev If you want to make a pull request to add such a tutorial to the documentation, I think it would be helpful for riverpod beginners.

esDotDev commented 3 years ago

Sure! Busy week this week, but more than happy to do that

joshminton commented 3 years ago

@esDotDev For what it's worth, I've come to Riverpod after reading the general Flutter 'intro to state' doc that uses ChangeNotifier, and your example was super helpful and I suspect would be similarly appreciated by others. Thank you!

esDotDev commented 3 years ago

Thanks! I'll add a PR to the doc and also write a blog post on this next week.

rivella50 commented 2 years ago

@esDotDev Do you have a link to that blog post? I would be very interested in reading it.

rrousselGit commented 2 years ago

A doc for ChangeNotifierProvider was added, so I'll close this https://riverpod.dev/docs/providers/change_notifier_provider