feat(app): offline-кэш на остальных экранах — фаза 5

accounts, cashflow, categories, goals, income, portfolio (+instrument),
rebalance, tax, rules переведены на Cached<T> по контракту
docs/ai/offline-cache.md. Общие/второстепенные селекторы (scopesProvider,
categoriesListProvider и т.п.) оставлены как есть — не основной контент
экрана. sync_page.dart и settings_page.dart не тронуты (первый — заменён
health-page отдельно, второй — чистые действия без списка для баннера).
This commit is contained in:
Dmitry
2026-09-19 14:00:13 +03:00
parent ffc5ed959a
commit 7b419f4188
39 changed files with 388 additions and 186 deletions
@@ -3,11 +3,13 @@ import 'package:fintracker_api/fintracker_api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/cache/cached.dart';
import '../../core/theme/chart_colors.dart';
import '../../core/utils/ru_date.dart';
import '../../core/widgets/async_value_view.dart';
import '../../core/widgets/empty_state.dart';
import '../../core/widgets/money_text.dart';
import '../../core/widgets/stale_banner.dart';
import 'providers.dart';
class _Group {
@@ -79,6 +81,7 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
Widget build(BuildContext context) {
final month = ref.watch(selectedSpendingMonthProvider);
final spending = ref.watch(spendingProvider(month));
final stale = oldestFetch([spending.valueOrNull?.fetchedAt]);
return Scaffold(
appBar: AppBar(title: const Text('Категории')),
@@ -108,10 +111,12 @@ class _CategoriesPageState extends ConsumerState<CategoriesPage> {
],
),
const SizedBox(height: 8),
if (stale != null) StaleBanner(fetchedAt: stale),
AsyncValueView(
value: spending,
onRetry: () => ref.invalidate(spendingProvider(month)),
data: (rows) {
data: (cached) {
final rows = cached.data;
if (rows.isEmpty) {
return const EmptyState(
icon: Icons.donut_small_outlined,
+7 -4
View File
@@ -2,9 +2,11 @@ import 'package:fintracker_api/fintracker_api.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api/api_client.dart';
import '../../core/cache/cached.dart';
import '../../core/utils/ru_date.dart';
/// Flat ZenMoney tag tree — the client nests it by `parent_id` where needed.
/// Flat ZenMoney tag tree — the client nests it by `parent_id` where needed. Shared with
/// Транзакции; not wrapped in `Cached` since it is a lookup, not a screen's own primary read.
final categoriesListProvider = FutureProvider.autoDispose<List<CategoryOut>>((ref) async {
final r = await ref.watch(apiProvider).getCategoriesApi().categoriesList();
return r.data ?? const [];
@@ -16,11 +18,12 @@ final categoryNamesProvider = Provider.autoDispose<Map<int, String>>((ref) {
return {for (final c in categories) c.id: c.name};
});
/// Spending by category for one month (`YYYY-MM`); null means "the latest month".
/// Spending by category for one month (`YYYY-MM`); null means "the latest month". See
/// `docs/ai/offline-cache.md`.
final spendingProvider =
FutureProvider.autoDispose.family<List<SpendingRow>, String?>((ref, month) async {
FutureProvider.autoDispose.family<Cached<List<SpendingRow>>, String?>((ref, month) async {
final r = await ref.watch(apiProvider).getCashflowApi().cashflowSpending(month: month);
return r.data ?? const [];
return Cached(r.data ?? const [], fetchedAt: r.extra['fetchedAt'] as DateTime?);
});
/// The month currently selected on the Категории screen, `YYYY-MM`.