liangxianzhe / creator

A state management library that enables concise, fluid, readable and testable business logic code.
MIT License
212 stars 19 forks source link

Request for the documentation of a typical ViewModel/Controller implementation #10

Open Kypsis opened 2 years ago

Kypsis commented 2 years ago

As the title says would like to have an official example how to implement a typical view model/controller implementation. Basically Creator version of this documentation: https://riverpod.dev/docs/providers/state_notifier_provider/

This is to facilitate easier transition (by having reference documentation) for people wishing to refactor from other state management solutions (eg bloc).

Aside: I keep automatically trying to access creator via a non existing .value getter as if it was a hook or observable.

liangxianzhe commented 2 years ago

Hi @Kypsis , the way I typically do it is by using a logic file to represent the ViewModel / controller layer. Using Decremental counter as an example, if we change increment to addTodo, then it is exactly what StateNofifierProvider does in your link.

Another example would be the DartPad. If using bloc, I would probably put loading and news inside one NewsState object, then either use freeze or define copyWith to mutate the object. I feel that's a little bit boilerplate without real gain.

Let me know what you think. If you are happy with these, I can make the doc more obvious.

As for .value getter, did you use mobx or getx? To implement that, I guess we need some kind of global state to track which observable is under recreation. You can check out the "Using subscription" section in my article here. I don't feel creator could/should do that, but happy to hear what is in your mind.

Kypsis commented 2 years ago

From my pov the bigger issue is namespaces. I've been to React/RN projects where these atomic state enablers (useState/proxy/create/whatever) usually mean you have singular values bound to singular identifier. And unless you enforce a very strict naming schema it becomes very messy fast, especially with complex interconnected domains. So I guess my question is using an example from Valtio from JS land: https://github.com/pmndrs/valtio/wiki/How-to-organize-actions If we can do the first example and probably second, how can we achieve the 5th result (last one with class, 3rd and 4th are undoable since Dart has no anonymous objects like JS or Kotlin).

And when taking the news example then again from my pov it's better dx if I can do news.loading and news.fetchMore. Or more precisely having the state field and action available from same origin. If I think about it more I feel as there is a way to achieve hook like usability but I can't quite put the lego bricks together in my head.

liangxianzhe commented 2 years ago

If the namespace is a concern, how about just wrapping them into a class and using static variables?

class News {
  static final _page = Creator.value(0);
  static void fetchMore(Ref ref) => ref.update<int>(_page, (n) => n + 1);

  // Loading indicator.
  static final loading = Creator.value(true);

  // News fetches next page when _page changes.
  static final news = Emitter<List<String>>((ref, emit) async {
    ref.set(loading, true);
    final next = await fetchNews(ref.watch(_page));
    final current = ref.readSelf<List<String>>() ?? [];
    emit([...current, ...next]);
    ref.set(loading, false);
  });
}
... News.fetchMore(ref)

If you don't like static variables, then you can make it plain variables and then create an instance:

class News {
  final _page = Creator.value(0);
  void fetchMore(Ref ref) => ref.update<int>(_page, (n) => n + 1);

  // Loading indicator.
  final loading = Creator.value(true);

  // News fetches next page when _page changes.

  late final data = Emitter<List<String>>((ref, emit) async {
    ref.set(loading, true);
    final next = await fetchNews(ref.watch(_page));
    final current = ref.readSelf<List<String>>() ?? [];
    emit([...current, ...next]);
    ref.set(loading, false);
  });
}

final news = News();
... news.fetchMore(ref)

In either approach, it is just how to organize Dart code. There is no magic behind it.

liangxianzhe commented 2 years ago

Though I might miss something about hook like usability. I'm not super familiar with the React hooks.

Kypsis commented 2 years ago

Hah the static field one is brilliantly simple way to address namespace. It very well exemplifies how it is sometimes very hard to get out of the mindset that everything has to be instantiated when doing OOP.

For the second example, how do you optimally watch for say the loading or data value in Watcher. At the moment I have

final news = Creator.value(News());

...
Watcher((context, ref, _) => Text(ref.read(news.map((state) => ref.watch(state.data)[0]))))

which seems kinda clunky way to access the field and have it rebuild the widget. This is the use-case I meant when I said it would be nice to have .value getter eg ref.watch(news.value.data)[0].

liangxianzhe commented 2 years ago

The example above, I use a global variable final news = News();. The reason is that news object doesn't really need to interact with the graph. Testability is also fine, since in the test, you can do something like ref.set(news.loading, false).

If you really want to put that in the graph, use the graph as a service locator.

final newsCreator = Creator.value(News(), keepAlive: true);

...
Watch ( (context, ref, _) {
  // Just get it, no need to watch it. It doesn't change anyway.
  final news = ref.read(newsCreator);

  // Watch the list of news, then render the data somehow, e.g. into a list view.
  final data = ref.watch(news.data);

  return ListView(...);
});
Kypsis commented 2 years ago

Now I see where I went wrong and why I asked hopefully not too obvious questions. I was trying to do:

Widget build(BuildContext context) {
final levelStateCreator =
        Creator((ref) => LevelState(ref, onWin: _playerWon, goal: widget.level.difficulty));

...
Watcher((context, ref, child) {
  return Slider(
    label: 'Level Progress',
    autofocus: true,
    value: ref.watch(ref.read(levelStateCreator).progress) / 100,
    onChanged: (value) => ref.read(levelStateCreator).setProgress((value * 100).round()),
    onChangeEnd: (value) => ref.read(levelStateCreator).evaluate(),
  );
}),
...

and wondering why on earth is the slider not updating. Adding keepAlive fixed it. There is also a subtle "bug" with using the class version and instantiating it that way. On route change the slider resets to 0. That bug is not present when using the non service locator pattern variants.

Overall the issues I've had so far seems to be of the pbkac kind. I see great potential for this library as it is I think the only state management one for Flutter that truly allows for bottom-up state management (as described in this article: https://frontendmastery.com/posts/the-new-wave-of-react-state-management/). I think with some effort one could create a solution using Creator that covers the use-cases that the hugely popular react_query solves in the React land and other ways to effectively create black-box abstractions to cover repetitive use-cases (looking at you ddd with bloc where you can potentially write over 1k lines and touch/create 10s of files just for a simple API call over and over again).

Maybe the only real suggestion to give would be to align the documentation with what Zustand, Recoil, Jotai and Valtio in JS land are doing as it seems Creator can potentially fill the same problem space niche in Flutter.

liangxianzhe commented 2 years ago

As for the example, it seems you put a creator variable inside the build function. If that's the case, be really careful and read the https://github.com/liangxianzhe/creator#creator-equality section.

As for the bottom-up, I'm not so sure how to improve doc since I'm not familiar with all these :) If you have more actionable ideas, let me know. Though from what I know, vuejs and solidjs also work similarly, meaning they also use a fine-grain reactive model.