ワンクリックで
flutter-hooks
Using flutter_hooks for cleaner state management in iOS Design System widgets
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Using flutter_hooks for cleaner state management in iOS Design System widgets
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
Specialized widgets and themes for Music, Stocks, Weather, App Store, and Books apps
Button widgets in iOS Design System - CupertinoButtonWidget and ButtonWidget with all variants
Guide to creating new widgets following iOS Design System patterns and conventions
Icons, tags, titles, descriptions, dividers, and markdown rendering for iOS Design System
Text fields, switches, pickers, date pickers, and form validation for iOS Design System
ScaffoldWidget, GroupedTableWidget, RowWidget, NavigationBar, and ToolBar for iOS Design System
SOC 職業分類に基づく
| name | flutter-hooks |
| description | Using flutter_hooks for cleaner state management in iOS Design System widgets |
This project includes flutter_hooks: ^0.21.2. Benefits:
Traditional (Verbose):
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(controller: _controller);
}
}
Hooks (Clean):
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useTextEditingController(); // Auto-disposed!
return TextField(controller: controller);
}
}
final count = useState(0);
// Access: count.value
// Update: count.value++
final controller = useTextEditingController();
// Auto-disposed when widget removed
useEffect(() {
// Setup
controller.addListener(listener);
// Cleanup (returned function)
return () => controller.removeListener(listener);
}, [controller]); // Dependencies
final filtered = useMemoized(
() => items.where((item) => item.isActive).toList(),
[items], // Only recomputes when items change
);
final animController = useAnimationController(
duration: Duration(milliseconds: 300),
);
// No vsync needed, auto-disposed!
class ProfileForm extends HookWidget {
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
final nameController = useTextEditingController();
final emailController = useTextEditingController();
final isValid = useState(false);
final isLoading = useState(false);
// Auto-validation
useEffect(() {
void validate() {
isValid.value = nameController.text.isNotEmpty &&
emailController.text.contains('@');
}
nameController.addListener(validate);
emailController.addListener(validate);
return () {
nameController.removeListener(validate);
emailController.removeListener(validate);
};
}, [nameController, emailController]);
return Column(
children: [
GroupedTableWidget(
rows: [
CupertinoTextFieldWidget(
placeholder: 'Name',
controller: nameController,
),
CupertinoTextFieldWidget(
placeholder: 'Email',
controller: emailController,
),
],
),
ButtonWidget.label(
size: const LargeButtonSize(),
color: const BlueButtonColor(),
label: 'Save',
displayCupertinoActivityIndicator: isLoading.value,
onPressed: !isValid.value || isLoading.value
? null
: () async {
isLoading.value = true;
await Future.delayed(Duration(seconds: 2));
isLoading.value = false;
},
),
],
);
}
}
class SearchScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final searchController = useTextEditingController();
final results = useState<List<String>>([]);
final isSearching = useState(false);
useEffect(() {
Timer? debounce;
void search() {
debounce?.cancel();
debounce = Timer(Duration(milliseconds: 500), () async {
if (searchController.text.isEmpty) {
results.value = [];
return;
}
isSearching.value = true;
// Simulate API call
await Future.delayed(Duration(seconds: 1));
results.value = ['Result 1', 'Result 2', 'Result 3'];
isSearching.value = false;
});
}
searchController.addListener(search);
return () {
debounce?.cancel();
searchController.removeListener(search);
};
}, [searchController]);
return Column(
children: [
CupertinoSearchTextFieldWidget(controller: searchController),
if (isSearching.value) CupertinoActivityIndicator(),
Expanded(
child: ListView.builder(
itemCount: results.value.length,
itemBuilder: (context, index) => RowWidget.standard(
title: results.value[index],
onPressed: () {},
),
),
),
],
);
}
}
class ThemeSwitcher extends HookWidget {
@override
Widget build(BuildContext context) {
final isDark = useState(false);
// Memoize theme (only recreates when isDark changes)
final themeData = useMemoized(
() => isDark.value ? IosDarkThemeData() : IosLightThemeData(),
[isDark.value],
);
return IosAnimatedTheme(
data: themeData,
child: ScaffoldWidget(
navigationBar: CupertinoNavigatorBarWidget(title: 'Theme'),
child: SwitchWidget(
value: isDark.value,
onChanged: (value) => isDark.value = value,
),
),
);
}
}
class InfiniteList extends HookWidget {
@override
Widget build(BuildContext context) {
final items = useState<List<String>>([]);
final isLoading = useState(false);
final scrollController = useScrollController();
// Load more when near bottom
useEffect(() {
void checkScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!isLoading.value) {
_loadMore(items, isLoading);
}
}
}
scrollController.addListener(checkScroll);
return () => scrollController.removeListener(checkScroll);
}, [scrollController]);
// Initial load
useEffect(() {
_loadMore(items, isLoading);
return null;
}, []);
return ListView.builder(
controller: scrollController,
itemCount: items.value.length + (isLoading.value ? 1 : 0),
itemBuilder: (context, index) {
if (index >= items.value.length) {
return CupertinoActivityIndicator();
}
return RowWidget.standard(
title: items.value[index],
onPressed: () {},
);
},
);
}
Future<void> _loadMore(
ValueNotifier<List<String>> items,
ValueNotifier<bool> isLoading,
) async {
isLoading.value = true;
await Future.delayed(Duration(seconds: 1));
items.value = [
...items.value,
...List.generate(20, (i) => 'Item ${items.value.length + i}'),
];
isLoading.value = false;
}
}
class TabbedScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final selectedTab = useState(0);
return ScaffoldWidget(
toolBar: ToolBarWidget(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int i = 0; i < 3; i++)
ButtonWidget.label(
size: const MediumButtonSize(),
color: selectedTab.value == i
? const BlueButtonColor()
: const GreyTransparentButtonColor(),
label: 'Tab ${i + 1}',
onPressed: () => selectedTab.value = i,
),
],
),
),
child: _buildContent(selectedTab.value),
);
}
Widget _buildContent(int tab) => Center(
child: Text('Tab $tab Content'),
);
}
extends StatefulWidget → extends HookWidgetState classinitState with useEffect(() { ... return null; }, [])dispose with effect cleanup return () => ...setState with useStateuseTextEditingController(), etc.)flutter_hooksHookWidgetbuild()useEffectuseState<T>(T initialValue)
useTextEditingController({String? text})
useScrollController({double initialOffset})
useAnimationController({Duration? duration})
useMemoized<T>(T Function() compute, [keys])
useEffect(Function() effect, [keys])
useCallback<T>(T Function() callback, [keys])
useFuture<T>(Future<T> future)
useStream<T>(Stream<T> stream)
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:ios_design_system/ios_design_system.dart';