Комментарии 23
Видимо автор не имел дело с Java раз боится длинных имен классов и методов ). (сарказм если что)
Тогда я и решил написать "свой"
mobx
...
А почему не $mol_wire, который по всем аспектам минимум в 2 раза лучше MobX и умеет в прозрачные для потребителя асинхронные реактивные инварианты?
Ну а пока вы ставите минусы, продолжу бесплатно просвещать..
Изменение projectId спровоцирует моментальное изменение filteredUsers
Возможно, я неправильно выразился - изменение state'ов всегда собирается в "кучу", а обновление computed
'ов выполняется в следующем микротаске.
Но я бы отнес это к implementation detail, потому как вызов value
на computed
спровоцирует моментальный rebuild.
Следующий микротаск - тоже слишком рано, так как в нём могут произойти её какие-то изменения, а значение компутеда всё ещё не понадобиться.
Если значение computed'а не слушается в данный момент, он не ребилдится.
Оно может в данный момент слушаться, но в следующем микротаске уже перестать.
Посмотрите реализацию алгоритма - она укладывается в одном файле из 200 строк. Если знаете, как улучшить, буду рад PR'у.
Не увидел там отсечения обновлений, когда новое значение компутеда эквивалентно предыдущему.
Это вшито в сам state, который computed использует под капотом.
Не увидел там аналога doubt состояния.
Если я не ошибаюсь, doubt состояние не нужно в моем алгоритме. Т.к. проход осуществляется в 2 этапа: от рута к листьям, а потом от листьев к руту. Я наверняка знаю, какие узлы должны быть обновлены
Хм, я понял, о чем вы. Мой "doubt" реализован в виде кэша в Unit Of Work - если computed'а там нет, то он считается состоянием "doubt".
То есть вы каждый раз ходите по всему дереву вместо того пути, где есть изменения. Это крайне ресурсоёмко.
Я хожу по дереву от измененных рутов. Если дохожу до хоть одного конечного обзервера (не-computed), то начинаю идти от него и запрашивать обновления.
Существование у вас "множества рутов" тоже сомнительная практика, так как приводит к недетерминизму поведения со всеми вытекающими. В идеале, у приложении должен быть ровно один рут в точке входа, который гарантирует стабильную очерёдность всех вычислений и своевременное уничтожение атомов до их возможного избыточного пересчёта.
Кроме того, на сколько я понял, компутеды сами по себе не в курсе, есть ли у них косвенные изменённые зависимости, то есть могут выдавать устаревшее значение, что приводит как минимум к лишним перевычисляениям, а как максимум к побочным эффектам с некорректным поведением.
Непосредственное нахардкоживание 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.
AsyncState
,AsyncValue
,Result.guard
- это всё мне что-то очень сильно напоминает на подход в R..?, ну ладно, окей.
С AsyncState была опечатка, должен быть AsyncValue.
Действительно, мне очень понравилось то, как был сделан этот union в riverpod - очень емкое и универсальное средство для описания асинхронных состояний.
Возможно, напишу статью по внутреннему алгоритму или какой-нибудь туториал с боевым use-case'ом. Очень ценные у Вас советы, еще раз - спасибо!
flutter_solidart имеет схожий API.
Простой, но масштабируемый State Management для Flutter