How to Provide App Config in Riverpod
Learn how to provide app configuration when using Riverpod in Flutter applications.
Typical problem
When using Riverpod in Flutter applications, you might need to provide configuration settings that can be accessed throughout your app. This is often necessary for things like API endpoints, feature flags, or other environment-specific settings.
The issue
While working on several projects, I noticed many developers resort to provide configuration using all means possible, such as:
- Using global variables
- Using predefined constants that you have to change manually in the code in order to change the configuration, in order to for example, switch between development and production environments
- Using
--dart-define
withString.fromEnvironment('key)
scattered throughout the codebase, which leads to hard-to-maintain code and potential errors caused by typos or missing definitions.
(This is actually a good solution if dart defines are used properly - for example everything is loaded in one.dart
file, but it is often not the case)
Approach that I recommend - using ProviderScope overrides
The approach I recommend is to use Riverpod’s ProviderScope
overrides to provide configuration settings in a clean and maintainable way.
How to do it
- Create a configuration class that holds your app settings. This class can be a simple data class with the necessary fields. I recommend using
freezed
for immutability and easy copying, but this is not necessary.
For example:1 2 3 4 5 6 7 8 9 10 11
import 'package:freezed_annotation/freezed_annotation.dart'; @freezed class AppConfig with _$AppConfig { const AppConfig({ required this.apiUrl, }); @override final String apiUrl; }
- Create a provider for your configuration using
Provider
. Good practice is to use aProvider
that throws an Exception if not overridden, so you can catch configuration issues early in development.1 2 3 4 5
import 'package:flutter_riverpod/flutter_riverpod.dart'; final appConfigProvider = Provider<AppConfig>((ref) { throw UnimplementedError('You forgot to override the appConfigProvider in tests or main.dart'); });
- Override the provider in your main application using
ProviderScope
overrides. This allows you to provide different configurations for different environments (e.g., development, staging, production).1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
class App extends StatelessWidget { const App({ required this.appConfig, super.key, }); final AppConfig appConfig; @override Widget build(BuildContext context) { return ProviderScope( overrides: [ // Override the appConfigProvider with the provided appConfig appConfigProvider.overrideWith( (ref) => appConfig, ), ], child: MaterialApp( title: 'My App', home: HomeScreen(), ), ); } }
- Access the configuration in your app using the provider. You can access the configuration anywhere in your app using
useProvider
orref.watch
/ref.read
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class HomeScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final appConfig = ref.watch(appConfigProvider); return Scaffold( appBar: AppBar( title: Text('Home Screen'), ), body: Center( child: Text('Api URL: ${appConfig.apiUrl}'), ), ); } }
Conclusions
Using Riverpod’s ProviderScope
overrides to provide app configuration is a clean and maintainable approach that allows you to easily switch between different configurations for different environments. This method avoids the pitfalls of global variables and scattered dart-define
usage, leading to more robust and testable code. By following this pattern, you can ensure that your app’s configuration is centralized and easily manageable, making it simpler to maintain and scale your Flutter applications.