dart 如何防止Riverpod的ConsumerWidget重建管理ThemeMode

kpbwa7wx  于 2023-07-31  发布在  其他
关注(0)|答案(3)|浏览(124)

我正在使用Riverpod state Provider管理flutter应用程序的ThemeMode,该提供程序一直按预期工作,直到我尝试读取Theme.of(context)以获取ThemeData的当前值,这会导致部件的重建过多(连续13~14次)。因此,我决定在Riverpod的repository example之后为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({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(),
    );
  }
}

字符串
主题模式提供程序:

@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;
    }
  }
}


主屏幕:

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();
              },
            ),
          ],
        ),
      ),
    );
  }
}

gt0wga4j

gt0wga4j1#

我附上一个较短的代码来重现这个问题(不使用生成):

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小部件,特别是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));
  }

sqougxex

sqougxex2#

您不希望在应用程序中收听整个主题。您希望小部件只监听它们使用的内容。
由于您已将主题转换为 Provider,因此您可以使用Riverpod的“选择”功能:
TL;DR,而不是:

final ThemeData themeData = ref.watch(themeProvider);
final TextStyle headlineMedium = themeData.textTheme.headlineLarge!;

字符串
执行:

final TextStyle headlineMedium = ref.watch(
  themeProvider.select((themeData) => themeData.textTheme.headlineLarge!,
);


这样,您的消费者将只收听headlineMedium,而不是整个主题。

yyyllmsg

yyyllmsg3#

MediaQuery.of(context).platformBrightness == Brightness.light会在MediaQuery发生任何更改时触发,包括屏幕大小(旋转?)。您可能希望使用新的MediaQuery.platformBrightnessOf仅在更改该参数时触发。

相关问题