Flutter自定义钩子插件utopia_hooks的使用

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

Flutter自定义钩子插件utopia_hooks的使用

Logo

Visit hooks.utopiasoft.io

Overview

utopia_hooks 是一个为 Flutter 应用提供全面且灵活的状态管理解决方案的包。它受到 React Hooks 和 Flutter Hooks 的启发,但采用了更整体的方法,允许在各种上下文中使用钩子,覆盖构建完整移动应用架构所需的所有用例,包括不仅限于本地状态、全局状态以及单元测试和集成测试。

Hooks

钩子是表示单一状态(或业务逻辑)的函数。它们返回一个值,该值可以在 UI 或其他钩子中使用,并可以请求重新构建(类似于 StatefulWidget 中的 setState)。

基本钩子

  • useState:表示任何类型的单个可变值(例如开关的当前值)。
  • useEffect:表示在状态变化时发生的副作用(例如当搜索字段内容变化时从互联网获取数据)。效果可以返回一个“销毁”函数,当效果被移除时调用(例如取消网络请求)。

示例

最简单的方式是通过 HookWidget 使用钩子,它类似于 StatelessWidget,但在其 build 方法中可以调用钩子:

class CounterButton extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // 创建一个类型为 `int` 的可变状态,初始值为 0
    final counter = useState(0);

    // 注册一个副作用
    useEffect(() {
      print('Counter changed to ${counter.value}'); // 这将在 `counter` 的值变化时打印
    }, [counter.value]); // “键”是效果的依赖项;当其中任何一个变化时执行。

    // 注册一个一次性副作用
    useEffect(() {
      print('Counter created'); // 这将在小部件创建时打印一次

      // 返回“销毁”函数,当此效果被移除时调用
      return () => print('Counter destroyed'); // 这将在小部件销毁时打印一次
    }); // 没有键相当于空依赖项 - 效果将只运行一次。

    return ElevatedButton(
      child: Text('Counter: ${counter.value}'), // 访问状态的当前值
      onPressed: () => counter.value++, // 在用户交互时更新状态的值
    );
  }
}

Hook 规则

使用钩子很简单,但需要遵循一些规则:

  1. 钩子必须直接在支持的地方调用(如 HookWidgetbuild 方法),或在其他钩子中调用。直接调用意味着它们不能在回调中调用(如 ElevatedButtononPressed)。
  2. 钩子不能在 if 语句或循环中调用。本质上,每次构建时必须以相同的顺序调用相同的钩子集。
  3. 钩子应该以 use 前缀开头。这是一个约定,使区分钩子和其他函数更容易。
  4. 钩子应该操作并返回不可变对象。这使代码更容易理解,并防止意外的错误。

组合钩子

钩子是可组合的,这意味着可以从更简单的钩子构建更复杂的钩子。这类似于如何通过组合 Widget 来创建任意复杂的 UI。以下是从前面的例子中提取的 useCounterState 钩子:

// 不可变对象,表示计数器的状态
class CounterState {
  final int value;
  final void Function() onPressed; // 按钮按下时调用的动作

  const CounterState({required this.value, required this.onPressed});
}

// 返回 `CounterState` 对象的钩子
CounterState useCounterState() {
  final counter = useState(0);

  useEffect(() {
    print('Counter changed to ${counter.value}');
  }, [counter.value]);

  return CounterState(
    value: counter.value,
    onPressed: () => counter.value++,
  );
}

class CounterButton extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final state = useCounterState();

    return ElevatedButton(
      child: Text('Counter: ${state.value}'),
      onPressed: state.onPressed,
    );
  }
}

Hook-based 架构

虽然钩子可以用作 StatefulWidget 的简单替代品,但当用作整个应用程序架构的基础时,它们更加强大。utopia_hooks 包包含构建基于钩子的可扩展应用程序架构所需的一切。

本地状态

“本地状态”指的是单个屏幕或小部件的展示逻辑。通常由以下组件组成:

  1. State 类:包含组件的全部状态和可以对其执行的操作(函数)。
  2. Hook:返回状态并对其动作做出反应。
  3. View:根据当前状态显示 UI,并根据用户输入触发动作。
  4. Coordinator:作为组件的入口点,通过绑定 Hook 和 View 并提供外部功能(如导航)。
// State
class MyScreenState {
  final int someValue;

  // ... 其他状态

  final void Function() onSomethingPressed;

  // ... 其他动作

  const MyScreenState({/* ... */});
}

// Hook
MyScreenState useMyScreenState({required MyScreenArgs args, required void Function() moveToOtherScreen}) {
  // ... 屏幕逻辑

  return MyScreenState({/* ... */});
}

// View
class MyScreenView extends StatelessWidget {
  final MyScreenState state;

  const MyScreenView(this.state);

  @override
  Widget build(BuildContext context) {
    // ...
  }
}

// Coordinator
class MyScreen extends HookWidget {
  const MyScreen();

  @override
  Widget build(BuildContext context) {
    final state = useMyScreenState(
      args: ModalRoute.of(context)!.settings.arguments as MyScreenArgs,
      moveToOtherScreen: () => Navigator.of(context).push(/* ... */),
    );

    return MyScreenView(state);
  }
}

全局状态

“全局状态”指的是在整个应用程序中共享的任何逻辑,例如认证、数据库管理或用户设置。将这些逻辑拆分为更小的部分(“全局状态”),遵循单一职责原则。每个部分都可以由一个独立的钩子表示,然后可以被其他全局或本地状态依赖。

class AuthState {
  final User? user;
  final Future<void> Function(User) logIn;
  final Future<void> Function() logOut;

  const AuthState({/* ... */});

  bool get isLoggedIn => user != null;
}

AuthState useAuthState() {
  final userState = useState<User?>(null);

  Future<void> logIn() async {
    // ...
  }

  Future<void> logOut() async {
    // ...
  }

  return AuthState(user: userState.value, logIn: logIn, logOut: logOut);
}

消费全局状态

使用 useProvided 钩子创建对全局状态的依赖,当任何依赖项发生变化时,钩子会被重新构建。这允许更高层次的全局状态依赖较低层次的全局状态,形成依赖关系的层次结构,本地状态位于底部。

// 全局或本地状态钩子
MyState useMyState() {
  final stateA = useProvided<StateA>();
  final stateB = useProvided<StateB>();

  // ... 其余逻辑
}

注册全局状态

全局状态通常通过将 MaterialApp 包装在 HookProviderContainerWidget 中来注册。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return HookProviderContainerWidget(
      providers: {
        AuthState: useAuthState,
        // ... 其他全局状态
      },
      child: MaterialApp(
        // ...
      ),
    );
  }
}

示例

完整示例

以下是一个完整的示例,展示了如何使用 utopia_hooks 创建一个带有文本输入和计算状态的应用程序:

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

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

class MyApp extends HookWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final fieldState = useState("");

    final computedState = useAutoComputedState<String>(
      debounceDuration: const Duration(seconds: 1),
      keys: [fieldState.value],
      () async {
        debugPrint("Computing at ${DateTime.now().toIso8601String()}");
        await Future<void>.delayed(const Duration(seconds: 5));
        return fieldState.value + DateTime.now().toIso8601String();
      },
    );

    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: Column(
          children: [
            TextEditingControllerWrapper(
              text: fieldState,
              builder: (controller) => TextField(controller: controller),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: Center(
                child: RefreshableComputedStateWrapper<String>(
                  state: computedState,
                  inProgressBuilder: (context) => const Text("InProgress"),
                  failedBuilder: (context) => const Text("Failed"),
                  builder: (context, value) => Text(value),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,我们使用了 useState 来管理文本输入框的状态,并使用 useAutoComputedState 来计算一个延迟的结果。RefreshableComputedStateWrapper 用于显示计算状态的不同状态(进行中、失败、成功)。


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

1 回复

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


当然,以下是如何在Flutter中使用自定义钩子插件utopia_hooks的一个示例。假设你已经安装并配置好了utopia_hooks插件。

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

dependencies:
  flutter:
    sdk: flutter
  utopia_hooks: ^latest_version  # 请替换为实际的最新版本号

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

创建一个自定义钩子

通常,自定义钩子会放在一个单独的文件中,例如hooks/use_counter.dart

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

HookResult<int> useCounter(int initialValue) {
  final count = useState(initialValue);

  final increment = useCallback(() => {
    count.value++;
  }, []);

  return HookResult(value: count.value, callbacks: { 'increment': increment });
}

注意:HookResult是一个自定义类,用于返回值和回调函数。在真实场景中,你可能需要根据你的需求来定义它。在这个例子中,我们简单地使用了一个包含值和回调函数的类。

使用自定义钩子

现在,你可以在你的组件中使用这个自定义钩子。

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

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

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

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Utopia Hooks Example'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${counterHook.value}',
                style: Theme.of(context).textTheme.headline4,
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: counterHook.callbacks['increment'] as VoidCallback,
                child: Text('Increment'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// HookResult class definition (if not defined elsewhere)
class HookResult<T> {
  final T value;
  final Map<String, dynamic> callbacks;

  HookResult({required this.value, required this.callbacks});
}

解释

  1. 自定义钩子useCounter是一个自定义钩子,它接收一个初始值并返回一个包含当前计数值和一个增加计数值的回调函数的HookResult对象。

  2. 使用钩子:在MyApp组件中,我们使用useCounter钩子并获取其值和回调函数。然后我们在UI中使用这些值和函数。

  3. HookResult:这是一个简单的类,用于封装返回值和回调函数。你可以根据你的需求来扩展这个类。

注意事项

  • 自定义钩子的创建和使用需要你对Flutter Hooks有一定的了解。
  • 确保你的utopia_hooks插件与Flutter Hooks库兼容。
  • 在真实项目中,你可能需要更复杂的钩子逻辑和错误处理。

这个示例展示了如何在Flutter中使用自定义钩子插件utopia_hooks的基本概念。你可以根据你的实际需求来扩展和修改这个示例。

回到顶部