With Provider
Provider and ChangeNotifier are effective for UI state and dependency injection. Server data management introduces repetitive boilerplate in each notifier: isLoading, error, manual cache guards, and inconsistent retry handling.
Qora takes over that specific responsibility. Provider keeps everything else.
The Problem with ChangeNotifier for Server Data
A typical data-fetching notifier:
class UserNotifier extends ChangeNotifier {
User? data;
bool isLoading = false;
Object? error;
int? _loadedId;
Future<void> fetchUser(int id) async {
if (_loadedId == id && data != null) return; // manual cache, no TTL, no SWR
isLoading = true;
error = null;
notifyListeners();
try {
data = await UserApi.getUser(id);
_loadedId = id;
} catch (e) {
error = e; // no retry, no backoff
} finally {
isLoading = false;
notifyListeners();
}
}
}
Consumer<UserNotifier>(
builder: (context, notifier, _) {
if (notifier.isLoading) return const CircularProgressIndicator();
if (notifier.error != null) return ErrorWidget('${notifier.error}');
if (notifier.data == null) {
notifier.fetchUser(userId); // side effect in build, fragile lifecycle
return const SizedBox.shrink();
}
return UserCard(notifier.data!);
},
)
This pattern is written identically for every API call. It also misses: stale-while-revalidate, background refetch on focus/reconnect, request deduplication, and offline support.
After: Let Qora Own Server Data
final qoraClient = QoraClient(
config: const QoraClientConfig(
defaultOptions: QoraOptions(staleTime: Duration(minutes: 5)),
),
);
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeNotifier()),
ChangeNotifierProvider(create: (_) => AuthNotifier()),
],
child: QoraScope(
client: qoraClient,
lifecycleManager: FlutterLifecycleManager(qoraClient: qoraClient),
connectivityManager: FlutterConnectivityManager(),
child: const MyApp(),
),
),
);
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => UserApi.getUser(userId),
builder: (context, state, fetchStatus) => switch (state) {
Success(:final data) => UserCard(data),
Loading() => const CircularProgressIndicator(),
Failure(:final error) => ErrorWidget('$error'),
Initial() => const SizedBox.shrink(),
},
)
The Qora version removes manual loading flags, notifier notifications, and custom cache guards. Retry, request deduplication, background revalidation, and SWR behavior are built in.
The third builder argument fetchStatus distinguishes FetchStatus.fetching from FetchStatus.paused in offline conditions. Use this state to render a waiting-for-connection indicator instead of a loading spinner.
Division of Responsibilities
| Responsibility | Keep in Provider | Move to Qora |
|---|---|---|
| Data fetched from an API (users, products, feeds) | QoraBuilder + fetcher: | |
| Mutations (create, update, delete) | QoraMutationBuilder | |
| UI state (selected tab, modal open, form values) | ChangeNotifier | |
| Authentication session (token in memory) | ChangeNotifier | |
| Dependency injection (services, repositories) | Provider / ChangeNotifierProvider |
Key API Mapping
| Provider pattern | Qora equivalent |
|---|---|
ChangeNotifier + fetch() method | QoraBuilder with fetcher: |
Consumer + ternary on isLoading / error / data | switch (state) on QoraState |
Manual if (data != null) return | staleTime in QoraOptions |
ChangeNotifierProvider (data layer) | QoraScope |
notifyListeners() after write | client.invalidate(key) |
Checklist
- Add
qora_fluttertopubspec.yamland runflutter pub get - Nest
QoraScopeinside existingMultiProviderroot (not a replacement) - Identify
ChangeNotifierclasses that only fetch and emit loading/error/data and replace them withQoraBuilder(fetcher:, 3-arg builder) - Replace
Consumerloading/error/data ternaries withswitch (state) - Replace manual cache guards with
staleTime - Replace
notifyListeners()post-write withclient.invalidate(key) - Keep
ChangeNotifierProviderentries for UI state, auth, and DI