I'm managing the ThemeMode of my flutter application with Riverpod state Provider that works as expected up until I try to read Theme.of(context)
to get ThemeData's current values which causes rebuilding of the widget in excess (13~14 times in a row). So I decided create a provider for ThemeData following Riverpod's repository example but I'm still getting these unecessary rebuilds. How can I prevent these unnecessary riverpod rebuilds to get ThemeData? and why is it happening?
This code is available on github.
main app:
final themeProvider = Provider<ThemeData>(
(ref) => throw UnimplementedError(),
dependencies: const [],
);
void main() {
runApp(const ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeMode themeMode = ref.watch(themeModeStateProvider);
if (kDebugMode) {
print("building app");
}
return MaterialApp(
theme: FlexThemeData.light(scheme: FlexScheme.mandyRed),
darkTheme: FlexThemeData.dark(scheme: FlexScheme.mandyRed),
themeMode: themeMode,
builder: (context, child) {
final theme = Theme.of(context);
return ProviderScope(
overrides: [
themeProvider.overrideWithValue(theme),
],
child: child!,
);
},
home: const HomeScreen(),
);
}
}
ThemeMode Provider:
@riverpod
class ThemeModeState extends _$ThemeModeState {
@override
ThemeMode build() {
return ThemeMode.dark;
}
static ThemeMode getSystemTheme(BuildContext context) {
ThemeMode mode = ThemeMode.system;
if (mode == ThemeMode.system) {
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
mode = ThemeMode.light;
} else {
mode = ThemeMode.dark;
}
}
return mode;
}
void toggleThemeMode() {
if (state == ThemeMode.dark) {
state = ThemeMode.light;
} else {
state = ThemeMode.dark;
}
}
}
homescreen:
class HomeScreen extends ConsumerWidget {
static String routeName = "home";
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeData themeData = ref.watch(themeProvider);
final TextStyle headlineMedium = themeData.textTheme.headlineLarge!;
if (kDebugMode) {
print("building home");
}
return Scaffold(
body: Center(
child: Column(
children: [
Text(
"Hello World",
style: headlineMedium,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Theme mode'),
value: ref.watch(themeModeStateProvider) == ThemeMode.light,
onChanged: (value) {
ref.watch(themeModeStateProvider.notifier).toggleThemeMode();
},
),
],
),
),
);
}
}
I am attaching a shorter code to reproduce this problem (without using generation):
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
final themeProvider = Provider<ThemeData>(
(ref) => throw UnimplementedError(),
dependencies: const [],
);
void main() {
runApp(const ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeMode themeMode = ref.watch(themeModeStateProvider);
print("#building app");
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: themeMode,
builder: (context, child) {
print("##building builder");
final theme = Theme.of(context);
return ProviderScope(
overrides: [themeProvider.overrideWithValue(theme)],
child: child!,
);
},
home: const HomeScreen(),
);
}
}
class HomeScreen extends ConsumerWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeData themeData = ref.watch(themeProvider);
print("###building home");
return Scaffold(
body: SwitchListTile(
title: const Text('Theme mode'),
value: ref.watch(themeModeStateProvider) == ThemeMode.light,
onChanged: (value) {
ref.read(themeModeStateProvider.notifier).toggleThemeMode();
},
),
);
}
}
final themeModeStateProvider =
AutoDisposeNotifierProvider<ThemeModeState, ThemeMode>(
ThemeModeState.new,
);
class ThemeModeState extends AutoDisposeNotifier<ThemeMode> {
@override
ThemeMode build() => ThemeMode.dark;
void toggleThemeMode() {
if (state == ThemeMode.dark) {
state = ThemeMode.light;
} else {
state = ThemeMode.dark;
}
}
}
By the way, don't use ref.watch
in widget lifecycle management methods and callbacks. Use ref.read
instead:
onChanged: (value) {
ref.read(themeModeStateProvider.notifier).toggleThemeMode();
},
Your problem lies in the MainApp
widget, specifically in the builder
parameter. The short solution to the problem is to not use of(context)
inside builder
, and it looks like this:
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeMode themeMode = ref.watch(themeModeStateProvider);
final theme = themeMode == ThemeMode.light
? ThemeData.light()
: ThemeData.dark();
return MaterialApp(
theme: theme,
darkTheme: theme,
themeMode: themeMode,
builder: (context, child) {
return ProviderScope(
overrides: [themeProvider.overrideWithValue(theme)],
child: child!,
);
},
home: const HomeScreen(),
);
}
Now your rebuilds are optimized.
Speaking for the future, most likely your ThemeData
should also have a full-fledged NotifierProvider
and inside the build
method elegantly watch
to the current themeModeStateProvider
. Then the ProviderScope -> overrideWithValue
construct is not useful at all.
Well, the long solution is to write an issue to the flutter repository.
The final version, taking into account Localizations
, will look like this:
@override
Widget build(BuildContext context, WidgetRef ref) {
final ThemeMode themeMode = ref.watch(themeModeStateProvider);
final ThemeData themeLight =
FlexThemeData.light(scheme: FlexScheme.mandyRed);
final ThemeData themeDark = FlexThemeData.dark(scheme: FlexScheme.mandyRed);
final ThemeData themeData = (themeMode == ThemeMode.light)
? localizeThemeData(context, themeLight)
: localizeThemeData(context, themeDark);
return MaterialApp(
theme: themeLight,
darkTheme: themeDark,
themeMode: themeMode,
builder: (context, child) {
return ProviderScope(
overrides: [themeProvider.overrideWithValue(themeData)],
child: child!,
);
},
home: const HomeScreen(),
);
static ThemeData localizeThemeData(BuildContext context, ThemeData themeData) {
final MaterialLocalizations? localizations =
Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
final ScriptCategory category =
localizations?.scriptCategory ?? ScriptCategory.englishLike;
return ThemeData.localize(
themeData, themeData.typography.geometryThemeFor(category));
}