Flutter Riverpod Theme Changer Example: A Guide for Beginners
Learn how to implement a theme changer in Flutter using Riverpod. This step-by-step tutorial is perfect for beginners.
# LightDark
## [Code](https://github.com/CodeHariK/Hashnode/blob/main/riverpod/lightdark/README.md)
* ```dart
Install dependencies
flutter create lightdark flutter pub add flutter_riverpod riverpod_annotation, shared_preferences flutter pub add dev:custom_lint dev:riverpod_lint dev:build_runner dev:riverpod_generator Add to analysis_options.yaml analyzer: plugins: - custom_lint Command to run build_runner: dart run build_runner watch --delete-conflicting-outputs Add VsCode Extention Flutter Riverpod Snippets
Adding Provider Scope
For widgets to be able to read providers, we need to wrap the entire application in a "ProviderScope" widget. This is where the state of our providers will be stored. void main() { runApp( const ProviderScope( child: MyApp(), ), ); }
Add part file directive on top of file and import dependencies.
###################(shortcut: riverpodPart) part 'main.g.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
Providers
Providers are the most important part of a Riverpod application. A provider is an object that encapsulates a piece of state and allows listening to that state.
Allows easily accessing that state in multiple locations. Providers are a complete replacement for patterns like Singletons, Service Locators, Dependency Injection or InheritedWidgets.
Simplifies combining this state with others. Ever struggled to merge multiple objects into one? This scenario is built directly inside providers.
Enables performance optimizations. Whether for filtering widget rebuilds or for caching expensive state computations; providers ensure that only what is impacted by a state change is recomputed.
General provider syntax
Simplest provider which does nothing more than returning a constant or running a synchronous function. ###################(shortcut: riverpod) @riverpod MyValue my(MyRef ref) { return MyValue(); } Example @riverpod String hello(HelloRef ref, String name) { return '${Random().nextInt(100)} Hello $name'; }
FutureProvider is the equivalent of Provider but for asynchronous code. ###################(shortcut: riverpodFuture) @riverpod Future<MyValue> my(MyRef ref) async { await someFunction(); return MyValue(); } #Example @riverpod Future<String> futureHello(FutureHelloRef ref, String name) async { await Future.delayed(const Duration(seconds: 2)); return '${Random().nextInt(100)} Hello $name'; }
Class based provider are like above ordinary provider can contain few functions. ###################(shortcut: riverpodAsyncClassKeepAlive) @Riverpod(keepAlive: true) class Storage extends _$Storage { SharedPreferences? get _pref => state.value; @override Future<SharedPreferences> build() async { SharedPreferences pref = await SharedPreferences.getInstance(); return pref; } //Theme static const String _theme = 'theme'; Future<void> saveTheme(ThemeType theme) async => await _pref?.setString(_theme, theme.toString()); ThemeType getTheme() => ThemeType.from(_pref?.getString(_theme)); }
Providers watching Providers
Providers can watch each other and rebuild accordingly. To watch a provider use ref.watch(someProvider); Example @riverpod String hello(HelloRef ref, String person) { final count = ref.watch(futureHelloProvider).value; return '$count $person'; } @riverpod Future<int> futureHello(FutureHelloRef ref) async { await Future.delayed(const Duration(seconds: 1)); return Random().nextInt(100); } Here hello is watching futureHello Whenever futureHello change hello will also rebuild itself.
Consumers
Watching providers in widgets
ConsumerWidget ###################(shortcut: stlessConsumer) Example class HelloConsumer extends ConsumerWidget { const HelloConsumer({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final lightdark = ref.watch(helloProvider('LightDark')); final futureHello = ref.watch(futureHelloProvider); return futureHello.when( data: (data) => InkWell( child: Text(lightdark), onTap: () { ref.invalidate(futureHelloProvider); }, ), error: (error, stackTrace) => const Text('error'), loading: () => const CircularProgressIndicator(), ); } }
Now returning back to LightDark
We have a storageProvider which initializes SharedPreferences which we will use to store theme information. We have another provider called GlobalTheme, which exposes a method to change theme and save it in SharedPreferences. @Riverpod(keepAlive: true) class Storage extends _$Storage { SharedPreferences? get _pref => state.value; @override Future<SharedPreferences> build() async { SharedPreferences pref = await SharedPreferences.getInstance(); return pref; } //Theme static const String _theme = 'theme'; Future<void> saveTheme(ThemeType theme) async => await _pref?.setString(_theme, theme.toString()); ThemeType getTheme() => ThemeType.from(_pref?.getString(_theme)); } @Riverpod(keepAlive: true) class GlobalTheme extends _$GlobalTheme { @override GlobalAppTheme build() { ref.watch(storageProvider); ThemeType theme = ref.read(storageProvider.notifier).getTheme(); return GlobalAppTheme( theme: theme, ); } void setThemeType(ThemeType theme) { state = state.copyWith(theme: theme); ref.read(storageProvider.notifier).saveTheme(state.theme); } }
To change theme
ref.read(globalThemeProvider.notifier).setThemeType(v);
To watch theme
class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { // final GlobalAppTheme globalAppTheme = ref.watch(globalThemeProvider); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed( brightness: globalAppTheme.theme.brightness, seedColor: Colors.deepPurple, ), useMaterial3: true, ), home: const HomePage(), ); } }
```