Flutter自定义钩子插件utopia_hooks的使用
Flutter自定义钩子插件utopia_hooks的使用
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 规则
使用钩子很简单,但需要遵循一些规则:
- 钩子必须直接在支持的地方调用(如
HookWidget
的build
方法),或在其他钩子中调用。直接调用意味着它们不能在回调中调用(如ElevatedButton
的onPressed
)。 - 钩子不能在
if
语句或循环中调用。本质上,每次构建时必须以相同的顺序调用相同的钩子集。 - 钩子应该以
use
前缀开头。这是一个约定,使区分钩子和其他函数更容易。 - 钩子应该操作并返回不可变对象。这使代码更容易理解,并防止意外的错误。
组合钩子
钩子是可组合的,这意味着可以从更简单的钩子构建更复杂的钩子。这类似于如何通过组合 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
包包含构建基于钩子的可扩展应用程序架构所需的一切。
本地状态
“本地状态”指的是单个屏幕或小部件的展示逻辑。通常由以下组件组成:
- State 类:包含组件的全部状态和可以对其执行的操作(函数)。
- Hook:返回状态并对其动作做出反应。
- View:根据当前状态显示 UI,并根据用户输入触发动作。
- 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(
// ...
),
);
}
}
示例
- Counter v1 - 简单的计数器示例
- Counter v2 - 使用 Hook-based 架构的计数器示例
- Search - Firestore - 基于 Firebase Firestore 的动态列表和实时更新
- Search - Clean Architecture - 基于 Clean Architecture 的动态列表和搜索
- Form Validation - 复杂表单验证,使用 utopia_validation
完整示例
以下是一个完整的示例,展示了如何使用 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
更多关于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});
}
解释
-
自定义钩子:
useCounter
是一个自定义钩子,它接收一个初始值并返回一个包含当前计数值和一个增加计数值的回调函数的HookResult
对象。 -
使用钩子:在
MyApp
组件中,我们使用useCounter
钩子并获取其值和回调函数。然后我们在UI中使用这些值和函数。 -
HookResult:这是一个简单的类,用于封装返回值和回调函数。你可以根据你的需求来扩展这个类。
注意事项
- 自定义钩子的创建和使用需要你对Flutter Hooks有一定的了解。
- 确保你的
utopia_hooks
插件与Flutter Hooks库兼容。 - 在真实项目中,你可能需要更复杂的钩子逻辑和错误处理。
这个示例展示了如何在Flutter中使用自定义钩子插件utopia_hooks
的基本概念。你可以根据你的实际需求来扩展和修改这个示例。