LogoSkills

figma-to-coui Templates

Ready-to-use code templates that can be copied directly (kobic Co<X> standard).

Flutter UI Patterns - Templates#

๊ธฐ์ค€: coui v0.97.15 (latest). kobic vendored๋Š” v0.92.0์ด๋ผ gap/input ๋“ฑ ์ผ๋ถ€ API๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ.

Ready-to-use code templates that can be copied directly (kobic Co standard). Customization points are marked with comments.

All components are Co<X> (CoButton / CoBadge / CoIcon / CoGap / CoInput), spacing uses CoGap.spaceN() token constructors, and text styling uses context.textStyles. Import CoUI types through package:core/core.dart.


1. Basic Page Template#

BLoC Page#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] Page description
class MyPage extends HookWidget {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      // [TODO] Change BLoC type
      create: (_) => MyBloc()..add(const MyEvent.started()),
      child: Scaffold(
        headers: [
          AppBar(
            // [TODO] Change page title
            title: Text('ํŽ˜์ด์ง€ ์ œ๋ชฉ'),
            leading: [
              CoButton(
                variant: .ghost,
                shape: .square,
                onPressed: () => context.pop(),
                child: const CoIcon(CoLucideIcons.arrowLeft),
              ),
            ],
            // [TODO] Add trailing actions if needed
            trailing: const [],
          ),
        ],
        child: const _Body(),
      ),
    );
  }
}

class _Body extends StatelessWidget {
  const _Body();

  @override
  Widget build(BuildContext context) {
    final colorScheme = context.theme.colorScheme;

    return BlocBuilder<MyBloc, MyState>(
      builder: (context, state) {
        // [TODO] UI branching by state
        return switch (state.status) {
          LoadingStatus.initial || LoadingStatus.loading =>
            const Center(child: CircularProgressIndicator()),
          LoadingStatus.failure => _ErrorView(message: state.errorMessage),
          LoadingStatus.success => _SuccessView(data: state.data),
        };
      },
    );
  }
}

class _SuccessView extends StatelessWidget {
  const _SuccessView({required this.data});

  final MyData data;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const CoGap.space16(),
          // [TODO] Implement content
          Text('์ปจํ…์ธ ', style: context.textStyles.lgSemibold),
        ],
      ),
    );
  }
}

class _ErrorView extends StatelessWidget {
  const _ErrorView({required this.message});

  final String message;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const CoIcon(
            CoLucideIcons.circleAlert,
            iconStyle: CoreIconStyle(size: 48),
          ),
          const CoGap.space16(),
          Text(message, style: context.textStyles.baseNormal),
          const CoGap.space16(),
          CoButton(
            onPressed: () {
              context.read<MyBloc>().add(const MyEvent.retried());
            },
            child: const Text('๋‹ค์‹œ ์‹œ๋„'),
          ),
        ],
      ),
    );
  }
}

Simple Page (Local State Only)#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] Page description
class SimplePage extends HookWidget {
  const SimplePage({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = context.theme.colorScheme;
    // [TODO] Define local state
    final selectedIndex = useState(0);
    final isExpanded = useState(false);

    return Scaffold(
      headers: [
        AppBar(
          title: Text('ํŽ˜์ด์ง€ ์ œ๋ชฉ'),
          leading: [
            CoButton(
              variant: .ghost,
              shape: .square,
              onPressed: () => context.pop(),
              child: const CoIcon(CoLucideIcons.arrowLeft),
            ),
          ],
        ),
      ],
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
        child: Column(
          children: [
            const CoGap.space16(),
            // [TODO] Implement content
          ],
        ),
      ),
    );
  }
}

2. Form Page Template#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] Form page description
class MyFormPage extends HookWidget {
  const MyFormPage({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = context.theme.colorScheme;
    final formKey = useMemoized(GlobalKey<FormState>.new);

    // [TODO] Define controllers
    final nameController = useTextEditingController();
    final emailController = useTextEditingController();

    final isValid = useState(false);
    final isLoading = useState(false);

    void validate() {
      isValid.value = formKey.currentState?.validate() ?? false;
    }

    Future<void> submit() async {
      if (!formKey.currentState!.validate()) return;

      isLoading.value = true;
      try {
        // [TODO] Implement submit logic
        await Future<void>.delayed(const Duration(seconds: 1));
        if (context.mounted) {
          context.pop();
        }
      } finally {
        isLoading.value = false;
      }
    }

    return Scaffold(
      headers: [
        AppBar(
          title: Text('ํผ ์ œ๋ชฉ'),
          leading: [
            CoButton(
              variant: .ghost,
              shape: .square,
              onPressed: () => context.pop(),
              child: const CoIcon(CoLucideIcons.x),
            ),
          ],
        ),
      ],
      child: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
          child: Form(
            key: formKey,
            onChanged: validate,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const CoGap.space16(),

                // [TODO] Define form fields
                TextFormField(
                  controller: nameController,
                  decoration: const InputDecoration(
                    labelText: '์ด๋ฆ„',
                    hintText: '์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”',
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return '์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”';
                    }
                    return null;
                  },
                ),

                const CoGap.space16(),

                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 Spacer(),

                // ์ œ์ถœ ๋ฒ„ํŠผ
                CoButton(
                  expanded: true,
                  enabled: isValid.value && !isLoading.value,
                  onPressed: submit,
                  child: isLoading.value
                      ? const CircularProgressIndicator()
                      : const Text('์ œ์ถœ'),
                ),

                const CoGap.space16(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

3. List Page Template#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] List page description
class MyListPage extends HookWidget {
  const MyListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => MyListBloc()..add(const MyListEvent.started()),
      child: Scaffold(
        headers: [
          AppBar(
            title: Text('๋ชฉ๋ก'),
            trailing: [
              CoButton(
                variant: .ghost,
                shape: .square,
                onPressed: () => context.push('/create'),
                child: const CoIcon(CoLucideIcons.plus),
              ),
            ],
          ),
        ],
        child: const _Body(),
      ),
    );
  }
}

class _Body extends HookWidget {
  const _Body();

  @override
  Widget build(BuildContext context) {
    final scrollController = useScrollController();

    // Infinite scroll
    useEffect(() {
      void onScroll() {
        if (scrollController.position.pixels >=
            scrollController.position.maxScrollExtent - 200) {
          context.read<MyListBloc>().add(const MyListEvent.loadMore());
        }
      }

      scrollController.addListener(onScroll);
      return () => scrollController.removeListener(onScroll);
    }, []);

    return BlocBuilder<MyListBloc, MyListState>(
      builder: (context, state) {
        if (state.isInitialLoading) {
          return const Center(child: CircularProgressIndicator());
        }

        return RefreshIndicator(
          onRefresh: () async {
            context.read<MyListBloc>().add(const MyListEvent.refreshed());
            await context.read<MyListBloc>().stream.firstWhere(
              (s) => !s.isRefreshing,
            );
          },
          child: ListView.separated(
            controller: scrollController,
            padding: const EdgeInsets.all(CoreSpace.space16),
            itemCount: state.items.length + (state.hasMore ? 1 : 0),
            separatorBuilder: (_, __) => const CoGap.space8(),
            itemBuilder: (context, index) {
              if (index >= state.items.length) {
                return const Center(
                  child: Padding(
                    padding: EdgeInsets.all(CoreSpace.space16),
                    child: CircularProgressIndicator(),
                  ),
                );
              }
              return _ItemCard(item: state.items[index]);
            },
          ).emptyOrWhen(
            condition: () => state.items.isEmpty,
            emptyWidget: const _EmptyView(),
          ),
        );
      },
    );
  }
}

class _ItemCard extends StatelessWidget {
  const _ItemCard({required this.item});

  // [TODO] Change item type
  final MyItem item;

  @override
  Widget build(BuildContext context) {
    final colorScheme = context.theme.colorScheme;

    return CoButton(
      variant: .card,
      onPressed: () => context.push('/detail/${item.id}'),
      child: Padding(
        padding: const EdgeInsets.all(CoreSpace.space16),
        child: Row(
          children: [
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(item.title, style: context.textStyles.baseSemibold),
                  const CoGap.space4(),
                  Text(item.subtitle, style: context.textStyles.smNormal),
                ],
              ),
            ),
            CoIcon(
              CoLucideIcons.chevronRight,
              iconStyle: CoreIconStyle(color: colorScheme.base300.toValue()),
            ),
          ],
        ),
      ),
    );
  }
}

class _EmptyView extends StatelessWidget {
  const _EmptyView();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const CoIcon(CoLucideIcons.inbox, iconStyle: CoreIconStyle(size: 64)),
          const CoGap.space16(),
          Text('ํ•ญ๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค', style: context.textStyles.lgNormal),
          const CoGap.space8(),
          Text('์ƒˆ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”', style: context.textStyles.smNormal),
        ],
      ),
    );
  }
}

4. Tab Page Template#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] Tab page description
class MyTabPage extends HookWidget {
  const MyTabPage({super.key});

  @override
  Widget build(BuildContext context) {
    final selectedTab = useState(0);

    // [TODO] Define tabs
    final tabs = [
      _TabInfo(title: '์ฒซ ๋ฒˆ์งธ', icon: CoLucideIcons.house),
      _TabInfo(title: '๋‘ ๋ฒˆ์งธ', icon: CoLucideIcons.user),
      _TabInfo(title: '์„ธ ๋ฒˆ์งธ', icon: CoLucideIcons.settings),
    ];

    return Scaffold(
      headers: [
        AppBar(
          title: Text('ํƒญ ํŽ˜์ด์ง€'),
        ),
      ],
      child: Column(
        children: [
          // Tab bar
          TabList(
            selectedIndex: selectedTab.value,
            onSelectedIndexChanged: (index) => selectedTab.value = index,
            tabs: tabs.map((tab) => Tab(label: tab.title)).toList(),
          ),

          // Tab content
          Expanded(
            child: IndexedStack(
              index: selectedTab.value,
              children: const [
                _FirstTabContent(),
                _SecondTabContent(),
                _ThirdTabContent(),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _TabInfo {
  const _TabInfo({required this.title, required this.icon});

  final String title;
  final IconName icon;
}

class _FirstTabContent extends StatelessWidget {
  const _FirstTabContent();

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('์ฒซ ๋ฒˆ์งธ ํƒญ ์ปจํ…์ธ '),
    );
  }
}

class _SecondTabContent extends StatelessWidget {
  const _SecondTabContent();

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('๋‘ ๋ฒˆ์งธ ํƒญ ์ปจํ…์ธ '),
    );
  }
}

class _ThirdTabContent extends StatelessWidget {
  const _ThirdTabContent();

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('์„ธ ๋ฒˆ์งธ ํƒญ ์ปจํ…์ธ '),
    );
  }
}

5. Card Widget Template#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] Card description
class MyCard extends StatelessWidget {
  const MyCard({
    required this.title,
    required this.subtitle,
    this.onTap,
    super.key,
  });

  final String title;
  final String subtitle;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final colorScheme = context.theme.colorScheme;

    return CoButton(
      variant: .card,
      onPressed: onTap,
      child: Container(
        padding: const EdgeInsets.all(CoreSpace.space16),
        decoration: BoxDecoration(
          color: colorScheme.base200,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Row(
          children: [
            // Icon area
            Container(
              width: 48,
              height: 48,
              decoration: BoxDecoration(
                color: colorScheme.primary.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(24),
              ),
              child: Center(
                child: CoIcon(
                  CoLucideIcons.star,
                  iconStyle: CoreIconStyle(color: colorScheme.primary.toValue()),
                ),
              ),
            ),

            const CoGap.space12(),

            // Text area
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(title, style: context.textStyles.baseSemibold),
                  const CoGap.space4(),
                  Text(subtitle, style: context.textStyles.smNormal),
                ],
              ),
            ),

            // Arrow
            CoIcon(
              CoLucideIcons.chevronRight,
              iconStyle: CoreIconStyle(
                size: 20,
                color: colorScheme.base300.toValue(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

6. Dialog Template#

Confirmation Dialog#

/// Show confirmation dialog
Future<bool> showConfirmDialog(
  BuildContext context, {
  required String title,
  required String message,
  String confirmText = 'ํ™•์ธ',
  String cancelText = '์ทจ์†Œ',
  bool isDestructive = false,
}) async {
  final result = await showDialog<bool>(
    context: context,
    barrierColor: Colors.black.withValues(alpha: 0.2),
    builder: (context) => AlertDialog(
      title: Text(title),
      content: Text(message),
      actions: [
        CoButton(
          variant: .ghost,
          onPressed: () => Navigator.pop(context, false),
          child: Text(cancelText),
        ),
        CoButton(
          variant: isDestructive ? .destructive : .primary,
          onPressed: () => Navigator.pop(context, true),
          child: Text(confirmText),
        ),
      ],
    ),
  );

  return result ?? false;
}

// Usage example
void onDeleteTap(BuildContext context) async {
  final confirmed = await showConfirmDialog(
    context,
    title: '์‚ญ์ œ ํ™•์ธ',
    message: '์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.',
    confirmText: '์‚ญ์ œ',
    cancelText: '์ทจ์†Œ',
    isDestructive: true,
  );

  if (confirmed && context.mounted) {
    context.read<MyBloc>().add(const MyEvent.deleted());
  }
}

Input Dialog#

/// Show input dialog
Future<String?> showInputDialog(
  BuildContext context, {
  required String title,
  String? initialValue,
  String? placeholder,
  String confirmText = 'ํ™•์ธ',
  String cancelText = '์ทจ์†Œ',
}) async {
  final controller = TextEditingController(text: initialValue);

  final result = await showDialog<String>(
    context: context,
    barrierColor: Colors.black.withValues(alpha: 0.2),
    builder: (context) => AlertDialog(
      title: Text(title),
      content: CoInput(
        controller: controller,
        placeholder: placeholder,
      ),
      actions: [
        CoButton(
          variant: .ghost,
          onPressed: () => Navigator.pop(context),
          child: Text(cancelText),
        ),
        CoButton(
          onPressed: () => Navigator.pop(context, controller.text),
          child: Text(confirmText),
        ),
      ],
    ),
  );

  controller.dispose();
  return result;
}

7. BottomSheet Template#

Option Selection BottomSheet#

/// Show option selection BottomSheet
Future<T?> showOptionsSheet<T>(
  BuildContext context, {
  required String title,
  required List<OptionItem<T>> options,
}) {
  return showModalBottomSheet<T>(
    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,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Handle
            Center(
              child: Container(
                width: 40,
                height: 4,
                decoration: BoxDecoration(
                  color: context.theme.colorScheme.base300,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            const CoGap.space16(),

            // Title
            Text(title, style: context.textStyles.lgSemibold),
            const CoGap.space16(),

            // Option list
            ...options.map(
              (option) => ListTile(
                leading: option.icon != null ? CoIcon(option.icon!) : null,
                title: Text(option.label),
                onTap: () => Navigator.pop(context, option.value),
              ),
            ),

            const CoGap.space8(),
          ],
        ),
      ),
    ),
  );
}

class OptionItem<T> {
  const OptionItem({
    required this.label,
    required this.value,
    this.icon,
  });

  final String label;
  final T value;
  final IconName? icon;
}

// Usage example
void onSortTap(BuildContext context) async {
  final result = await showOptionsSheet<SortOrder>(
    context,
    title: '์ •๋ ฌ ๊ธฐ์ค€',
    options: [
      OptionItem(label: '์ตœ์‹ ์ˆœ', value: SortOrder.newest, icon: CoLucideIcons.arrowDown),
      OptionItem(label: '์˜ค๋ž˜๋œ์ˆœ', value: SortOrder.oldest, icon: CoLucideIcons.arrowUp),
      OptionItem(label: '์ด๋ฆ„์ˆœ', value: SortOrder.name, icon: CoLucideIcons.list),
    ],
  );

  if (result != null && context.mounted) {
    context.read<MyBloc>().add(MyEvent.sorted(result));
  }
}

8. Route Definition Template#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

part 'my_feature_route.g.dart';

/// [TODO] Route description
@TypedGoRoute<MyFeatureRoute>(
  path: '/my-feature',
  routes: [
    TypedGoRoute<MyFeatureDetailRoute>(path: ':id'),
    TypedGoRoute<MyFeatureCreateRoute>(path: 'create'),
  ],
)
class MyFeatureRoute extends GoRouteData {
  const MyFeatureRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return const MyFeaturePage();
  }
}

class MyFeatureDetailRoute extends GoRouteData {
  const MyFeatureDetailRoute({required this.id});

  final String id;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return MyFeatureDetailPage(id: id);
  }
}

class MyFeatureCreateRoute extends GoRouteData {
  const MyFeatureCreateRoute({this.parentId});

  final String? parentId;

  @override
  Widget build(BuildContext context, GoRouterState state) {
    return MyFeatureCreatePage(parentId: parentId);
  }
}

9. BLoC Event/State Template#

Event Definition#

import 'package:dependencies/dependencies.dart';

part 'my_event.freezed.dart';

@freezed
sealed class MyEvent with _$MyEvent {
  const factory MyEvent.started() = _Started;
  const factory MyEvent.refreshed() = _Refreshed;
  const factory MyEvent.loadMore() = _LoadMore;
  const factory MyEvent.itemSelected(String id) = _ItemSelected;
  const factory MyEvent.submitted() = _Submitted;
}

State Definition#

import 'package:dependencies/dependencies.dart';

part 'my_state.freezed.dart';

@freezed
abstract class MyState with _$MyState {
  const factory MyState({
    @Default(LoadingStatus.initial) LoadingStatus status,
    @Default([]) List<MyItem> items,
    @Default(false) bool hasMore,
    @Default('') String errorMessage,
    MyItem? selectedItem,
  }) = _MyState;
}

enum LoadingStatus { initial, loading, success, failure }

extension MyStateX on MyState {
  bool get isInitialLoading => status == LoadingStatus.initial || status == LoadingStatus.loading;
  bool get isRefreshing => status == LoadingStatus.loading && items.isNotEmpty;
  bool get hasError => status == LoadingStatus.failure;
}

BLoC Definition#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';

/// [TODO] BLoC description
class MyBloc extends Bloc<MyEvent, MyState> {
  /// Creates [MyBloc] and sets initial state.
  MyBloc() : super(const MyState()) {
    on<_Started>(_onStarted);
    on<_Refreshed>(_onRefreshed);
    on<_LoadMore>(_onLoadMore);
  }

  Future<void> _onStarted(_Started event, Emitter<MyState> emit) async {
    emit(state.copyWith(status: LoadingStatus.loading));

    // [TODO] Implement data load logic
    try {
      final items = await _fetchItems();
      if (isClosed) return;

      emit(state.copyWith(
        status: LoadingStatus.success,
        items: items,
        hasMore: items.length >= 20,
      ));
    } on Exception catch (e) {
      if (isClosed) return;
      emit(state.copyWith(
        status: LoadingStatus.failure,
        errorMessage: e.toString(),
      ));
    }
  }

  Future<void> _onRefreshed(_Refreshed event, Emitter<MyState> emit) async {
    emit(state.copyWith(status: LoadingStatus.loading));

    try {
      final items = await _fetchItems();
      if (isClosed) return;

      emit(state.copyWith(
        status: LoadingStatus.success,
        items: items,
        hasMore: items.length >= 20,
      ));
    } on Exception {
      if (isClosed) return;
      emit(state.copyWith(status: LoadingStatus.failure));
    }
  }

  Future<void> _onLoadMore(_LoadMore event, Emitter<MyState> emit) async {
    if (!state.hasMore || state.status == LoadingStatus.loading) return;

    try {
      final newItems = await _fetchItems(offset: state.items.length);
      if (isClosed) return;

      emit(state.copyWith(
        items: [...state.items, ...newItems],
        hasMore: newItems.length >= 20,
      ));
    } on Exception {
      // Infinite scroll ์‹คํŒจ๋Š” ๋ฌด์‹œ
    }
  }

  // [TODO] Replace with actual data load logic
  Future<List<MyItem>> _fetchItems({int offset = 0}) async {
    // API call or Repository usage
    return [];
  }
}

Usage#

  1. Select the appropriate template.
  2. Find [TODO] comments and customize for your project.
  3. Change types, names, and logic.
  4. Delete unnecessary parts.