英文:
How to prevent riverpod's ConsumerWidget rebuilds managing ThemeMode
问题
我正在使用Riverpod状态提供程序来管理我的Flutter应用程序的ThemeMode,一切都按预期工作,直到我尝试读取Theme.of(context)
以获取ThemeData的当前值,这导致部件多次不必要地重建(连续13~14次)。因此,我决定按照Riverpod的存储库示例创建一个ThemeData提供程序,但我仍然获得了这些不必要的重建。我该如何防止这些不必要的Riverpod重建以获取ThemeData?为什么会发生这种情况?
这段代码可在GitHub上找到。
主应用程序:
final themeProvider = Provider<ThemeData>(
(ref) => throw UnimplementedError(),
dependencies: const [],
);
void main() {
runApp(const ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({Key? 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提供程序:
@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'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();
},
),
],
),
),
);
}
}
答案1
得分: 1
我附上了一个更短的代码示例来重现这个问题(不使用生成):
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;
}
}
}
顺便说一下,在 widget 生命周期管理方法和回调中不要使用 ref.watch
,而是使用 ref.read
:
onChanged: (value) {
ref.read(themeModeStateProvider.notifier).toggleThemeMode();
},
你的问题出现在 MainApp
widget 中,特别是在 builder
参数中。问题的简短解决方案是不要在 builder
内部使用 of(context)
,看起来像这样:
@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(),
);
}
现在你的重建已经被优化了。
未来,很可能你的 ThemeData
也应该有一个完整的 NotifierProvider
,并在 build
方法内优雅地 watch
当前的 themeModeStateProvider
。然后,ProviderScope -> overrideWithValue
结构将毫无用处。
嗯,长期解决方案是向 Flutter 存储库提交一个问题。
最终版本,考虑到 Localizations
,将如下所示:
@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));
}
希望这能帮助你解决问题。
英文:
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));
}
答案2
得分: 1
你不希望在你的应用程序中监听整个主题。你希望小部件只监听它们所使用的部分。
由于你将主题转换为 Provider,你可以使用Riverpod的“select”功能:
简而言之,不要这样做:
final ThemeData themeData = ref.watch(themeProvider);
final TextStyle headlineMedium = themeData.textTheme.headlineLarge!;
而是这样做:
final TextStyle headlineMedium = ref.watch(
themeProvider.select((themeData) => themeData.textTheme.headlineLarge!),
);
这样,你的消费者将只监听 headlineMedium
而不是整个主题。
英文:
You do not want to listen to the whole Theme in your application. You want widgets to listen to only what they use.
Since you converted the Theme to a Provider, you can use Riverpod's "select" feature:
TL;DR, instead of:
final ThemeData themeData = ref.watch(themeProvider);
final TextStyle headlineMedium = themeData.textTheme.headlineLarge!;
Do:
final TextStyle headlineMedium = ref.watch(
themeProvider.select((themeData) => themeData.textTheme.headlineLarge!,
);
This way, your consumer will listen only to headlineMedium
instead of the whole theme.
答案3
得分: 0
MediaQuery.of(context).platformBrightness == Brightness.light
触发任何对 MediaQuery 的更改,包括屏幕尺寸(旋转等)。您应该使用新的 MediaQuery.platformBrightnessOf
,只在特定参数更改时触发。
英文:
MediaQuery.of(context).platformBrightness == Brightness.light
triggers on ANY change to MediaQuery, including things like screen size (rotation?). You'll want to use the new MediaQuery.platformBrightnessOf
to only trigger on changes to that parameter specifically.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论