(пробую новый тип контент - разбор конкретных кейсов из сегодняшнего дня, пишите в комментариях как вам)
Лента постов слала на сервер под 180 запросов в секунду при скролле. В логах — 3115 ошибок дубликата уникального ключа в сутки, до 89 отказов на одну пару (пользователь, пост).
Защита от повторов в коде была. Выглядела безупречно:
@Riverpod() // autoDispose по умолчанию
class ContentViewState extends _$ContentViewState {
late final _pendingViews = <int>{};
void viewPost(int postId) {
if (_pendingViews.contains(postId)) return; // не шлём дважды
_pendingViews.add(postId);
// ...saveModel
}
}
Вызывали её так — и только так, во всех трёх местах:
ref.read(contentViewStateProvider.notifier).viewPost(postId);
Вот здесь всё и ломается. Провайдер никто не
watchил.@Riverpod() — это autoDispose. Такой провайдер живёт ровно столько, сколько у него есть слушатели. А ref.read слушателя не создаёт: провайдер поднимается, отдаёт нотифаер и уничтожается в том же цикле событий.Следующий кадр — новый инстанс нотифаера.
_pendingViews это поле инстанса, у нового оно пустое. Листенер скролла дёргается на каждом кадре по каждому видимому посту, то есть гвард обнулялся шестьдесят раз в секунду и не отсекал ничего.Код при этом читается как рабочий. На ревью такое не видно: и гвард корректный, и вызов корректный — неверна только связка.
Состояние, которое должно пережить виджет, обязано лежать в провайдере, который переживает виджет. ref.read(provider.notifier) без единого watch — это не «взять существующий», а «создать, использовать и выбросить».
Лечится переносом состояния туда, где оно живёт всю сессию:
@Riverpod(keepAlive: true)
PostViewTracker postViewTracker(Ref ref) => PostViewTracker(...);
Как проверить себя за минуту: если храните состояние в поле нотифаера — найдите хоть один
watch этого провайдера. Не нашли ни одного — значит состояния у вас нет.ПС. я бы сказал, что это интересный пример, почему не стоит злоупотреблять кодогенерацией - riverpod по умолчанию фигачит всё autoDispose, лучше осознанно это писать, мне кажется