Как стать автором
Обновить

Комментарии 23

Видимо автор не имел дело с Java раз боится длинных имен классов и методов ). (сарказм если что)

Ну а пока вы ставите минусы, продолжу бесплатно просвещать..

Изменение projectId спровоцирует моментальное изменение filteredUsers

Это самая бестолковая стратегия.

Возможно, я неправильно выразился - изменение state'ов всегда собирается в "кучу", а обновление computed 'ов выполняется в следующем микротаске.

Но я бы отнес это к implementation detail, потому как вызов value на computed спровоцирует моментальный rebuild.

Следующий микротаск - тоже слишком рано, так как в нём могут произойти её какие-то изменения, а значение компутеда всё ещё не понадобиться.

Если значение computed'а не слушается в данный момент, он не ребилдится.

Это вшито в сам state, который computed использует под капотом.

Не увидел там аналога doubt состояния.

Если я не ошибаюсь, doubt состояние не нужно в моем алгоритме. Т.к. проход осуществляется в 2 этапа: от рута к листьям, а потом от листьев к руту. Я наверняка знаю, какие узлы должны быть обновлены

Хм, я понял, о чем вы. Мой "doubt" реализован в виде кэша в Unit Of Work - если computed'а там нет, то он считается состоянием "doubt".

То есть вы каждый раз ходите по всему дереву вместо того пути, где есть изменения. Это крайне ресурсоёмко.

Я хожу по дереву от измененных рутов. Если дохожу до хоть одного конечного обзервера (не-computed), то начинаю идти от него и запрашивать обновления.

Существование у вас "множества рутов" тоже сомнительная практика, так как приводит к недетерминизму поведения со всеми вытекающими. В идеале, у приложении должен быть ровно один рут в точке входа, который гарантирует стабильную очерёдность всех вычислений и своевременное уничтожение атомов до их возможного избыточного пересчёта.

Кроме того, на сколько я понял, компутеды сами по себе не в курсе, есть ли у них косвенные изменённые зависимости, то есть могут выдавать устаревшее значение, что приводит как минимум к лишним перевычисляениям, а как максимум к побочным эффектам с некорректным поведением.

Можете написать тесты и попробовать сломать алгоритм. Буду рад :D

Думаю, так будет гораздо продуктивнее. Можете писать мне в ЛС

А это кому больше надо: мне или вам? Я не трогал Дарт уже 10 лет, и как-то не планирую к нему больше прикасаться.

Но флаттера меньше 10 лет...

Нам повезло что VM Dart писали те же люди что и VM V8 😄

Непосредственное нахардкоживание late final во вьюмоделях может означать только одно - что нам делать, когда придёт время тестов. И честно, не увидел в readme пакета и не услышал в статье ни одного слова о тестировании: как и возможно ли?

Далее, примеры счётчиков настолько заезженные и банальные, что не отражают ровным счётом ничего и плохо пахнут. В противовес вашему примеру, пример на ValueNotifier(соблюдая именования и стиль):

import 'package:flutter/material.dart';

class CounterViewModel extends ValueNotifier<int> {
  CounterViewModel() : super(0);

  void increment() => value++;
}

// Внутри StatefulWidget
final vm = CounterViewModel();

// ...

@override
Widget build(BuildContext context) {
  return ValueListenableBuilder<int>(
    valueListenable: vm,
    builder: (context, count, _) {
      return ElevatedButton(
        onPressed: vm.increment,
        child: Text("$count"),
      );
    },
  );
}

// ...

Организация, к примеру, полноценной поисковой строки, с фильтрами и различными состояниями, и парой необычных возможностей была бы куда интересней и продуктивней.

Пункт "Почему не использовать уже существующее решение?" откровенно слаб и очень хочется его реального раскрытия. И вот почему:

Riverpod:

  • Не нравится подход со смешиванием DI и State Management'а.

Однако заметьте, что в реальном приложении придётся использовать и то и другое (под каким бы соусом не был подан DI). В данном случае, мы бы воспользовались vessel + beholder. А есть ли смысл импортировать два пакета вместо одного?

  • Засорение глобального скоупа

Тем, что мы имеем один ProviderScope, в котором содержится один ProviderContainer, который и содержит состояния наших провайдеров? Ну я вам скажу, что ещё можно поиграться с UncontrolledProviderScope и контейнеры создавать независимо. А ещё использовать ProviderScope.overrides и ProviderScope.parent для переопределения для конкретной ветки.

Тяжело масштабировать

Пожалуй это самое нелепое обвинение в сторону Riverpod. Начну с того, что StateNotifier уже устаревшая концепция. Используйте (Async)NotifierProvider. И комбинируйте состояния ровно также, как вы это делаете в случае с вашей библиотекой (ваш последний пример не ясен, возможно он содержит ошибку в именовании SearchUsersViewModel|UsersViewModel):

final selectedProjectId = StateProvider((_) => 32);

final users = Provider((_) => <User>[]);

final filteredUsers = Provider((ref) {
  final projectId = ref.watch(selectedProjectId);
  return ref
      .watch(users)
      .where((user) => user.projects.contains(projectId))
      .toList();
});

Это классический стиль. При необходимости дополнительного namespase перенесите провайдеров в статические поля ваших ViewModel, либо используйте Notifier, если планируется управление над получившимся состоянием:

class FilteredUsersNotifier extends Notifier<List<User>> {
  late List<User> _users;
  late int _projectId;

  @override
  List<User> build() {
    _users = ref.watch(users);
    _projectId = ref.watch(selectedProjectId);
    return _users.where((user) => user.projects.contains(_projectId)).toList();
  }

  User findById(id) {/*делайте что-то*/}
}

Опять же, такие примеры выглядят глупо из-за отсутствия реальной задачи.

Bloc:

  • Определение более-менее сложных состояний требует кодогенерации copyWith.

copyWith используется, когда модели являются иммутабельными и стейт-менеджер основан на сравнении hashcode для обновления состояния. Как в этом плане работает beholder? Если он основан на мутабельном состоянии, то как избежать лишних перестроек, когда данные на самом деле не изменились, но их присвоение произошло?

  • Субъективно, но в больших проектах именование Event'ов и State'ов начинает напоминать энтерпрайз Java: class RefreshPostsHomeScreenEvent

А как мы это избегаем здесь? ModelView превращаются в повелители всего и вся с сотнями методов и сотнями состояний?

AsyncState, AsyncValue, Result.guard - это всё мне что-то очень сильно напоминает на подход в R..?, ну ладно, окей.

---

Подводя черту, ваш стейт-менеджер может намного больше, под капотом там всё действительно интересно. Хабр хочет внутренностей и живых примеров приложений, основанных на данном пакете. Быть может, стоит показать конкретный пример, на котором сильно забуксуют имеющиеся менеджеры, а ваш решит проблему с лёгкостью. Пишите, пожалуйста, ещё. Независимо от всего, вы молодец, проделали большую работу, а полученный опыт может послужить хорошим фундаментом для будущих улучшений и новых пакетов. 🙏

Спасибо за такой развернутый комментарий! Согласен со всем, что написали, но хотел бы прояснить пару моментов:

Непосредственное нахардкоживание late final во вьюмоделях может означать только одно - что нам делать, когда придёт время тестов. И честно, не увидел в readme пакета и не услышал в статье ни одного слова о тестировании: как и возможно ли?

Пока что не реализовывал библиотеку, предназначенную для тестов, но наличие late final не должно ничему помешать. Я обязательно обновлю readme, когда решу, каким способом будет наиболее удобно тестировать.

Далее, примеры счётчиков настолько заезженные и банальные, что не отражают ровным счётом ничего и плохо пахнут. В противовес вашему примеру, пример на ValueNotifier(соблюдая именования и стиль):

Да, понимаю, пример неудачный, но хотел предоставить одновременно информативный и не занудный для начала пример. Более подробный (и, на мой взгляд, интересный пример) - форма регистрации + исходный код beholder_form. Действительно показывает, насколько лаконичные и мощные получаются решения.

Пожалуй это самое нелепое обвинение в сторону Riverpod. Начну с того, что StateNotifier уже устаревшая концепция. Используйте (Async)NotifierProvider. И комбинируйте состояния ровно также, как вы это делаете в случае с вашей библиотекой (ваш последний пример не ясен, возможно он содержит ошибку в именовании SearchUsersViewModel|UsersViewModel):

Ошибку поправил, а про NotifierProvider не знал, работал с riverpod'ом еще 1-ой версии. Виноват, что не проверил :)

Почему у меня не вышло с riverpod:
Началось все с формы из 3 полей. Потом проект разросся, и форм стало много. Чтобы не получать по 4 autocomplete'а на firstNameFieldProvider, я начал их класть в static классы - стало неудобно, часть провайдеров лежала в глобальном неймспейсе, часть - в классах. После этого я решил переиспользовать логику форм, но провайдеры на то и статические - много инстансов не создашь. Пришлось абсолютно все сносить и переписывать на StateNotifier (но, насколько помню, решение все равно получилось некрасивым - либо вследствие отсутствия опыта, либо из-за неуклюжести riverpod'а).

copyWith используется, когда модели являются иммутабельными и стейт-менеджер основан на сравнении hashcode для обновления состояния. Как в этом плане работает beholder? Если он основан на мутабельном состоянии, то как избежать лишних перестроек, когда данные на самом деле не изменились, но их присвоение произошло?

Каждый observable принимает equals; по умолчанию - это сравнение (== ). Значения observable на самом деле иммутабельные - разработчик переприсваивает value отдельных observable также, как BLoC переприсваивает state. Вот и получается, что в BLoC тебе нужно определять стейт целиком, а в beholder - по кусочкам - без нужды в copyWith.

AsyncStateAsyncValueResult.guard - это всё мне что-то очень сильно напоминает на подход в R..?, ну ладно, окей.

С AsyncState была опечатка, должен быть AsyncValue.
Действительно, мне очень понравилось то, как был сделан этот union в riverpod - очень емкое и универсальное средство для описания асинхронных состояний.

Возможно, напишу статью по внутреннему алгоритму или какой-нибудь туториал с боевым use-case'ом. Очень ценные у Вас советы, еще раз - спасибо!

flutter_solidart имеет схожий API.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории