我正在使用Riverpod状态提供程序管理我的Ffltter应用程序的ThemeMode,该模式一直按预期工作,直到我try 读取Theme.of(context)以获取ThemeData的当前值,这会导致超出(连续13~14次)的小部件重建.所以我决定在Riverpod的repository example之后为ThemeData创建一个提供者,但我仍然会得到这些不必要的重建.我如何防止这些不必要的Riverpod重建以获取ThemeData?为什么会发生这种情况呢?

此代码在github上可用.

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

主题模式提供程序:

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

推荐答案

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

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,在Widget生命周期管理方法和回调中不使用ref.watch.改用ref.read:

onChanged: (value) {
  ref.read(themeModeStateProvider.notifier).toggleThemeMode();
},

您的问题在于MainApp小部件,特别是builder参数.这个问题的简短解决方案是不使用of(context) Inside builder,它看起来是这样的:

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

现在,您的重建已优化.

说到future ,你的ThemeData最有可能也应该有一个完整的NotifierProvider,并在build方法中优雅地从watch到目前的themeModeStateProvider.那么ProviderScope -> overrideWithValue构造就一点用处都没有了.

好的,长期的解决方案是写一个问题到Flutter 库.


The final version,将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));
  }

Dart相关问答推荐

捕获一个区域中的所有期货

Const 构造函数不适用于 Dart 并显示 id 为 0

当未指定返回类型时,dart 显示另一个函数内部的确切返回类型,但对于顶层函数显示为动态

flutter.io (dart) - 将填充设置为设备宽度的百分比?

在Flutter中重命名文件/图像

如何将指针事件发送到堆栈中的低级子级

在`lib`文件夹中使用`src`子文件夹有什么好处吗

如何go 除字符串中的变音符号?

如何在Flatter中打开设备GPS?

Flutter web url 导航

在 Dart 中将对象推入数组

Flutter ToggleButton 类 - Flutter 1.9.1

在向下滚动时隐藏底部导航栏,反之亦然

dart2js代码比javascript快多少?

Flutter DataTable - 点击行

对该常量表达式的求值会抛出一个表达式

如何在 Dart 中将字符串转换为 utf8?

dart 如何创建、监听和发出自定义事件?

DART:future的语法 then

dart中的max/mi int/double 值是否有常数?