如何防止riverpod的ConsumerWidget在管理ThemeMode时重新构建

huangapple go评论57阅读模式
英文:

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&lt;ThemeData&gt;(
  (ref) =&gt; 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(&quot;building app&quot;);
    }

    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 = &quot;home&quot;;

  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(&quot;building home&quot;);
    }

    return Scaffold(
      body: Center(
        child: Column(
          children: [
            Text(
              &quot;Hello World&quot;,
              style: headlineMedium,
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text(&#39;Theme mode&#39;),
              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 &#39;package:flutter_riverpod/flutter_riverpod.dart&#39;;
import &#39;package:flutter/material.dart&#39;;

final themeProvider = Provider&lt;ThemeData&gt;(
  (ref) =&gt; 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(&quot;#building app&quot;);

    return MaterialApp(
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      themeMode: themeMode,
      builder: (context, child) {
        print(&quot;##building builder&quot;);
        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(&quot;###building home&quot;);

    return Scaffold(
      body: SwitchListTile(
        title: const Text(&#39;Theme mode&#39;),
        value: ref.watch(themeModeStateProvider) == ThemeMode.light,
        onChanged: (value) {
          ref.read(themeModeStateProvider.notifier).toggleThemeMode();
        },
      ),
    );
  }
}

final themeModeStateProvider =
    AutoDisposeNotifierProvider&lt;ThemeModeState, ThemeMode&gt;(
  ThemeModeState.new,
);

class ThemeModeState extends AutoDisposeNotifier&lt;ThemeMode&gt; {
  @override
  ThemeMode build() =&gt; 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 -&gt; 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&lt;MaterialLocalizations&gt;(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) =&gt; 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.

huangapple
  • 本文由 发表于 2023年7月27日 20:58:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76779987.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定