Flutter UI Patterns - Reference#
๊ธฐ์ค: coui v0.97.15 (latest). kobic vendored๋ v0.92.0์ด๋ผ gap/input ๋ฑ ์ผ๋ถ API๊ฐ ๋ค๋ฅผ ์ ์์.
Provides detailed descriptions, example code, and precautions for each Use Case.
1. Page Structure#
Overview#
All pages follow a consistent structure.
Basic Structure#
class MyPage extends HookWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => MyBloc()..add(const MyEvent.started()),
child: Scaffold(
headers: [
AppBar(
title: Text('ํ์ด์ง ์ ๋ชฉ'),
leading: [
CoButton(
variant: .ghost,
shape: .square,
onPressed: () => context.pop(),
child: const CoIcon(CoLucideIcons.arrowLeft),
),
],
trailing: [
CoButton(
variant: .ghost,
shape: .square,
onPressed: onSettings,
child: const CoIcon(CoLucideIcons.settings),
),
],
),
],
child: const _Body(),
),
);
}
}
class _Body extends StatelessWidget {
const _Body();
@override
Widget build(BuildContext context) {
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
child: Column(
children: [
// ์ปจํ
์ธ
],
),
);
},
);
}
}
Pattern Selection Criteria#
| Pattern | When to Use | Example |
|---|---|---|
| HookWidget + BlocProvider | Async data loading | Details Page, List |
| HookWidget only | Only local state needed | Settings Page, Form |
| StatelessWidget | No state | Static info Page |
Precautions#
// โ
CORRECT: BlocProvider๋ ํ์ด์ง ๋ ๋ฒจ์์ ์์ฑ
class MyPage extends HookWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => MyBloc(),
child: _Body(),
);
}
}
// โ WRONG: build ๋ฉ์๋ ๋ด์์ ๋งค๋ฒ ์์ฑ ๊ธ์ง
class MyPage extends HookWidget {
@override
Widget build(BuildContext context) {
final bloc = MyBloc(); // ๋งค๋ฒ ์๋ก ์์ฑ๋จ!
return BlocProvider.value(
value: bloc,
child: _Body(),
);
}
}
2. State Management#
BLoC Pattern Overview#
User Action โ Event โ BLoC โ State โ UI Update
BlocBuilder#
Used for UI rendering based on state.
BlocBuilder<MyBloc, MyState>(
// Optional: specify rebuild conditions
buildWhen: (previous, current) => previous.items != current.items,
builder: (context, state) {
// State branching with switch expression (recommended)
return switch (state) {
MyStateInitial() => const SizedBox.shrink(),
MyStateLoading() => const LoadingIndicator(),
MyStateSuccess(:final data) => SuccessWidget(data: data),
MyStateFailure(:final error) => ErrorWidget(message: error),
};
},
)
BlocConsumer#
Used when both UI rendering and side effects are needed.
BlocConsumer<AuthBloc, AuthState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
// Side effects: navigation, snackbar, dialog
if (state.isAuthenticated) {
context.go('/home');
}
if (state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
}
},
buildWhen: (previous, current) => previous.formState != current.formState,
builder: (context, state) {
return LoginForm(
isLoading: state.isLoading,
isValid: state.isValid,
);
},
)
BlocListener#
Used when only side effects are needed.
BlocListener<PaymentBloc, PaymentState>(
listenWhen: (previous, current) => current.isCompleted,
listener: (context, state) {
showDialog(
context: context,
builder: (_) => SuccessDialog(),
);
},
child: PaymentForm(),
)
MultiBlocProvider#
Used in pages requiring multiple BLoCs.
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => UserBloc()),
BlocProvider(create: (_) => SettingsBloc()),
BlocProvider(create: (_) => NotificationBloc()),
],
child: SettingsPage(),
)
Event Dispatching#
// โ
CORRECT: context.read ์ฌ์ฉ
CoButton(
onPressed: () {
context.read<MyBloc>().add(const MyEvent.submitted());
},
child: const Text('์ ์ถ'),
)
// โ WRONG: context.watch๋ก ์ด๋ฒคํธ ๋ฐ์ก ๊ธ์ง
CoButton(
onPressed: () {
context.watch<MyBloc>().add(...); // ๋ถํ์ํ ๋ฆฌ๋น๋ ๋ฐ์
},
)
3. Local State (Hooks)#
Basic Hooks#
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
// useState - ๋จ์ ์ํ
final counter = useState(0);
final isVisible = useState(true);
// useTextEditingController - ํ
์คํธ ์
๋ ฅ
final controller = useTextEditingController();
// useEffect - ์ฌ์ด๋ ์ดํํธ
useEffect(() {
final subscription = stream.listen((value) {
counter.value = value;
});
return subscription.cancel; // cleanup
}, []); // ๋น ๋ฐฐ์ด = componentDidMount
return Column(
children: [
Text('Count: ${counter.value}'),
CoInput(controller: controller),
Switch(
value: isVisible.value,
onChanged: (v) => isVisible.value = v,
),
],
);
}
}
Animation Hooks#
class AnimatedWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(milliseconds: 300),
);
final animation = useAnimation(
CurvedAnimation(
parent: animationController,
curve: Curves.easeInOut,
),
);
return FadeTransition(
opacity: animation,
child: Content(),
);
}
}
useMemoized#
Caches expensive computation results.
final expensiveResult = useMemoized(
() => computeExpensiveValue(items),
[items], // ์์กด์ฑ ๋ณ๊ฒฝ ์ ์ฌ๊ณ์ฐ
);
useCallback#
Memoizes callback functions.
final onSubmit = useCallback(() {
context.read<MyBloc>().add(const MyEvent.submitted());
}, []);
4. Typography#
context.textStyles Pattern#
This project's standard text styling approach. context.textStyles returns the
CoUI Typography from the current theme; pick a <size><weight>
getter and pass
it to Text(..., style:).
// ํฌ๊ธฐ + ๊ตต๊ธฐ ๊ฒฐํฉ getter (์ด๋ฆ์ด ๊ณง ์คํ์ผ)
Text('ํ
์คํธ', style: context.textStyles.xsSemibold) // 12px / 600
Text('ํ
์คํธ', style: context.textStyles.smNormal) // 14px / 400
Text('ํ
์คํธ', style: context.textStyles.baseNormal) // 16px / 400
Text('ํ
์คํธ', style: context.textStyles.lgSemibold) // 18px / 600
Text('ํ
์คํธ', style: context.textStyles.xlBold) // 20px / 700
// ์์ฃผ ์ฐ๋ ์กฐํฉ
Text('์ ๋ชฉ', style: context.textStyles.lgSemibold)
Text('๋ถ์ ๋ชฉ', style: context.textStyles.baseSemibold)
Text('๋ณธ๋ฌธ', style: context.textStyles.smNormal)
Color Composition (copyWith)#
Apply color via copyWith on the style getter โ there is no color chaining.
Text(
'์ ๋ชฉ',
style: context.textStyles.lgSemibold.copyWith(
color: context.appColors.neutral5,
),
)
Text(
'์บก์
',
style: context.textStyles.smNormal.copyWith(
color: context.appColors.neutral60,
),
)
Precautions#
// โ WRONG: TextStyle ์ง์ ์ ์ ๊ธ์ง
Text('ํ
์คํธ', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))
// โ WRONG: ์ ๊ฑฐ๋ ํจํด
Text('ํ
์คํธ').lg.bold // chaining ์ ๊ฑฐ๋จ
context.theme.typography.lg // context.theme.typography ๊ธ์ง
// โ
CORRECT: context.textStyles getter ์ฌ์ฉ
Text('ํ
์คํธ', style: context.textStyles.lgSemibold)
5. Buttons#
Button Variants (CoButton)#
A single CoButton widget selects its look via the variant: enum. Pass the
variant with dot-shorthand (variant: .ghost). Default is
variant: .primary, size: .md.
// Primary - ์ฃผ์ ์ก์
(์ ์ถ, ํ์ธ) โ variant defaults to primary
CoButton(
onPressed: onSubmit,
expanded: true, // ์ ์ฒด ๋๋น
enabled: isValid, // ํ์ฑํ ์ํ
child: const Text('ํ์ธ'),
)
// Ghost - ๋ณด์กฐ ์ก์
(์ทจ์, ๋ค๋ก)
CoButton(
variant: .ghost,
onPressed: onCancel,
child: const Text('์ทจ์'),
)
// Card - ์นด๋ ํํ ๋ฒํผ
CoButton(
variant: .card,
onPressed: onTap,
child: const Column(
children: [
CoIcon(CoLucideIcons.settings),
Text('์ค์ '),
],
),
)
// Outline - ํ
๋๋ฆฌ ๋ฒํผ
CoButton(
variant: .outline,
onPressed: onTap,
child: const Text('์์ธํ ๋ณด๊ธฐ'),
)
// Destructive - ์ํ ์ก์
CoButton(
variant: .destructive,
onPressed: onDelete,
child: const Text('์ญ์ '),
)
CoreButtonVariant: primary ยท secondary ยท outline ยท
ghost ยท link ยท
text ยท plain ยท destructive ยท card
ยท menu ยท menubar ยท fixed
Icon-only Buttons#
There is no separate IconButton. Use CoButton with shape: .square
(or
.circle) and a CoIcon child.
CoButton(
variant: .ghost,
shape: .square,
onPressed: onEdit,
child: const CoIcon(CoLucideIcons.pencil),
)
CoButton(
shape: .circle,
onPressed: onAdd,
child: const CoIcon(CoLucideIcons.plus),
)
CoLucideIconsonly covers the standard icon set. For a brand/custom icon with no Lucide equivalent, use the project's generatedAssets.svg.*accessor instead โ see SKILL.md ยง 5.5 Icons.
Button Size#
Size is the size: enum (CoreComponentSize): xs ยท sm
ยท md (default) ยท
lg ยท xl. There is no ButtonSize / ButtonDensity.
CoButton(
size: .sm,
onPressed: onTap,
child: const Text('์์ ๋ฒํผ'),
)
Precautions#
// โ WRONG: ์ ๊ฑฐ๋ named-constructor / ButtonStyle ํจํด
Button.primary(onPressed: onTap, child: Text('ํ์ธ'))
ButtonStyle.ghost()
Button.ghost(style: const ButtonStyle.ghost(), ...)
// โ
CORRECT: CoButton + variant enum, chrome ์ buttonStyle ๋ก
CoButton(variant: .ghost, onPressed: onTap, child: const Text('์ทจ์'))
CoButton(
buttonStyle: CoreButtonStyle(width: 200),
onPressed: onTap,
child: const Text('๊ณ ์ ํญ'),
)
6. Forms#
Basic Form Structure#
class LoginForm extends HookWidget {
@override
Widget build(BuildContext context) {
final formKey = useMemoized(GlobalKey<FormState>.new);
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
final isValid = useState(false);
void validate() {
isValid.value = formKey.currentState?.validate() ?? false;
}
return Form(
key: formKey,
onChanged: validate,
child: Column(
children: [
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: '์ด๋ฉ์ผ',
hintText: '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์',
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์';
}
if (!value.contains('@')) {
return '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค';
}
return null;
},
),
const CoGap.space16(),
TextFormField(
controller: passwordController,
decoration: const InputDecoration(
labelText: '๋น๋ฐ๋ฒํธ',
hintText: '๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์',
),
obscureText: true,
validator: (value) {
if (value == null || value.length < 8) {
return '8์ ์ด์ ์
๋ ฅํ์ธ์';
}
return null;
},
),
const CoGap.space24(),
CoButton(
expanded: true,
enabled: isValid.value,
onPressed: () {
if (formKey.currentState!.validate()) {
// ์ ์ถ ๋ก์ง
}
},
child: const Text('๋ก๊ทธ์ธ'),
),
],
),
);
}
}
FormTextFielddoes not exist. UseCoInput(bare text-entry primitive) or the CoUICoTextFieldcomponent (labelled field with slots). For Material-form validation you may still useForm+TextFormField.
CoInput / CoTextField Properties#
// CoInput - bare text-entry primitive (no label / leading / trailing slots)
CoInput(
type: CoreInputType.text, // text/password/email/number/tel/url/search
placeholder: 'ํ๋ ์ด์คํ๋',
value: value,
enabled: true,
required: false,
minLength: 3,
maxLength: 50,
onChanged: (value) {},
onSubmitted: (value) {},
)
// ์๋ฌ variant + chrome ์ค๋ฒ๋ผ์ด๋๋ inputStyle ์ฌ๋กฏ (size:/shape: ํ๋ผ๋ฏธํฐ ์์)
CoInput(
type: CoreInputType.email,
placeholder: '์ด๋ฉ์ผ',
variant: CoreInputVariant.error,
inputStyle: CoreInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
),
)
// CoTextField - label / placeholder / prefix / suffix / errorText ์ฌ๋กฏ
CoTextField(
label: '์ด๋ฉ์ผ',
placeholder: '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์',
controller: controller,
keyboardType: TextInputType.emailAddress,
prefix: const CoIcon(CoLucideIcons.mail),
errorText: emailError, // null ์ด๋ฉด ์๋ฌ ๋ฏธํ์
onChanged: (value) {},
)
Validation State Visualization#
enum ValidationState { pending, valid, invalid }
Widget buildValidationIcon(BuildContext context, ValidationState state) {
final colorScheme = context.theme.colorScheme;
return switch (state) {
ValidationState.pending => CoIcon(
CoLucideIcons.circle,
iconStyle: CoreIconStyle(color: colorScheme.baseContent.toValue()),
),
ValidationState.valid => CoIcon(
CoLucideIcons.circleCheck,
iconStyle: CoreIconStyle(color: colorScheme.success.toValue()),
),
ValidationState.invalid => CoIcon(
CoLucideIcons.circleAlert,
iconStyle: CoreIconStyle(color: colorScheme.error.toValue()),
),
};
}
7. Loading States#
Skeletonizer#
Converts widget to skeleton loading state.
BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return MyCard(
title: state.title ?? 'Loading Title',
subtitle: state.subtitle ?? 'Loading Subtitle',
).skeletonizer(enabled: state.isLoading);
},
)
loadingOr Extension#
Displays conditional loading widget.
// default ์ฌ์ฉ
ContentWidget().loadingOr(
isLoading: isLoading,
loadingWidget: const CircularProgressIndicator(),
)
// Mock ๋ฐ์ดํฐ๋ก ์ค์ผ๋ ํค
ContentWidget().loadingOrWithMock(
isLoading: isLoading,
mockWidget: () => ContentWidget(data: MockData()),
)
emptyOrWhen Extension#
Used for empty state handling.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemCard(item: items[index]),
).emptyOrWhen(
condition: () => items.isEmpty,
emptyWidget: const EmptyStateWidget(
icon: CoIcon(CoLucideIcons.inbox),
title: 'ํญ๋ชฉ์ด ์์ต๋๋ค',
subtitle: '์ ํญ๋ชฉ์ ์ถ๊ฐํด๋ณด์ธ์',
),
)
State-based UI Branching#
BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return switch (state.status) {
LoadingStatus.initial => const SizedBox.shrink(),
LoadingStatus.loading => const Center(
child: CircularProgressIndicator(),
),
LoadingStatus.success => SuccessWidget(data: state.data),
LoadingStatus.failure => ErrorWidget(
message: state.errorMessage,
onRetry: () => context.read<MyBloc>().add(const MyEvent.retried()),
),
};
},
)
8. Color System#
ColorScheme Access#
final colorScheme = context.theme.colorScheme;
Semantic Colors#
| Token | Purpose | Example |
|---|---|---|
primary | Primary actions, brand | Buttons, links |
primaryContent | Text on primary | Button text |
success | Success state | Completion, approval |
error | Error state | Failure, error |
warning | Warning state | Caution, alerts |
info | Information display | Guidance, tips |
Base Colors#
| Token | Purpose | Example |
|---|---|---|
base100 | Default background | Page background |
base200 | Card background | Cards, containers |
base300 | Divider lines, borders | Dividers |
baseContent | Default text | Titles, body text |
neutral | Neutral elements | Disabled buttons |
Usage Examples#
Container(
color: colorScheme.base100,
child: Column(
children: [
Container(
color: colorScheme.base200,
child: Text('์นด๋ ๋ด์ฉ').baseContent,
),
Divider(color: colorScheme.base300),
Container(
color: colorScheme.success,
child: Text('์ฑ๊ณต!').successContent,
),
],
),
)
Applying Opacity#
// โ
CORRECT: withValues ์ฌ์ฉ
colorScheme.baseContent.withValues(alpha: 0.5)
Colors.black.withValues(alpha: 0.2)
// โ WRONG: withOpacity ์ฌ์ฉ (deprecated)
colorScheme.baseContent.withOpacity(0.5)
9. Spacing#
CoGap Widget#
CoGap.spaceN() token constructors โ the token name equals its pixel value.
For non-token sizes / cross extent / fill color use gapStyle: CoreGapStyle(...).
const CoGap.space4() // 4px
const CoGap.space8() // 8px
const CoGap.space12() // 12px
const CoGap.space16() // 16px
const CoGap.space20() // 20px
const CoGap.space24() // 24px
const CoGap.space32() // 32px
// ๋นํ ํฐ ํฌ๊ธฐ / cross extent / fill color
const CoGap(gapStyle: CoreGapStyle(size: 18))
Removed/legacy:
Gap.s1(),Gap(Spacing.s4),Spacing.s*,Insets.*,CoGap(size: CoreSpace.spaceN)(v0.92 โ useCoGap.spaceN()).
CoreSpace in EdgeInsets#
const EdgeInsets.all(CoreSpace.space16)
const EdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
)
const EdgeInsets.only(
top: CoreSpace.space16,
bottom: CoreSpace.space8,
left: CoreSpace.space16,
right: CoreSpace.space16,
)
Consistent Spacing Usage#
Column(
children: [
Header(),
const CoGap.space16(), // ํค๋ ์๋ 16px
ContentSection(),
const CoGap.space24(), // ์น์
๊ฐ 24px
Footer(),
],
)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: CoreSpace.space16, // ์ข์ฐ 16px
vertical: CoreSpace.space8, // ์ํ 8px
),
child: Content(),
)
10. Overlays#
Dialog#
Future<bool?> showConfirmDialog(BuildContext context) {
return showDialog<bool>(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.2),
builder: (context) => AlertDialog(
title: const Text('์ญ์ ํ์ธ'),
content: const Text('์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?'),
actions: [
CoButton(
variant: .ghost,
onPressed: () => Navigator.pop(context, false),
child: const Text('์ทจ์'),
),
CoButton(
variant: .destructive,
onPressed: () => Navigator.pop(context, true),
child: const Text('์ญ์ '),
),
],
),
);
}
// ์ฌ์ฉ
final result = await showConfirmDialog(context);
if (result == true) {
// ์ญ์ ์ํ
}
BottomSheet#
void showOptionsSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true, // ๋์ด ์กฐ์ ๊ฐ๋ฅ
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(CoreSpace.space16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const CoIcon(CoLucideIcons.pencil),
title: const Text('ํธ์ง'),
onTap: () {
Navigator.pop(context);
// ํธ์ง ๋ก์ง
},
),
ListTile(
leading: const CoIcon(CoLucideIcons.trash),
title: Text(
'์ญ์ ',
style: context.textStyles.baseNormal.copyWith(
color: context.theme.colorScheme.error,
),
),
onTap: () {
Navigator.pop(context);
// ์ญ์ ๋ก์ง
},
),
],
),
),
),
);
}
Popover#
Popover(
positions: [PopoverPosition.bottom],
barrierColor: Colors.transparent,
builder: (context) => Container(
padding: const EdgeInsets.all(CoreSpace.space8),
decoration: BoxDecoration(
color: colorScheme.base200,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
PopoverItem(title: '์ต์
1', onTap: onOption1),
PopoverItem(title: '์ต์
2', onTap: onOption2),
],
),
),
child: CoButton(
variant: .ghost,
shape: .square,
onPressed: null, // Popover๊ฐ ์ฒ๋ฆฌ
child: const CoIcon(CoLucideIcons.ellipsisVertical),
),
)
11. Lists#
Basic ListView#
ListView.separated(
padding: const EdgeInsets.all(CoreSpace.space16),
itemCount: items.length,
separatorBuilder: (_, __) => const CoGap.space8(),
itemBuilder: (context, index) {
final item = items[index];
return ItemCard(item: item);
},
)
RefreshIndicator#
RefreshIndicator(
onRefresh: () async {
context.read<MyBloc>().add(const MyEvent.refreshed());
// BLoC ์ํ๊ฐ ์
๋ฐ์ดํธ๋ ๋๊น์ง ๋๊ธฐ
await context.read<MyBloc>().stream.firstWhere(
(state) => !state.isRefreshing,
);
},
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemCard(item: items[index]),
),
)
CustomScrollView + SliverList#
CustomScrollView(
slivers: [
SliverAppBar(
title: Text('๋ชฉ๋ก'),
floating: true,
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
sliver: SliverList.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const CoGap.space8(),
itemBuilder: (context, index) => ItemCard(item: items[index]),
),
),
],
)
Infinite Scroll (Pagination)#
class InfiniteListWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final scrollController = useScrollController();
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
context.read<MyBloc>().add(const MyEvent.loadMore());
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, []);
return BlocBuilder<MyBloc, MyState>(
builder: (context, state) {
return ListView.builder(
controller: scrollController,
itemCount: state.items.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= state.items.length) {
return const Center(child: CircularProgressIndicator());
}
return ItemCard(item: state.items[index]);
},
);
},
);
}
}
12. Navigation#
GoRouter Setup#
// route ์ ์
@TypedGoRoute<HomeRoute>(
path: '/',
routes: [
TypedGoRoute<ProfileRoute>(path: 'profile/:userId'),
TypedGoRoute<SettingsRoute>(path: 'settings'),
],
)
class HomeRoute extends GoRouteData {
const HomeRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const HomePage();
}
}
class ProfileRoute extends GoRouteData {
const ProfileRoute({required this.userId});
final String userId;
@override
Widget build(BuildContext context, GoRouterState state) {
return ProfilePage(userId: userId);
}
}
Navigation Methods#
// Navigate (replace history)
context.go('/home');
// Push page (keep history)
context.push('/details');
// Go back
context.pop();
// Go back with result
context.pop(result);
// TypedRoute usage (type-safe)
const ProfileRoute(userId: '123').go(context);
const ProfileRoute(userId: '123').push(context);
Receiving Navigation Results#
// Navigating page
CoButton(
onPressed: () async {
final result = await context.push<String>('/select-item');
if (result != null) {
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
}
},
child: const Text('์ ํ'),
)
// Page returning result
CoButton(
onPressed: () => context.pop(selectedItem),
child: const Text('ํ์ธ'),
)
Deep Link Parameters#
@TypedGoRoute<SearchRoute>(path: '/search')
class SearchRoute extends GoRouteData {
const SearchRoute({this.query, this.category});
final String? query;
final String? category;
@override
Widget build(BuildContext context, GoRouterState state) {
return SearchPage(
initialQuery: query,
initialCategory: category,
);
}
}
// Navigate with query parameters
const SearchRoute(query: 'flutter', category: 'tutorial').go(context);
// โ /search?query=flutter&category=tutorial