Pull to refresh

Comments 10

А по какой причине было решено наплодить несколько combineValueN вместо одного, который принимает список value и этот же список посылает в обработчик?

А так в целом хорошая статья, спасибо, думаю смело можно идти в пабдев

Методы combineN() хороши тем, что обработчики получаются строго типизированными.

На случай, когда нужно просто реагировать на список ReadonlyValue, заготовлен метод combine(), который как раз делает именно то что вы описали. В моей практике такие юзкейсы очень редки, по пальцам одной руки пересчитать.

Спасибо за статью.

Вы переизобрели MobX) Читая статью думал, будет ли computed, но не увидел (жаль). На самом деле с пользовательской точки зрения это намного удобнее (хотя свои минусы тоже есть). Вместо кучи combineN и RebuilderN был бы один ComputedValue и Rebuilder. Потому, что постоянно указывать и менять цифры, прописывать для всего типы и все это вручную, ну такое... "Продать" относительно Stream и rxdart конечно получилось, но сравнивать, на мой взгляд, нужно не с ними, а с MobX и его аналогами, и возможно даже с Riverpod.

  1. StreamController, через который предполагается посылать обновления в Stream, требует вызова dispose(), а также имеет переусложнённый синтаксис: Stream и StreamController это два разных объекта.

Строго говоря у StreamController -а нет метода dispose.

В подписчик передаётся значение на момент обновления Value через сеттер, а не на момент вызова подписчика, в ряде случаев это важно.

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

такое поведение можно отключить, задав параметр distinctMode = false:

Наверное лучше было бы использовать предикат? В таком случае возможности были бы шире.

Пример с combine2 не компилируется. Зачем делать в CombinedValueSubscription метод отмены с типом Future, если ассинхронность не используется?

При этом теряется возможность обработки ошибки в Stream (в Value эта концепция отсутствует), но можно через параметр errorBuilder задать преобразование ошибки в значение Value (например в null, если underlying тип nullable).

Если использовать тип Either, то можно не терять возможность обработки ошибок.

final startSw = Stopwatch()..start();

Создавать новый экземпляр Stopwatch на каждый вызов notify несколько накладно.

scheduleMicrotask(() async { // unawaited

На использование микротасков должны быть какие-то веские причины, у вас много работы выполняется, т.е. это явно не должен быть микротаск, иначе привет подвисания, особенно на слабых устройствах. Настолько много, что вы даже замеряете время выполнения. Это не то что звоночек, это колокол!

//! There is a potential problem in multiple entering paused state

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

@override R get value => transformer(origin.value);

Очевидно, что transformer может быть накладен для вычисления, и лучше кэшировать значение.

Примечателен тот факт, что Value сам по себе не требует вызова dispose(), он штатно утилизируется сборщиком мусора.

Вы в нескольких местах что-то подобное пишете, но по факту это не совсем правильно. По-хорошему ваш Value так же должен предоставлять метод dispose или его аналог, т.к. во-первых, чтобы можно было отписаться от все подписок, с текущим апи это сделать нельзя. Во вторых наследники этого класса используют, например Timer, который тоже никак не остановить, насколько я понял и он будет тикать, хотя уже может быть давно не нужен. Просто если тот же StreamController использовать по аналогии с вашими классами, то мы так же не обязаны вызывать dispose и будет плюс-минус такое же поведение, что и вас.

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

Вы переизобрели MobX

Да нет, MobX тут и не пахнет: судя про примерам, в библиотеке нет ни аналога computed, ни динамического определения зависимостей. Не говоря уже об избегании избыточных пересчётов.

Там много чего нет из того, что можно найти где-нибудь ещё.

Я много труда вложил в то, чтобы сохранить библиотеку простой: и по структуре, и в использовании. Там только то, что нам реально нужно в нашем проекте, и ничего лишнего.

Спасибо за конструктивный фидбек!

На самом деле с пользовательской точки зрения это намного удобнее (хотя свои минусы тоже есть). Вместо кучи combineN и RebuilderN был бы один ComputedValue и Rebuilder. Потому, что постоянно указывать и менять цифры, прописывать для всего типы и все это вручную, ну такое...

Цифры менять вручную действительно надо, но это быстро и просто. Как уже написал выше, семейства combineN, RebuilderN, ComputedValueN нужны для строгой типизации. Преимущества от её использования явно перевешивают небольшое неудобство от того, что иногда придётся подправить пару цифр.

 Вы переизобрели MobX)

"Продать" относительно Stream и rxdart конечно получилось, но сравнивать, на мой взгляд, нужно не с ними, а с MobX и его аналогами, и возможно даже с Riverpod.

Может быть, в каком-то смысле. Во всех инструментах работы с реактивностью можно найти похожие паттерны.

Мне совершенно не нравится как в MobX предлагается встраивать поддержку реактивности в свой класс. А также что там используется кодогенерация.

Сравнивать можно. Но я бы не говорил о переизобретении MobX. Некоторые ключевые идеи (как минимум ориентация на иммутабельность underlying типа и независимость underlying типа от самой концепции Value) принципиально другие.

Строго говоря у StreamController -а нет метода dispose

Да, вы правы, :) поправлю.

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

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

Наверное лучше было бы использовать предикат? В таком случае возможности были бы шире.

Не лучше. Операция сравнения определяется underlying типом, это его зона ответственности. Более широкие возможности тут не нужны, потому что усложняют логику работы кода. В случае, когда понадобился предикат, возможно стоит использовать трансформацию.

Пример с combine2 не компилируется. Зачем делать в CombinedValueSubscription метод отмены с типом Future, если ассинхронность не используется?

Это хвосты от предыдущей версии, где было гораздо больше асинхронности. Поправлю, спасибо.

Создавать новый экземпляр Stopwatch на каждый вызов notify несколько накладно.

Замер скорости старта остался от предыдущих версий. Сейчас он по факту более не нужен. Спасибо.

На использование микротасков должны быть какие-то веские причины, у вас много работы выполняется, т.е. это явно не должен быть микротаск, иначе привет подвисания, особенно на слабых устройствах. Настолько много, что вы даже замеряете время выполнения. Это не то что звоночек, это колокол!

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

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

Очевидно, что transformer может быть накладен для вычисления, и лучше кэшировать значение.

Не совсем так. Кешировать значение где? Непосредственно в экземпляре TransformedValue? А как тогда этот кэш инвалидировать? Т.е. в TransformedValue нужна подписка на оригинальный Value, даже если у TransformedValue нет своих подписчиков. Эту подписку надо потом как-то отменять.

Я пробовал сделать такой кэш, ничего хорошего из этого не получилось.

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

Вы в нескольких местах что-то подобное пишете, но по факту это не совсем правильно. По-хорошему ваш Value так же должен предоставлять метод dispose или его аналог, т.к. во-первых, чтобы можно было отписаться от все подписок, с текущим апи это сделать нельзя.

Так сделано специально. Отписка - это зона ответственности подписчиков.

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

В TimeoutValue таймер срабатывает единожды, ничего страшного в этом нет. В ThrottleValue и DebounceValue таймер привязан к экземпляру подписки и отменяется вместе с ней. В PeriodicValue таймер общий для всех подписок текущего экземпляра PeriodicValue, но он отменяется, когда все подписки отменены. Других бесконечно тикающих таймеров вроде нет.

Мне совершенно не нравится как в MobX предлагается встраивать поддержку реактивности в свой класс. А также что там используется кодогенерация.

А вы смотрите не на эту часть MobX, а на core api. Которое состоит из функций observable, computed и autorun/reaction

К слову, та же Observable - полный аналог вашей Value

Спасибо, что-то новенькое! Изучу на досуге)

В текущем проекте использую в основном только ValueNotifier и ValueListenableBuilder и эта библиотека выглядит как то же самое, но лучше. Та же простота и ничего лишнего. Самое заметное отличие – наличие варианта только для чтения. Сейчас больше всего напрягает при использовании ValueListenableBuilder то, что я вынужден передавать полноценный ValueNotifier в код интерфейса, который отвечает только за отрисовку изменений, но ничего не меняет сам.

Sign up to leave a comment.

Articles