PackRuble / cardoteka

The best type-safe wrapper over SharedPreferences. ⭐ Why so? -> strongly typed cards for access to storage -> don't think about type, use get|set -> can work with nullable values -> callback based updates
https://t.me/+AkGV73kZi_Q1YTMy
Apache License 2.0
3 stars 0 forks source link

Implementation of `attachAll` in `Watcher` #18

Open PackRuble opened 6 months ago

PackRuble commented 6 months ago

Implementing attachAll will make it easier to compose large models from different cards. Let's say there is such a model:

@immutable
class QuizConfig {
  const QuizConfig({
    required this.quizCategory,
    ...,
  });

  final CategoryDTO quizCategory;
  final TriviaQuizDifficulty quizDifficulty;
  final TriviaQuizType quizType;

  QuizConfig copyWith({
    CategoryDTO? quizCategory,
    TriviaQuizDifficulty? quizDifficulty,
    TriviaQuizType? quizType,
  }) {...}
}

and this type of attachment:

return QuizConfig(
  quizCategory: _storage.attach(
    GameCard.quizCategory,
    (value) => state = state.copyWith(quizCategory: value),
    detacher: ref.onDispose,
  ),
  quizDifficulty: _storage.attach(
    GameCard.quizDifficulty,
    (value) => state = state.copyWith(quizDifficulty: value),
    detacher: ref.onDispose,
  ),
  quizType: _storage.attach(
    GameCard.quizType,
    (value) => state = state.copyWith(quizType: value),
    detacher: ref.onDispose,
  ),
);

Not that it looks bad at all, but it seems like there are options for optimizations. Need elaboration.

And a more complex case, based on a non-immutable model with late fields:

class StatsModel {
  late Map<TriviaQuizDifficulty, StatsAmount> byDifficulty;
  late Map<CategoryName, StatsAmount> byCategory;
  late List<Quiz> quizzesPlayed;
  late int winning;
  late int losing;
}

class QuizStatsNotifier extends AutoDisposeNotifier<StatsModel> {
  late GameStorage _storage;

  @override
  StatsModel build() {
    _storage = ref.watch(StorageNotifiers.game);

    final stats = StatsModel();

    return stats
      ..quizzesPlayed = _storage.attach(
        GameCard.quizzesPlayed,
        (quizzes) {
          final (byDifficulty, byCategory) = _calcAll(quizzes);
          stats
            ..quizzesPlayed = quizzes
            ..byDifficulty = byDifficulty
            ..byCategory = byCategory;
          ref.notifyListeners();
        },
        detacher: ref.onDispose,
        onRemove: () {
          stats
            ..quizzesPlayed = []
            ..byDifficulty = {}
            ..byCategory = {};
          ref.notifyListeners();
        },
        fireImmediately: true,
      )
      ..winning = _storage.attach(
        GameCard.winning,
        (value) {
          state.winning = value;
          ref.notifyListeners();
        },
        detacher: ref.onDispose,
        onRemove: () {
          state.winning = 0;
          ref.notifyListeners();
        },
      )
      ..losing = _storage.attach(
        GameCard.losing,
        (value) {
          state.losing = value;
          ref.notifyListeners();
        },
        detacher: ref.onDispose,
        onRemove: () {
          state.losing = 0;
          ref.notifyListeners();
        },
      );
  }

There are probably options here to simplify and improve the user experience. We could throw something like a list of cards (?) and get a list from the lists.., on the other hand even if we make some kind of implementation with a single detacher method, we are already approaching a better result.