从头学 Dart 第十集

183 阅读19分钟

最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。

  1. Scaffold 中的新配置 drawer, 这个用于打开侧边栏,通常是在页面的左侧 通常情况下抽屉组件是相当复杂的,所以我们一般会重新构建一个新的组件(通常是 StatelessWidget)来做渲染
drawer: Drawer(),

Drawer 的一般形式如下所示:

import 'package:flutter/material.dart';

class MainDrawer extends StatelessWidget {
  const MainDrawer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          DrawerHeader(
            padding: const EdgeInsets.all(20),
            child: Text('Drawer Header'), // 假设你想要显示的文本
          ),
          // 这里可以添加更多的列项
        ],
      ),
    );
  }
}

这里我们使用到了其专用组件,名为 DrawerHeader 配置项有 padding 和 child 两个属性,其较为完整的代码如下所示:

import 'package:flutter/material.dart';

class MainDrawer extends StatelessWidget {
  const MainDrawer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          DrawerHeader(
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [
                  Theme.of(context).colorScheme.primaryContainer,
                  Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8),
                ],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
              child: Row(
                children: [
                  Icon(
                    Icons.fastfood,
                    size: 48,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                  const SizedBox(width: 18),
                  Text(
                    'Cooking up!',
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
                      color: Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ],
              ),
            ),
          ),
          // 在这里可以添加更多的抽屉项
        ],
      ),
    );
  }
}

上述代码在 DrawerHeader 组件中配置了 padding 背景色 然后完成了一个左图右文字的布局。

需要注意的是一旦设置了 drawer 那么 appBar 中的返回按钮就消失了。

  1. 与 DrawerHeader 并列的组件

与抽屉头部内容并列的内容就是抽屉按钮了,有一个组件专门用来做这个事情 ListTile 注意是 ListTile 而不是 ListTitle。

给每一个 ListTile 配置如下属性:leading title onTap 顾名思义可以清楚的知道每一个的作用

import 'package:flutter/material.dart';

class ClickableItem extends StatelessWidget {
  const ClickableItem({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(
        Icons.restaurant,
        size: 26,
        color: Theme.of(context).colorScheme.onBackground,
      ),
      title: Text(
        'Meals',
        style: Theme.of(context).textTheme.titleSmall!.copyWith(
          color: Theme.of(context).colorScheme.onBackground,
          fontSize: 24,
        ),
      ),
      onTap: () {
        // 在这里添加点击事件的处理逻辑
      },
    );
  }
}
  1. 关闭抽屉

关闭抽屉是相当简单的,正如关闭所有弹出窗口一样,我们只需要 pop 就可以了

Navigator.of(context).pop();

同样在打开一个抽屉之前,我们需要将可能存在的抽屉关掉(当然只是有的时候)

void setScreen(BuildContext context, String identifier) {
  if (identifier == 'filters') {
    // 如果标识符是 'filters',则跳转到 FiltersScreen 页面
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => const FiltersScreen(),
      ),
    );
  } else {
    // 如果标识符不是 'filters',则返回到上一页
    Navigator.of(context).pop();
  }
}
  1. 开关组件 -- SwitchListTile 这个组件难度不大。
import 'package:flutter/material.dart';

class GlutenFreeSwitchTile extends StatelessWidget {
  final bool value;
  final ValueChanged<bool> onChanged;

  const GlutenFreeSwitchTile({
    Key? key,
    required this.value,
    required this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SwitchListTile(
          value: value,
          onChanged: onChanged,
          title: Text(
            'Gluten-free',
            style: Theme.of(context).textTheme.titleLarge!.copyWith(
              color: Theme.of(context).colorScheme.onBackground,
            ),
          ),
          subtitle: Text(
            'Only include gluten-free meals.',
            style: Theme.of(context).textTheme.labelMedium!.copyWith(
              color: Theme.of(context).colorScheme.onBackground,
            ),
          ),
          activeColor: Theme.of(context).colorScheme.tertiary,
          contentPadding: const EdgeInsets.only(left: 34, right: 22),
        ),
      ],
    );
  }
}

配置项详细作用

  • value

    • 类型:bool
    • 作用:控制开关的当前状态,true 表示开关打开,false 表示开关关闭。
  • onChanged

    • 类型:ValueChanged<bool>
    • 作用:当开关状态改变时调用的回调函数。它接收一个布尔值,表示开关的新状态。
  • title

    • 类型:Widget
    • 作用:显示在开关旁边的标题。在这个例子中,它是一个 Text Widget,显示文字 "Gluten-free"。
  • style

    • 类型:TextStyle
    • 作用:定义标题文本的样式。这里使用了主题中的 titleLarge 文本样式,并修改了颜色为 onBackground
  • subtitle

    • 类型:Widget
    • 作用:显示在标题下方的副标题。在这个例子中,它是一个 Text Widget,显示文字 "Only include gluten-free meals."。
  • activeColor

    • 类型:Color
    • 作用:定义开关打开时的激活颜色。这里使用了主题中的 tertiary 颜色。
  • contentPadding

    • 类型:EdgeInsets
    • 作用:定义 SwitchListTile 内容的内边距。这里设置了左边距为 34,右边距为 22,这有助于调整开关控件与其周围内容的空间。

这个 SwitchListTile 通常用于设置界面,允许用户通过开关来启用或禁用某个功能,例如在这个例子中的 "Gluten-free" 选项。用户可以通过点击开关来改变其状态,并且状态的改变会通过 onChanged 回调函数通知给应用程序。

  1. 开关按钮的处理逻辑
var _glutenFreeFilterSet = false;

void onChanged(isChecked) {
  setState((){
    _glutenFreeFilterSet = isChecked;
  })
}
...
SwitchListTile(
  value: value,
  onChanged: onChanged,
  ...
),

回调函数中的 setState 是必须的,否则开关不会动的,尽管后面的数据已经变了。

  1. Navigator.of(context).pushReplacement

这个方法相当于先 pop 然后再 push

在Flutter中,Navigator类用于管理应用的导航栈,其中pushpushReplacement是两个用于导航到新页面的方法,但它们的行为有所不同:

  1. Navigator.of(context).push

    • push方法将一个新路由(页面)压入当前的导航栈。
    • 当你使用push导航到新页面时,用户可以通过按下物理返回键或在设备上使用后退按钮来返回到前一个页面。
    • push方法返回一个Future,当新路由被弹出时,这个Future会被完成。
  2. Navigator.of(context).pushReplacement

    • pushReplacement方法同样将一个新路由压入导航栈,但它会替换当前路由而不是简单地添加一个新的路由。
    • 使用pushReplacement时,当前页面会被新页面替换,用户无法返回到被替换的页面。
    • 这个方法也返回一个Future,当新路由被弹出时,这个Future会被完成。

对比

  • 导航行为push会保留当前页面的状态,允许用户返回到原来的页面;而pushReplacement会替换当前页面,用户无法返回被替换的页面。
  • 返回行为:使用push时,用户可以使用返回操作回到前一个页面;使用pushReplacement时,返回操作会将用户带到新页面的前一个页面,而不是被替换的页面。
  • 历史记录push会增加一个新的历史记录;pushReplacement会替换当前的历史记录。

使用场景

  • 当你希望用户能够回到原来的页面时,使用push
  • 当你希望替换当前页面,并且不希望用户返回到当前页面时,使用pushReplacement

示例代码

// 使用push导航到新页面
Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewPage()));

// 使用pushReplacement替换当前页面
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context) => NewPage()));

在实际应用中,选择使用push还是pushReplacement取决于你的应用逻辑和用户体验需求。

  1. dart 中的属性名变量

和 js 中一样,dart 中也可以将 map 的属性名设置成变量,但是不同之处在于,dart 中属性名变量无需使用中括号包裹

{
    Filter.glutenFree: _glutenFreeFilterSet,
}
  1. WillPopscope 组件

简单来说,这个组件包裹原来的组件,并且为原来的组件提供了一个声明周期函数 onWillPop, 这个函数会在当前页面被弹走之前执行,可用在二次确认等功能的实现上。

WillPopScope组件在Flutter中用于自定义导航时的后退按钮行为。它允许你决定当用户按下后退按钮时会发生什么,而不是简单地退出当前页面。这在某些情况下非常有用,比如在表单填写过程中提示用户保存数据,或者在游戏应用中确认用户是否真的想要退出当前关卡。

下面是一个简单的示例,展示了如何使用WillPopScope来自定义后退按钮的行为:

import 'package:flutter/material.dart';

class CustomWillPopScope extends StatefulWidget {
  @override
  _CustomWillPopScopeState createState() => _CustomWillPopScopeState();
}

class _CustomWillPopScopeState extends State<CustomWillPopScope> {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // 当用户尝试通过后退按钮退出页面时,会调用这个函数
        return await showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: Text('确认退出'),
            content: Text('你想要退出这个页面吗?'),
            actions: <Widget>[
              TextButton(
                child: Text('取消'),
                onPressed: () => Navigator.of(context).pop(false),
              ),
              ElevatedButton(
                child: Text('退出'),
                onPressed: () => Navigator.of(context).pop(true),
              ),
            ],
          ),
        );
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text('WillPopScope示例'),
        ),
        body: Center(
          child: Text('按后退按钮将触发自定义操作'),
        ),
      ),
    );
  }
}

在这个示例中,WillPopScope组件包裹了一个ScaffoldWillPopScopeonWillPop属性定义了一个函数,该函数会在用户尝试通过后退按钮退出页面时被调用。在这个函数中,我们使用showDialog来显示一个AlertDialog,询问用户是否真的想要退出页面。如果用户选择“退出”,则返回true,页面将被关闭;如果用户选择“取消”,则返回false,页面将保持打开状态。

这个自定义的后退按钮行为可以用于很多场景,比如在用户离开一个填写了表单的页面时提醒他们保存数据,或者在游戏应用中防止用户意外退出当前关卡。通过这种方式,你可以提供更好的用户体验,确保用户在离开页面前有机会保存他们的工作。

再看一例,

onWillPop: () async {
  Navigator.of(context).pop({
    Filter.glutenFree: glutenFreeFilterSet,
    Filter.lactoseFree: lactoseFreeFilterSet,
    Filter.vegetarian: _vegetarianFilterSet,
    Filter.vegan: _veganFilterSet,
  });
  return false;
}

上面的代码中,我们在调用 Navigator.of(context).pop() 的时候传递了参数,而这些参数会在跳转目标页中以 future 格式被接受。或者说 onWillPop 钩子本身就要求返回值必须是 Future 结构的。因此 使用 async 之后,如果返回的是 true 而页面跳转继续,如果是 false 则不发生跳转。

  1. 接受 onWillPop 传递的信息

假设 A 页面弹出 B 页面,并且 B 页面在销毁之前通过 onWillPop 传递了信息,那么在 B 销毁并回到 A 页面的时候,A 页面如何获得这些信息呢?

这其实很简单,因为 Navigation.of(context).push 是具有返回值的,其返回值是一个 future 对象,其中包裹着将要打开的页面在退出之时传递的信息:

void setScreen(String identifier) async {
  Navigator.of(context).pop();
  if (identifier == 'filters') {
    final result = await Navigator.of(context).push<Map<Filter, bool>>(
      MaterialPageRoute(
        builder: (ctx) => const FiltersScreen(),
      ),
    );
    // 在弹出页面的地方接受其关闭的时候的传值,逻辑闭环
    print(result);
  }
}

注意上面代码中 push 方法带着泛型,表示函数的返回值类型,尽管它是被 Future 包裹的,但是却没有体现 Future, 这一点需要注意。

  1. 空合并运算符 在Dart中,?? 运算符被称为空合并运算符(null coalescing operator)。它用于在右侧的表达式之前,提供一个左侧变量的默认值,当且仅当左侧的变量为 null 时,才会使用右侧的表达式作为结果。如果左侧的变量非 null,则直接返回左侧变量的值。

这个运算符通常用于简化 null 检查逻辑,使代码更加简洁。以下是一些使用 ?? 运算符的例子:

void main() {
  String? greeting;

  print('Greeting: ${greeting ?? 'Hello, World!'}'); // 输出: Hello, World!
  greeting = 'Hi';

  print('Greeting: ${greeting ?? 'Hello, World!'}'); // 输出: Hi
}

int? score;
int highScore = score ?? 0; // 如果 score 为 null,则 highScore 为 0,否则 highScore 为 score 的值

在第一个例子中,greeting 变量可能为 null,所以使用 ?? 运算符提供了一个默认的问候语 'Hello, World!'。如果 greeting 有值,则使用 greeting 的值;如果 greetingnull,则使用 'Hello, World!'

在第二个例子中,score 变量可能为 null,使用 ?? 运算符可以简洁地设置一个默认值 0。如果 scorenull,则 highScore 将等于 score 的值;如果 scorenull,则 highScore 将等于 0

?? 运算符也可以用来链式合并多个表达式,例如:

String? first;
String? second;
String? third;

String result = first ?? second ?? third ?? 'Default';

在这个例子中,result 将首先尝试使用 first 的值,如果 firstnull,则尝试 second,如果 second 也为 null,则尝试 third,如果所有变量都为 null,则最终使用 'Default' 作为默认值。

使用 ?? 运算符可以有效地减少代码中的 null 检查逻辑,使代码更加清晰和简洁。

需要注意的是和 js 不一样,dart 中只检查 null 因为没有 undefined 这个数据类型。

  1. 使用入参计算组件的初始化数据

并不是组件所有的初始化数据都能被调用者全部传递进来,有的变量中保存的是组件根据入参自己计算出来的值,这种计算发生的地方在于 initState 钩子函数中,如下代码所示:

class _FiltersScreenState extends State<FiltersScreen> {
  var _glutenFreeFilterSet = false;
  var _lactoseFreeFilterSet = false;
  var _vegetarianFilterSet = false;
  var _veganFilterSet = false;

  @override
  void initState() {
    super.initState();
    // 假设widget.currentFilters是一个Map对象,包含了过滤条件
    _glutenFreeFilterSet = widget.currentFilters['glutenFree'] ?? false;
    _lactoseFreeFilterSet = widget.currentFilters['lactoseFree'] ?? false;
    _vegetarianFilterSet = widget.currentFilters['vegetarian'] ?? false;
    _veganFilterSet = widget.currentFilters['vegan'] ?? false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Filters'),
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: <Widget>[
            DrawerHeader(
              child: Text('Drawer Header'),
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
            ),
            ListTile(
              title: const Text('Meals'),
              onTap: () {
                Navigator.of(context).pop();
                if (identifier == 'meals') {
                  Navigator.of(context).pushReplacement(
                    MaterialPageRoute(
                      builder: (context) => SomeMealsScreen(),
                    ),
                  );
                }
              },
            ),
            // ... other drawer items ...
          ],
        ),
      ),
    );
  }
}
  1. 跨组件数据传递方案 Provider 和 Customer

与此极为相关的一个概念是: The riverpod Package

需要这样做的理由十分简单,那就是逐级传递信息会使得代码冗余并且维护困难;而对于不是父子关系的组件之间的通信依赖于最初的共同父组件,它们之间的信息交互在有的时候非常复杂甚至不易实现。

为此我们需要安装 flutter riverpodz ^2.3.2

flutter pub add flutter riverpod
  1. Riverpod 的原理

就是 Provider 中提供不可变状态以及可以更改这些状态的方法;而在需要使用数据的地方引用这些 Provider 并通过其提供的方法修改 Provider 中保存的状态。 Provider 中的状态改变之后,由于特殊的 watch 机制,能够自动刷新所有引用此 Provider 数据的组件。

// 创建一个Provider对象,用于管理应用中的状态。
Provider<dynamic>(
  // create: 函数参数,定义如何创建Provider要管理的数据。
  // 这个函数将在Provider被构建时调用,返回的值将被Provider管理。
  create: (context) => yourCreateFunction(),
  // name: 可选参数,为Provider提供一个名称,主要用于调试。
  name: name,
  // dependencies: 可选参数,定义Provider依赖的其他Provider。
  // 当依赖的Provider更新时,当前Provider也会重新构建。
  dependencies: dependencies,
  // from: 可选参数,指定一个Family对象,允许Provider从另一个Provider获取数据。
  from: from,
  // argument: 可选参数,传递给create函数的参数,可用于区分不同的Provider。
  argument: argument,
  // debugGetCreateSourceHash: 可选参数,提供一个函数,用于生成Provider创建源的哈希值,主要用于调试。
  debugGetCreateSourceHash: () => yourDebugFunction(),
)
  1. 创建一个最简单的 Provider 并使用之
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meals/data/dummy_data.dart';

final mealsProvider = Provider((ref){
    return dummyMeals;
})

这里我们使用其他类中的数据作为此 Provider 的初始状态,Provider 只接受了一个参数,就是其构造函数;这个 Provider 并没有向外提供修改状态的接口。

然后就可以在合适的地方使用了:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:meals/providers/meals_provider.dart';

如果一个可变组件决定使用 Provider 中的数据,或者更为确切的说,如果一个可变组件想要成为 Consumer 那么它就不能再继承自 StatefulWidget 基类了,应该更加具体的继承 ConsumerStatefulWidget 并实现 createState 接口,同理内部类也不再继承 State 而是更为精确的 ConsumerState(提供了包括 ref 在内的对象)

class TabsScreen extends ConsumerStateWidget {
    const TabsScreen({super.key});

    @override
    State<TabsScreen> createState () {
        return _TabsScreenState();
    }
}
...

class _TabsScreenState extends ConsumerState<TabsScreen> {
    ...

    @override
    Widget build(BuildContext context) {
        final meals = ref.watch(mealsProvider);
        final availableMeals = meals.where((meal){...});
    }
}

注意看,这里使用的 ref 来自于基类 ConsumerState, 这里我们使用 ref.watch 监视 import 引入的 mealsProvider,所以其中的状态发生变化时 _TabsScreenState 才会相应的做出变化,并重新执行 build。

然而,如果不是可变组件则应该继承 ConsumerWidget 而不是 StateLessWidget:

class TabsScreen extends ConsumerWidget {
    const TabsScreen({super.key});
    ...
}

最后,如果使用到了 Provider 功能,则必须使用 ProviderScope 包裹整个 App 根组件:

void main() {
    runApp(
        const ProviderScope(
            child: App(),
        ),
    );
}
  1. 向外提供用于改变自身状态的接口

代码片段中有一些语法错误和格式问题,我将帮你识别并整理这段代码,同时添加注释来解释每一部分的作用。

整理后的代码如下:

import 'package:meals/models/meal.dart';

// FavoriteMealsNotifier类继承自StateNotifier,用于管理收藏的菜品列表
class FavoriteMealsNotifier extends StateNotifier<List<Meal>> {
  // 构造函数初始化状态为一个空列表
  FavoriteMealsNotifier() : super([]);

  // 切换菜品的收藏状态
  void toggleMealFavoriteStatus(Meal meal) {
    // 检查当前状态中是否已包含该菜品
    final mealIsFavorite = state.contains(meal);
    if (mealIsFavorite) {
      // 如果已收藏,则从列表中移除
      state = state.where((m) => m.id != meal.id).toList();
      return false;
    } else {
      // 如果未收藏,则添加到列表中
      state = [...state, meal];
      return true;
    }
  }
}

// 使用StateNotifierProvider来提供FavoriteMealsNotifier,以便在Widget树中使用
final favoriteMealsProvider = StateNotifierProvider<FavoriteMealsNotifier, List<Meal>>((ref) {
  return FavoriteMealsNotifier();
});

注释解释:

  • FavoriteMealsNotifier类继承自StateNotifier,它是一个通用的类,用于管理状态并通知订阅者状态的变化。这里的状态是List<Meal>,表示一个菜品列表。
  • 构造函数FavoriteMealsNotifier接受一个初始状态,这里是一个空的菜品列表。
  • toggleMealFavoriteStatus方法用于切换一个菜品的收藏状态。它首先检查当前状态中是否已包含该菜品,如果是,则从列表中移除;如果不是,则添加到列表中。
  • stateStateNotifier中的一个内置属性,它代表了当前的状态,并且可以直接访问和修改。
  • StateNotifierProvider是一个Provider,用于在Flutter的Widget树中提供StateNotifier对象。它接受一个创建StateNotifier对象的函数,这里返回一个FavoriteMealsNotifier实例。

这个示例展示了如何使用StateNotifierStateNotifierProvider来管理一个复杂的状态,例如收藏的菜品列表。通过这种方式,你可以在Flutter应用中轻松地管理和访问状态。

  • 可以看出来,这和简单的 Provider 完全就是两码事。一个直接使用 Provider 构造函数,另一个使用的是 StateNotifierProvider 构造函数。
  • 还需要注意的事情是,在 toggleMealFavoriteStatus 方法中,我们是给 state 赋值,而不是返回这个新状态,这个和 redux 是完全不同的,相反 toggleMealFavoriteStatus 可以返回其他值(例如上面就返回了一个布尔值),并在调用方接受,并且能保证更新和返回值顺序的正确性,这本身就很强大。
  1. 状态的不可变性和扩展运算符 Provider 中的状态不可变,所谓不可变指的就是 s = [1,2,4] 不可以执行 s.push(5) 但是可以执行 s = [1,2,4,5] 或者简单的来说 s 的当前值(状态)仿佛是 const 的。

所以上面的代码中,新增我们用的是扩展符号 [..oldArr, newItem],而删除使用的是 watch + toList().

  1. 使用可变状态的 Provider
import 'package:meals/providers/favorites_provider.dart';

...
final favoriteMeals = ref.watch(favoriteMealsProvider)

...
@override
Widget build(BuildContext context, WidgetRef ref) {
  return Scaffold(
  appBar: AppBar(
    title: Text(meal.title), // 使用当前菜品的标题作为AppBar的标题
    actions: [
      IconButton(
        onPressed: () {
          // 当按钮被按下时,读取favoriteMealsProvider的notifier,
          // 并调用toggleMealFavoriteStatus方法来切换收藏状态
          final wasAdded = ref.read(favoriteMealsProvider.notifier).toggleMealFavoriteStatus(meal);
          // 清除之前的SnackBar,以避免重复显示
          ScaffoldMessenger.of(context).clearSnackBars();
          // 显示SnackBar来反馈操作结果
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(
                // 根据wasAdded的值显示不同的文本
                wasAdded ? 'Meal added as a favorite.' : 'Meal removed.',
              ),
              action: SnackBarAction(
                label: wasAdded ? 'UNDO' : 'UNDO',
                onPressed: () {
                  // 如果菜品被添加到收藏,则提供撤销操作
                  if (wasAdded) {
                    ref.read(favoriteMealsProvider.notifier).toggleMealFavoriteStatus(meal);
                  }
                },
              ),
              // SnackBar的图标,显示一个星星表示收藏操作
              icon: const Icon(Icons.star),
            ),
          );
        },
        icon: const Icon(Icons.star), // 使用星星图标表示收藏
      ),
    ],
  ),
);
}

The riverpod package automatically extracts the “state” property value from the notifier class that belongs to the provider. Hence, ref.watch() yields List here (instead of the Notifier class).

  1. dart 中对象的解构赋值
class FiltersNotifier extends StateNotifierMap<Filter, bool> {
  // 在构造函数中初始化所有过滤器的默认状态为false
  FiltersNotifier() : super({
    Filter.glutenFree: false,
    Filter.lactoseFree: false,
    Filter.vegetarian: false,
    Filter.vegan: false,
  });

  // 设置特定过滤器的状态
  void setFilter(Filter filter, bool isActive) {
    // 使用不可变的方式更新状态
    state = {
      ...state,
      filter: isActive,
    };
  }
}

// filtersProvider是一个StateNotifierProvider,用于在Widget树中提供FiltersNotifier
final filtersProvider = StateNotifierProvider<FiltersNotifier, Map<Filter, bool>>((ref) {
  return FiltersNotifier();
});

使用示例,

@override
void initState() {
  super.initState();
  // 从Provider中读取活动的过滤器状态
  final activeFilters = ref.read(filtersProvider);
  // 使用活动的过滤器状态来初始化本地状态变量
  glutenFreeFilterSet = activeFilters[Filter.glutenFree]!;
  lactoseFreeFilterSet = activeFilters[Filter.lactoseFree]!;
  vegetarianFilterSet = activeFilters[Filter.vegetarian]!;
  veganFilterSet = activeFilters[Filter.vegan]!;
}
  1. Provider 作为出入口结合组件内部状态自理

使用Provider管理状态时,我们可以将其作为状态的来源(入口),而将更改状态的方法作为触发更新的途径(出口)。在组件的生命周期内,可以通过局部状态来管理界面逻辑,这样就无需让组件本身持有可变状态,也避免了频繁直接更新Provider中的状态,从而减少了不必要的界面刷新和潜在的性能问题。

body: WillPopScope(
  onWillPop: () async {
    // 当用户尝试退出页面时,更新Provider中的状态
    ref.read(filtersProvider.notifier).setFilters({
      Filter.glutenFree: _glutenFreeFilterSet,
      Filter.lactoseFree: _lactoseFreeFilterSet,
      Filter.vegetarian: _vegetarianFilterSet,
      Filter.vegan: _veganFilterSet,
    });
    // 不需要显式调用 Navigator.pop,因为我们只更新状态并不退出页面
    // Navigator.of(context).pop();
    return true; // 返回 true 表示允许后退操作
  },
  child: ... // 页面的其余部分
),

以及,

class FiltersScreen extends ConsumerWidget {
  const FiltersScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 使用ref.watch来监听filtersProvider中的状态变化
    final activeFilters = ref.watch(filtersProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Filters'), // AppBar标题
      ),
      // 使用Column作为body的布局
      body: Column(
        children: [
          SwitchListTile(
            // 使用activeFilters中对应的值来设置SwitchListTile的value
            value: activeFilters[Filter.glutenFree] ?? false,
            // 当SwitchListTile的值改变时,调用callbacks来更新状态
            onChanged: (bool? newValue) {
              // 根据newValue的值来更新filtersProvider中的状态
              ref.read(filtersProvider.notifier).setFilter(Filter.glutenFree, newValue!);
            },
            title: const Text('Gluten-free'), // 开关项的标题
            subtitle: const Text('Only show gluten-free meals'), // 开关项的副标题
          ),
          // ... 可以添加更多的SwitchListTile或其他Widget来展示和控制其他过滤器 ...
        ],
      ),
    );
  }
}
  1. 创建依赖于其他Provider的新Provider

在Flutter应用中,可以基于已有的Provider创建新的Provider,这是一种高效的方式来构建一个可响应的状态树。这种方式允许新的Provider依赖于一个或多个现有的Provider,当依赖的Provider状态更新时,依赖于它们的新Provider也会自动更新。这与计算机科学中的“计算属性”或“衍生状态”的概念类似,即新的Provider的输出是基于输入的当前状态动态计算得出的。

下面是如何实现这一过程的示例代码:

// 定义一个StateNotifierProvider来管理过滤器的状态
final filtersProvider = StateNotifierProvider<FiltersNotifier, Map<Filter, bool>>(
  (ref) => FiltersNotifier()
);

// 定义一个Provider来根据激活的过滤器筛选菜品
final filteredMealsProvider = Provider((ref) {
  // 使用ref.watch来读取mealsProvider中的状态
  final meals = ref.watch(mealsProvider);
  // 使用ref.watch来读取filtersProvider中的状态
  final activeFilters = ref.watch(filtersProvider);

  // 根据激活的过滤器筛选菜品
  return meals.where((meal) {
    // 如果启用了无麸质过滤器,但菜品不是无麸质的,则排除该菜品
    if (activeFilters[Filter.glutenFree]! && !meal.isGlutenFree) {
      return false;
    }
    // 如果启用了无乳糖过滤器,但菜品不是无乳糖的,则排除该菜品
    if (activeFilters[Filter.lactoseFree]! && !meal.isLactoseFree) {
      return false;
    }
    // 如果启用了素食过滤器,但菜品不是素食的,则排除该菜品
    if (activeFilters[Filter.vegetarian]! && !meal.isVegetarian) {
      return false;
    }
    // 如果启用了纯素过滤器,但菜品不是纯素的,则排除该菜品
    if (activeFilters[Filter.vegan]! && !meal.isVegan) {
      return false;
    }
    // 如果菜品通过了所有过滤器的检查,则包含该菜品
    return true;
  }).toList();
});

在这个示例中:

  • filtersProvider是一个StateNotifierProvider,用于管理应用中的过滤器状态。
  • filteredMealsProvider是一个Provider,它依赖于mealsProviderfiltersProvider。它根据激活的过滤器来筛选菜品列表。
  • ref.watch方法用于读取其他Provider的状态。
  • where方法用于根据条件筛选meals列表中的菜品。
  • toList方法用于将where方法返回的迭代器转换为列表,以便filteredMealsProvider可以返回一个具体的值。
  1. 收藏功能图标问题
icon: Icon(isFavorite ? Icons.star : Icons.star_border),