-
Notifications
You must be signed in to change notification settings - Fork 18
State Management
We use flutter_riverpod for state management. However, we do not simply use standard providers; we have built a custom architecture to ensure the app works offline-first.
The backbone of our data layer is the CachedAsyncNotifier<T> abstract class. This is a custom extension of Riverpod's AsyncNotifier designed to handle the synchronization between local storage and remote APIs (Sigarra).
When a provider is initialized, CachedAsyncNotifier follows this lifecycle:
-
Load from Storage: It immediately attempts to fetch data from the local device (
loadFromStorage()). - Display Local Data: If local data exists, it is displayed immediately to the user.
-
Check Validity: It checks if the cache is expired using
cacheDurationand_lastUpdateTime. -
Fetch Remote: If the data is missing, empty, or the cache is expired, it fetches fresh data from the API (
loadFromRemote()). - Update & Persist: New remote data updates the state and should be saved to local storage within the implementation.
-
Automatic Caching: Handles timestamp checks automatically via
PreferencesController. - Error Handling: If the network call fails, it falls back to the local data (if available) without crashing the UI.
-
Refresh Logic: Includes a
refreshRemote()method to force a network update (used in pull-to-refresh).
To create a new provider that fetches and stores data (e.g., Exams, Schedule, Profile), follow this pattern:
-
Inherit from
CachedAsyncNotifier<T>: Define the type of data this provider manages. -
Implement Required Methods:
-
cacheDuration: How long the data remains valid before a refetch is required (returnnullfor manual management). -
loadFromStorage(): Logic to retrieve data from the local database. -
loadFromRemote(): Logic to fetch data from the web.
-
-
Define the Provider: Use
AsyncNotifierProvider.
Example (session_provider.dart):
final sessionProvider = AsyncNotifierProvider<SessionNotifier, Session?>(
SessionNotifier.new,
);
class SessionNotifier extends CachedAsyncNotifier<Session?> {
@override
Duration? get cacheDuration => null; // No auto-expire, manual management
@override
Future<Session?> loadFromStorage() async {
// Logic to read from SharedPreferences
return await PreferencesController.getSavedSession();
}
@override
Future<Session?> loadFromRemote() async {
// Logic to fetch from API
// Note: You must handle saving to storage here usually
return newSession;
}
}For providers managing complex states (like tuples or maps), you can define specific methods to update partial state, as seen in CourseUnitsInfoNotifier:
// Example: Updating only one part of the state
final updatedSheetsMap = Map<CourseUnit, Sheet>.from(currentState.$1);
updatedSheetsMap[courseUnit] = sheet;
updateState((updatedSheetsMap, currentState.$2, currentState.$3));To reduce boilerplate when handling AsyncValue (loading, error, and data states), we use a custom widget called DefaultConsumer.
Located in lib/model/providers/riverpod/default_consumer.dart.
Instead of writing .when(data: ..., loading: ..., error: ...) in every widget, use DefaultConsumer.
Properties:
-
provider: The provider to watch. -
builder: The widget to build when data is successfully loaded. -
nullContentWidget: What to show if the data is null or empty. -
hasContent: A boolean function to check if the data list is empty. -
mapper: (Optional) A function to filter or transform data before the builder sees it.
Example Usage (home.dart):
DefaultConsumer<List<Lecture>>(
provider: lectureProvider,
// 1. Filter data before building (Optional)
mapper: (lectures) => lectures
.where((lecture) => lecture.endTime.isAfter(DateTime.now()))
.toList(),
// 2. Check if there is data to show
hasContent: (lectures) => lectures.isNotEmpty,
// 3. Fallback if empty
nullContentWidget: const SizedBox.shrink(),
// 4. Build the UI
builder: (context, ref, lectures) {
return ScheduleCard(lecture: lectures[0]);
},
)For synchronous configuration state (like Theme Mode) that doesn't require complex fetching or caching logic, we use the simpler StateNotifier.
Example (theme_provider.dart):
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>(
(ref) => ThemeNotifier(PreferencesController.getThemeMode()),
);uni by NIAEFEUP
Need help? Open an Issue | Read Contributing Guidelines
Licensed under the GPL-3.0 License.