feat: auto-refresh data on app resume and tab switch
- App resume: invalidates all main providers (conversations, calendar, news, knowledge, briefing) when the app returns to foreground. Throttled to once per 5 minutes to avoid hammering the server. - Tab switch: refreshes the incoming tab's provider when navigating between Briefing / Knowledge / Conversations shell tabs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+62
-1
@@ -11,6 +11,11 @@ import 'providers/api_client_provider.dart';
|
|||||||
import 'providers/auth_provider.dart';
|
import 'providers/auth_provider.dart';
|
||||||
import 'providers/capture_queue_provider.dart';
|
import 'providers/capture_queue_provider.dart';
|
||||||
import 'providers/capture_work_queue_provider.dart';
|
import 'providers/capture_work_queue_provider.dart';
|
||||||
|
import 'providers/briefing_provider.dart';
|
||||||
|
import 'providers/calendar_provider.dart';
|
||||||
|
import 'providers/chat_provider.dart';
|
||||||
|
import 'providers/knowledge_provider.dart';
|
||||||
|
import 'providers/news_provider.dart';
|
||||||
import 'providers/notes_provider.dart';
|
import 'providers/notes_provider.dart';
|
||||||
import 'providers/settings_provider.dart';
|
import 'providers/settings_provider.dart';
|
||||||
import 'providers/update_provider.dart';
|
import 'providers/update_provider.dart';
|
||||||
@@ -182,16 +187,22 @@ class _Shell extends ConsumerStatefulWidget {
|
|||||||
ConsumerState<_Shell> createState() => _ShellState();
|
ConsumerState<_Shell> createState() => _ShellState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShellState extends ConsumerState<_Shell> {
|
class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver {
|
||||||
static const _tabs = [
|
static const _tabs = [
|
||||||
Routes.briefing,
|
Routes.briefing,
|
||||||
Routes.knowledge,
|
Routes.knowledge,
|
||||||
Routes.conversations,
|
Routes.conversations,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Minimum gap between app-resume refreshes to avoid hammering the server.
|
||||||
|
static const _resumeCooldown = Duration(minutes: 5);
|
||||||
|
DateTime? _lastResumeRefresh;
|
||||||
|
int? _prevTabIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Silent update check — only if we haven't already checked this session.
|
// Silent update check — only if we haven't already checked this session.
|
||||||
final repoUrl = ref.read(forgejoRepoUrlProvider);
|
final repoUrl = ref.read(forgejoRepoUrlProvider);
|
||||||
@@ -206,6 +217,49 @@ class _ShellState extends ConsumerState<_Shell> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state != AppLifecycleState.resumed) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (_lastResumeRefresh != null &&
|
||||||
|
now.difference(_lastResumeRefresh!) < _resumeCooldown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastResumeRefresh = now;
|
||||||
|
_refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh every major data provider. Safe to call speculatively —
|
||||||
|
/// providers that aren't currently watched are already disposed.
|
||||||
|
void _refreshAll() {
|
||||||
|
ref.invalidate(conversationsProvider);
|
||||||
|
ref.invalidate(calendarProvider);
|
||||||
|
ref.invalidate(newsProvider);
|
||||||
|
// Notifier (not AsyncNotifier) — needs explicit refresh call.
|
||||||
|
ref.read(knowledgeProvider.notifier).refresh();
|
||||||
|
// briefingProvider is an AsyncNotifier family; invalidating the family
|
||||||
|
// is safe even if no conversation is open.
|
||||||
|
ref.invalidate(briefingProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh only the provider backing the given shell tab index.
|
||||||
|
void _refreshTab(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
ref.invalidate(briefingProvider);
|
||||||
|
case 1:
|
||||||
|
ref.read(knowledgeProvider.notifier).refresh();
|
||||||
|
case 2:
|
||||||
|
ref.invalidate(conversationsProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _syncTimezone() async {
|
Future<void> _syncTimezone() async {
|
||||||
try {
|
try {
|
||||||
final tzInfo = await FlutterTimezone.getLocalTimezone();
|
final tzInfo = await FlutterTimezone.getLocalTimezone();
|
||||||
@@ -340,6 +394,13 @@ class _ShellState extends ConsumerState<_Shell> {
|
|||||||
});
|
});
|
||||||
final location = GoRouterState.of(context).matchedLocation;
|
final location = GoRouterState.of(context).matchedLocation;
|
||||||
final index = _tabIndex(location);
|
final index = _tabIndex(location);
|
||||||
|
|
||||||
|
// Refresh the incoming tab's data when switching between shell tabs.
|
||||||
|
if (_prevTabIndex != null && _prevTabIndex != index && index < 3) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _refreshTab(index));
|
||||||
|
}
|
||||||
|
_prevTabIndex = index;
|
||||||
|
|
||||||
final child = widget.child;
|
final child = widget.child;
|
||||||
final isWide = MediaQuery.of(context).size.width >= 600;
|
final isWide = MediaQuery.of(context).size.width >= 600;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user