Flutter自定义Hook管理插件hooked_bloc的使用

发布于 1周前 作者 yibo5220 来自 Flutter

Flutter自定义Hook管理插件hooked_bloc的使用

简介



Build   codecov   pub package   stars   GitHub license  

hooked_bloc 是一个Flutter包,简化了Bloc/Cubit的注入和使用。该库基于React Native中引入的钩子概念,并适应Flutter环境。通过Flutter Hooks,您可以将视图逻辑提取到通用用例中并重用它们,从而加快编写小部件的速度并使其更简单。

目录

动机

在应用程序中使用Bloc/Cubit时,您需要在小部件树中提供对象实例以接收状态。这通常通过BlocBuilderBlocProvider实现,增加了小部件的复杂性。每次使用BlocBuilderBlocListenerBlocSelector时,都会增加代码量和复杂度。借助hooked_bloc,我们可以通过钩子来简化这些操作。

例如,以下代码:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: ...,
    body: BlocProvider<RealLifeCubit>(
      create: (context) => RealLifeCubit()..loadData(),
      child: BlocListener<RealLifeCubit, hooked.BuildState>(
        listenWhen: (_, state) => state is ErrorState,
        listener: (context, state) {
          // Show some view on event
        },
        child: BlocBuilder<RealLifeCubit, hooked.BuildState>(
          buildWhen: (_, state) =>
              [LoadedState, LoadingState, ShowItemState]
                  .contains(state.runtimeType),
          builder: (BuildContext context, hooked.BuildState state) {
            return // Build your widget using `state`
          },
        ),
      ),
    ),
  );
}

可以简化为:

@override
Widget build(BuildContext context) {
  final cubit = useBloc<RealLifeCubit>();

  useBlocListener<RealLifeCubit, BuildState>(cubit, (cubit, value, context) {
    // Show some view on event
  }, listenWhen: (state) => state is ErrorState);

  final state = useBlocBuilder(
    cubit,
    buildWhen: (state) =>
        [LoadedState, LoadingState, ShowItemState].contains(
          state.runtimeType,
        ),
  );

  return // Build your widget using `state`
}

这段代码功能等同于之前的例子,它仍然会在适当的时间重建小部件。寻找合适的Cubit/Bloc并提供当前状态的全部逻辑都隐藏在useBlocuseBlocBuilder钩子中。

安装

安装包

运行命令:

flutter pub add hooked_bloc

或者手动添加依赖项到pubspec.yaml

dependencies:
  # Library already contains flutter_hooks package
  hooked_bloc:

初始化HookedBloc(可选):

void main() async {
  // With GetIt or Injectable
  await configureDependencies();

  runApp(
    HookedBlocConfigProvider(
      injector: () => getIt.get,
      builderCondition: (state) => state != null, // Global build condition
      listenerCondition: (state) => state != null, // Global listen condition
      child: const MyApp(),
    )
  );

  // Or you can omit HookedBlocInjector(...)
  // and allow library to find the cubit in the widget tree
}

然后你可以开始使用钩子编写小部件:

// Remember to inherit from HookWidget
class MyApp extends HookWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // At start obtain a cubit instance
    final cubit = useBloc<CounterCubit>();
    // Then observe state's updates
    // `buildWhen` param will override builderCondition locally
    final state = useBlocBuilder(cubit, buildWhen: (state) => state <= 10);
    // Create a listener for the side-effect
    useBlocListener(cubit, (cubit, value, context) {
      ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Button clicked"),
          ));
    });

    // Build widget's tree without BlocProvider
    return MaterialApp(
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: () => cubit.increment(), // Access cubit in tree
          child: const Icon(Icons.add),
        ),
        // Consume state without BlocBuilder
        body: Center(child: Text("The button has been pressed $state times")),
      ),
    );
  }
}

基础

钩子列表

名称 描述
useBloc 返回所需的Cubit/Bloc
useBlocFactory 通过提供的工厂创建并返回预期的Cubit/Bloc
useBlocBuilder 返回当前的Cubit/Bloc状态,类似于BlocBuilder
useBlocComparativeBuilder 根据比较结果返回当前的Cubit/Bloc状态
useBlocListener 调用回调函数,类似于BlocListener
useBlocComparativeListener 根据状态比较结果调用回调函数
useActionListener 调用回调函数,但独立于Bloc/Cubit状态

useBloc

useBloc钩子尝试通过Cubit提供者找到Cubit,如果未指定则查找小部件树。

@override
Widget build(BuildContext context) {
  // The hook will provide the expected object
  final cubit = useBloc<SimpleCubit>(
    // For default hook automatically closes cubit
    closeOnDispose: true,
  );

  return // Access provided cubit
}

useBlocFactory

useBlocFactory钩子尝试通过提供的注入方法找到工厂并返回由其创建的Cubit。

class SimpleCubitFactory extends BlocFactory<SimpleCubit> {
  bool _value = true;

  @override
  SimpleCubit create() {
    return _value ? SimpleCubitA() : SimpleCubitB();
  }

  void configure(bool value) {
    _value = value;
  }
}

@override
Widget build(BuildContext context) {
  // The hook will provide the expected object
  final cubit = useBlocFactory<SimpleCubit, SimpleCubitFactory>(
    onCubitCreate: (cubitFactory) {
      cubitFactory.configure(false);
    }
  );

  return // Access provided cubit
}

useBlocBuilder

useBlocBuilder钩子在新状态出现时重建小部件。

final CounterCubit cubit = CounterCubit("My cubit");

@override
Widget build(BuildContext context) {
  // The state will be updated along with the widget
  // For default the state will be updated basing on `builderCondition`
  final int state = useBlocBuilder(cubit);

  return // Access provided state
}

useBlocComparativeBuilder

useBlocComparativeBuilder钩子在新状态出现且比较结果为正时重建小部件。

final CounterCubit cubit = CounterCubit("My cubit");

@override
Widget build(BuildContext context) {
  // The state will be updated along with the widget
  // We can compare state's changes to allow rebuild
  final state = useBlocComparativeBuilder(
    cubit,
    buildWhen: (int previous, int current) {
      return current != previous;
    },
  );

  return // Access provided state
}

useBlocListener

useBlocListener钩子允许观察代表动作的Cubit状态(例如显示Snackbar)。

final EventCubit cubit = EventCubit();

@override
Widget build(BuildContext context) {
  // Handle state as event independently of the view state
  useBlocListener(cubit, (_, value, context) {
    _showMessage(context, (value as ShowMessage).message);
  }, listenWhen: (state) => state is ShowMessage);

  return // Build your widget
}

useBlocComparativeListener

useBlocComparativeListener钩子允许观察并比较代表动作的Cubit状态(例如显示Snackbar)。

final EventCubit cubit = EventCubit();

@override
Widget build(BuildContext context) {
  // Handle state as event independently of the view state
  // We can compare state changes to allow listener function to be called
  useBlocComparativeListener(
    cubit,
    (_, value, context) {
       _showMessage(context, (value as ShowMessage).message);
    },
    listenWhen: (previousState, currentState) => previousState is! ShowMessage && currentState is ShowMessage,
  );

  return // Build your widget
}

useActionListener

useActionListener钩子与useBlocListener类似,但监听不同于状态流的流,并可用于需要不同通知流程的动作。

class MessageActionCubit extends EventCubit with BlocActionMixin<String, BuildState> {
  @override
  void dispatch(String action) {
    super.dispatch(action);
  }
}

@override
Widget build(BuildContext context) {
  // Handle separate action stream with values other than a state type
  useActionListener(
    cubit, 
    (String action) {
      _showMessage(context, action);
    },
    actionWhen: (previousAction, action) => true,
  );

  return // Build your widget
}

贡献

欢迎对项目进行任何贡献!

  • 提交新的功能请求或修复建议应通过pull-request或issue。
  • 检查是否已有相同的功能请求或修复建议。
  • 描述为什么需要此功能或描述问题所在。
  • 编写测试用例。
  • 更新README文档。

更多详情请参考贡献指南

示例代码

以下是hooked_bloc的一个完整示例demo:

import 'package:example/di/injector.dart';
import 'package:example/page/home_page.dart';
import 'package:flutter/material.dart';
import 'package:hooked_bloc/hooked_bloc.dart';

void main() async {
  // With GetIt or Injectable
  await configureDependencies();

  runApp(
    HookedBlocConfigProvider(
      injector: () => getIt.get,
      builderCondition: (state) => state != null, // Global build condition
      listenerCondition: (state) => state != null, // Global listen condition
      child: const MyApp(),
    ),
  );
}

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

  final appName = "Hooked Bloc";

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appName,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: HomePage(title: appName),
    );
  }
}

// Quickstart example counter page
class CounterPage extends HookWidget {
  const CounterPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // At start obtain a cubit instance
    final cubit = useBloc<CounterCubit>();
    // Then observe state's updates
    //`buildWhen` param will override builderCondition locally
    final state = useBlocBuilder(cubit, buildWhen: (state) => state <= 10);
    // Create a listener for the side-effect
    useBlocListener(cubit, (cubit, value, context) {
      ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Button clicked")));
    });

    // Build widget's tree without BlocProvider
    return Scaffold(
      appBar: AppBar(title: Text("Counter Page")),
      floatingActionButton: FloatingActionButton(
        onPressed: () => cubit.increment(), // Access cubit in tree
        child: const Icon(Icons.add),
      ),
      // Consume state without BlocBuilder
      body: Center(child: Text("The button has been pressed $state times")),
    );
  }
}

这个示例展示了如何使用hooked_bloc中的各种钩子来简化状态管理和UI更新。希望这能帮助你更好地理解和使用hooked_bloc


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

1 回复

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


当然,以下是一个关于如何在Flutter中使用hooked_bloc插件来管理自定义Hook的示例代码。hooked_bloc是一个用于在Flutter应用中创建和使用Hook的库,它类似于React中的Hooks,但适用于Flutter。

首先,确保你已经在pubspec.yaml文件中添加了hooked_bloc依赖:

dependencies:
  flutter:
    sdk: flutter
  hooked_bloc: ^最新版本号  # 替换为实际版本号

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

1. 创建一个自定义Hook

让我们创建一个简单的自定义Hook,用于管理一个计数器的状态。

// hooks/use_counter.dart

import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooked_bloc/hooked_bloc.dart';

part 'use_counter.g.dart';  // 确保生成代码的文件被导入

@HookBloc()
class CounterBloc with _$CounterBloc {
  int _count = 0;

  void increment() {
    _count++;
  }

  int get count => _count;
}

Hook useCounter() {
  return useHookedBloc<CounterBloc>(
    () => CounterBloc(),
  );
}

运行以下命令来生成Bloc的模板代码:

flutter pub run build_runner build

2. 使用自定义Hook

现在,我们可以在一个Flutter Widget中使用这个自定义Hook。

// main.dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'hooks/use_counter.dart';

void main() {
  runApp(HookedBlocProvider(
    child: MyApp(),
  ));
}

class MyApp extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final counterBloc = useCounter();

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Counter Hook Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${counterBloc.state.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            counterBloc.context.read<CounterBloc>().increment();
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

3. 完整项目结构

确保你的项目结构类似于以下:

your_flutter_project/
├── android/
├── ios/
├── lib/
│   ├── hooks/
│   │   ├── use_counter.dart
│   │   └── use_counter.g.dart (自动生成)
│   ├── main.dart
│   └── pubspec.yaml
└── ...

总结

上述代码展示了如何在Flutter中使用hooked_bloc来创建一个自定义Hook,并在Widget中使用它。这个示例中,我们创建了一个简单的计数器Hook,并在UI中展示了计数器的值,同时提供了一个按钮来增加计数器的值。你可以根据需要扩展这个示例,添加更多的逻辑和功能。

回到顶部