深入理解組合式 API
在「快速上手」中,您看到了 CompositionWidget 的基本用法。現在,讓我們深入探討其背後的強大功能:屬性 (Props)、生命週期和依賴注入。
setup() 的黃金法則
setup() 方法是 CompositionWidget 的心臟,但請務必記住這條黃金法則:
setup()只會在 Widget 的生命週期中執行一次(相當於StatefulWidget的initState)。
這意味著您可以在此處安全地初始化狀態、建立控制器、註冊監聽器,而無需擔心它們會在每次 Widget 重建時被重新建立。
相對地,從 setup() 返回的 builder 函式則會在它所依賴的任何響應式數據發生變化時被重新執行。
響應式屬性 (Reactive Props)
如果 setup 只執行一次,我們要如何響應來自父 Widget 的屬性變化呢?直接在 setup 中存取 widget.myProp 是無效的,因為它只會讀取到初始值。
正確的答案是使用 widget() API。
widget() 函式會返回一個響應式的 ComputedRef,它總是代表著最新的 Widget 實例。當父 Widget 重建並傳入新的屬性時,這個 ComputedRef 會觸發更新。
讓我們來看一個 UserCard 範例:
class UserCard extends CompositionWidget {
const UserCard({super.key, required this.name});
final String name;
@override
Widget Function(BuildContext) setup() {
// ✅ 正確:使用 widget() 獲取響應式的屬性參考
final props = widget<UserCard>();
// ❌ 錯誤:直接存取 this.name 或 name,這不是響應式的!
// final greeting = computed(() => 'Hello, $name');
// `greeting` 會在 `props.value.name` 變化時自動更新
final greeting = computed(() => 'Hello, ${props.value.name}!');
// 監聽 name 屬性的變化
watch(() => props.value.name, (newName, oldName) {
print('Name changed from $oldName to $newName');
});
return (context) => Text(greeting.value);
}
}重點: 始終透過 widget<YourWidget>().value.yourProp 來存取屬性,以確保您的 computed 和 watch 能夠正確地響應變化。
生命週期鉤子 (Lifecycle Hooks)
flutter_compositions 提供了類似 Vue 的生命週期鉤子,讓您可以在 setup 內部掛載和卸載邏輯。
onMounted(callback): 在 Widget 被掛載到畫面上後(initState之後的第一幀)執行。適合用來發送網路請求、初始化需要BuildContext的控制器等。onUnmounted(callback): 在 Widget 被銷毀前(dispose期間)執行。這是清理控制器、取消訂閱、釋放資源的最佳位置。
@override
Widget Function(BuildContext) setup() {
final (animationController, progress) = useAnimationController(
duration: const Duration(milliseconds: 300),
);
onMounted(() {
print('Widget is mounted!');
animationController.forward();
});
onUnmounted(() {
print('Widget is unmounted, cleaning up.');
// `useAnimationController` 會自動 dispose,
// 若您額外建立資源可在此清理
});
return (context) => AnimatedOpacity(
opacity: progress.value,
duration: const Duration(milliseconds: 300),
child: const Placeholder(),
);
}依賴注入 (Provide / Inject)
當您需要在組件樹中向下傳遞數據時,除了透過一層層的建構子傳遞,您還可以使用 provide 和 inject 來實現類似 Provider 套件的依賴注入功能,但它更輕量且型別安全。
provide(key, value): 使用InjectionKey將一個值提供給所有後代CompositionWidget。inject(key): 使用相同的InjectionKey從祖先CompositionWidget中獲取值。
這個機制透過 InjectionKey<T> 作為查找用的 key,泛型會參與相等性比較,確保型別安全並避免不同類型的衝突。
範例:提供一個主題狀態
// 1. 定義一個自訂的資料類別
class AppTheme {
AppTheme(this.mode);
String mode;
}
// 2. 定義一個 injection key 用於型別安全的依賴注入
final themeKey = InjectionKey<Ref<AppTheme>>('app.theme');
// 3. 在父 Widget 中 provide 一個響應式狀態
class ThemeProvider extends CompositionWidget {
@override
Widget Function(BuildContext) setup() {
final theme = ref(AppTheme('light'));
// 使用 InjectionKey 進行 provide - 型別安全!
provide(themeKey, theme);
return (context) => Column(
children: [
// ... 用於切換主題的按鈕 ...
const ThemeDisplay(),
],
);
}
}
// 4. 在子 Widget 中 inject
class ThemeDisplay extends CompositionWidget {
@override
Widget Function(BuildContext) setup() {
// 透過 key 注入,完全型別安全!
final theme = inject(themeKey);
return (context) => Text('Current mode: ${theme.value.mode}');
}
}provide/inject 的一大優勢是它不會引起不必要的 Widget 重建。只有真正 inject 並使用了該響應式值的 builder 才會在值變化時更新。