Flutter状态管理插件bloc_builder的使用

Flutter状态管理插件bloc_builder的使用

bloc_builder

bloc_builder 是一个用于创建 BLoC(业务逻辑组件)的简单方法。您可以在这里查看完整的文档:bloc_builder 文档

bloc_builder 2 将不再支持 Stream,请使用 flutter_live_data 或 LiveData.stream()

Helper Endpoint

$watch

标准小部件,每次 LiveData 更新值时都会重新渲染。

(如果 LiveData 设置了标志 verifyDataChange: true,则只有在值发生变化时才会重新渲染)

verifyDataChange 的默认值为 false

LiveData<int> counter = LiveData(0, verifyDataChange: false);

Widget build(BuildContext context) {
  return $watch(counter, build: (context, int count){
    return Text('counter is $count');
  });
}

$watchMany

在需要使用多个 LiveData 的情况下:

LiveData<int> $a = LiveData(1);
LiveData<String?> $b = LiveData<String?>(null);
LiveData<bool> $c = LiveData(false);

如果对每个 LiveData 使用 $watch 会导致嵌套的 $watch

$watch($a, (context, int a){
    return $watch($b, (context, String? b){
        return $watch($a, (context, bool c){
            return Text('x=$x, y=$y, z=$z');
        })
    })
})

对于这种情况,有一个 makeMemorize 函数可以将 LiveData 分组,您可以设置每个 LiveData 的键。

$watch(
    makeMemorize({
        #a: logic.$a,
        #b: logic.$b,
        #c: logic.$c,
    }).owner(...), build: (context, Memorize m) {
    int x = m[#a] as int;
    String? y = m[#b] as String?;
    bool z = m[#c] as bool;
    return Text('x=$x, y=$y, z=$z');
})

// 或者

$watchMany({
    #a: logic.$a,
    #b: logic.$b,
    #c: logic.$c,
}, owner: ..., build: (context, Memorize m) {
    int x = m[#a] as int;
    String? y = m[#b] as String?;
    bool z = m[#c] as bool;
    return Text('x=$x, y=$y, z=$z');
})

$when

当您希望基于条件进行渲染时使用。第一个匹配的 $case 将被渲染,否则会执行 $else

LiveData<int> counter = LiveData(0);

Widget build(BuildContext context) {
    return $when(counter) |
        $case(
            (int value) => value % 2 == 0,
            build: (context, int count){
                return Text('$count is Even');
            },
        ) |
        $case(
            (int value) => value % 2 != 0,
            build: (context, int count){
                return Text('$count is Odd');
            },
        ) |
        $else(
            build: (context, int count){
                return Text('Impossible!');
            },
        ),;
}

但是在某些情况下,Dart 泛型类型不适用于 $case。因此,我们建议使用以下最佳实践模式:

LiveData<int> counter = LiveData(0);

Widget build(BuildContext context) {
    return $when(counter)
        ..$case(
            (int value) => value % 2 == 0,
            build: (context, int count){
                return Text('$count is Even');
            },
        )
        ..$case(
            (int value) => value % 2 != 0,
            build: (context, int count){
                return Text('$count is Odd');
            },
        )
        ..$else(
            build: (context, int count){
                return Text('Impossible!');
            },
        ),
}

还有减少 $when 代码的快捷方式:if, else, true, false

$if(
    counter,
    (int value) => value % 2 == 0,
    build: (context, int count){
        return Text('$count is Even');
    },
) |
$else(
    build: (context, int count){
        return Text('$count is Odd');
    },
)

$when(isEven) |
    $true(build: (context, int count){
        return Text('$count is Even');
    }) |
    $false(build: (context, int count){
        return Text('$count is Odd');
    }),

$guard

类似于 $when,但 $guard 允许您根据不同的 LiveData 进行条件判断,并在条件匹配时终止。

例如:我们想要显示计数器数字,但是有加载状态和错误状态(如果加载失败等)。

我们可以正常地为计数器创建 $watch,并将加载状态和错误状态的 UI 分离到 $guard 中:

LiveData<int> counter = LiveData(0);
LiveData<bool> loading = LiveData(false);
LiveData<String?> errorMessage = LiveData(null);

Widget build(BuildContext context) {
    return $guard(
        loading,
        when: (loading) => loading == true,
        build: (context, isLoading){
            return Text('now loading...');
        },
    ) |
    $guard.isNotNull(
        errorMessage,
        build: (context, msg){
            return Text('error: $msg');
        },
    ) |
    $watch(counter, build: (context, int count){
        return Text('counter is $count');
    }),
}

注意:从这个例子中,你会看到 $guard 需要添加 when 条件,而 $guard.isNotNull 则不需要 when 条件。

isNotNull 是 Guard 条件辅助函数。

Guard Helper
$guard.isNull
$guard.isNotNull
$guard.isEmpty // 适用于 String 和 List
$guard.isNotEmpty // 适用于 String 和 List
$guard.isTrue
$guard.isFalse

$for

用于创建 ListView(或 AnimatedList 等),它只接收包含列表的 LiveData。

LiveData<List<String>> items = LiveData(<String>["A", "B", "C"]);

Widget build(BuildContext context) {
    return $for(items);
}

$for 有选项自定义列表和项目:

  • buildItem: 用于构建每个项目的 Widget(默认是 toString 文本)
  • buildList: 用于构建 ListView,您将接收到由 buildItem 构建的小部件作为参数(默认是 ListView.builder
  • buildEmpty: 数据为空的情况(默认是无小部件)
LiveData<List<String>> items = LiveData(<String>["A", "B", "C"]);

Widget build(BuildContext context) {
  return $for(
    items,
    buildItem: (context, String item, int index){
      return Text('$item');
    },
    buildList: (context, List<ItemViewHolder<T>> holder){
      // ItemViewHolder 有两个属性
      // - T data
      // - Widget widget
      return ListView.builder(
          itemCount: holder.length,
          itemBuilder: (context, int i) => holder[i].widget,
      );
    },
    buildEmpty: (context, List<String> data){
      return Text('empty!');
    },
  );
}

$for 与嵌套 LiveData

在某些情况下,我们需要直接修改每个项目的数据。

class Item {
  int id;
  String name;
  ...
}

LiveData<List<Item>> $items = LiveData(<Item>[...]);

$for($items);

$items.value[0] = ...;
$items.tick();

在这个例子中,我们想修改索引为 0 的元素。

是的,您可以修改对象的状态,但根据 LiveData 的概念,您必须调用 tick 来通知 LiveData 发生了更改。

这是问题所在! 当我们调用 tick 时,这将触发包含列表的 LiveData,这意味着即使我们只修改列表中的一个对象,它也需要重新渲染整个列表。

直接的解决方案是您必须将 LiveData 添加到您的模型中(在这种情况下是 Item)。

但是,当然,您不想修改模型类。因此,还有一个解决方案,即让 LiveData 帮助您处理这个问题,通过使用 eachItemsInListAsLiveData

class Item {
  int id;
  String name;
  ...
}

LiveData<List<Item>> $items = LiveData(<Item>[...])
    .apply(eachItemsInListAsLiveData());

$for($items);

// 更新列表
$items.value = ...;

// 更新每个元素
detach($items, $items.value[0])!.value = ...

当使用 eachItemsInListAsLiveData 时,您可以通过 detach 从父 LiveData 中获取每个项目的 LiveData。

背后的原理

eachItemsInListAsLiveData 将调用 attach,这将基于您提供的项创建 LiveData 并将其绑定到父 LiveData。

LiveData<List<Item>> $items = LiveData(<Item>[...]);

attach($items, $items.value[0]);
attach($items, $items.value[1]);
attach($items, $items.value[2]);
...

detach($items, $items.value[0])!.value = ...
detach($items, $items.value[1])!.value = ...
detach($items, $items.value[2])!.value = ...

此外,

attach($items, $items.value[0])

这表示我们希望 $items.value[0] 成为 LiveData 并绑定到 $items

建议您将父 LiveData 作为自己的列表使用,因为新的 LiveData 可以遵循其父 LiveData 的生命周期。

最后,与 attach 相反的是 detach,当您想要获取 LiveData 回来时使用。

detach($items, $items.value[2])

更多关于Flutter状态管理插件bloc_builder的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter状态管理插件bloc_builder的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


BlocBuilderflutter_bloc 包中的一个 widget,用于根据 BLoC 的状态来构建 UI。它会在 BLoC 的状态发生变化时自动重建,从而更新 UI。BlocBuilder 是 Flutter 中实现状态管理的一种有效方式。

安装 flutter_bloc

首先,你需要在 pubspec.yaml 文件中添加 flutter_bloc 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.0

然后运行 flutter pub get 来安装依赖。

使用 BlocBuilder

1. 定义 BLoC 和事件

首先,你需要定义一个 BLoC 和一些事件。假设我们有一个简单的计数器应用:

import 'package:flutter_bloc/flutter_bloc.dart';

// 定义事件
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}

class DecrementEvent extends CounterEvent {}

// 定义 BLoC
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0);

  [@override](/user/override)
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event is IncrementEvent) {
      yield state + 1;
    } else if (event is DecrementEvent) {
      yield state - 1;
    }
  }
}

2. 在 UI 中使用 BlocBuilder

接下来,你可以在 UI 中使用 BlocBuilder 来响应 BLoC 的状态变化:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter App')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, state) {
            return Text(
              'Count: $state',
              style: TextStyle(fontSize: 24),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () {
              context.read<CounterBloc>().add(IncrementEvent());
            },
            child: Icon(Icons.add),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () {
              context.read<CounterBloc>().add(DecrementEvent());
            },
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
回到顶部