# 코드 템플릿 개발 시간을 단축하고 일관된 코드 스타일을 유지하기 위해 코드 템플릿을 활용하는 것은 매우 유용합니다. 이 페이지에서는 Flutter 개발에 도움이 되는 다양한 코드 템플릿과 예제를 제공합니다. ## 프로젝트 구조 템플릿 ### 기능별 폴더 구조 ``` lib/ ├── core/ │ ├── constants/ │ ├── exceptions/ │ ├── extensions/ │ ├── routes/ │ ├── services/ │ ├── theme/ │ └── utils/ ├── data/ │ ├── datasources/ │ ├── models/ │ └── repositories/ ├── domain/ │ ├── entities/ │ ├── repositories/ (interfaces) │ └── usecases/ ├── presentation/ │ ├── pages/ │ ├── providers/ │ ├── viewmodels/ │ └── widgets/ ├── main.dart └── app.dart ``` ### 간소화된 구조 (소규모 프로젝트) ``` lib/ ├── common/ │ ├── constants.dart │ ├── theme.dart │ └── utils.dart ├── data/ │ ├── models/ │ └── repositories/ ├── providers/ ├── screens/ │ ├── home/ │ ├── details/ │ └── settings/ ├── widgets/ │ ├── common/ │ └── specialized/ ├── main.dart └── app.dart ``` ## 클래스 및 위젯 템플릿 ### StatelessWidget 템플릿 ```dart class CustomWidget extends StatelessWidget { final String title; final VoidCallback onTap; const CustomWidget({ super.key, required this.title, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(8.0), ), child: Text( title, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ); } } ``` ### StatefulWidget 템플릿 ```dart class CounterWidget extends StatefulWidget { final int initialValue; final ValueChanged? onChanged; const CounterWidget({ super.key, this.initialValue = 0, this.onChanged, }); @override State createState() => _CounterWidgetState(); } class _CounterWidgetState extends State { late int _counter; @override void initState() { super.initState(); _counter = widget.initialValue; } void _increment() { setState(() { _counter++; widget.onChanged?.call(_counter); }); } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text('카운터: $_counter'), ElevatedButton( onPressed: _increment, child: const Text('증가'), ), ], ); } } ``` ### HookWidget 템플릿 (flutter_hooks) ```dart class HookCounter extends HookWidget { final ValueChanged? onChanged; const HookCounter({ super.key, this.onChanged, }); @override Widget build(BuildContext context) { final counter = useState(0); void increment() { counter.value++; onChanged?.call(counter.value); } return Column( mainAxisSize: MainAxisSize.min, children: [ Text('카운터: ${counter.value}'), ElevatedButton( onPressed: increment, child: const Text('증가'), ), ], ); } } ``` ### ConsumerWidget 템플릿 (Riverpod) ```dart // 프로바이더 정의 @riverpod class Counter extends _$Counter { @override int build() => 0; void increment() => state++; } // 위젯 구현 class CounterView extends ConsumerWidget { const CounterView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final counter = ref.watch(counterProvider); return Column( mainAxisSize: MainAxisSize.min, children: [ Text('카운터: $counter'), ElevatedButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: const Text('증가'), ), ], ); } } ``` ### HookConsumerWidget 템플릿 (hooks_riverpod) ```dart class HookConsumerCounter extends HookConsumerWidget { const HookConsumerCounter({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final counter = ref.watch(counterProvider); final isActive = useState(true); return Column( mainAxisSize: MainAxisSize.min, children: [ Switch( value: isActive.value, onChanged: (value) => isActive.value = value, ), Text('카운터: $counter'), ElevatedButton( onPressed: isActive.value ? () => ref.read(counterProvider.notifier).increment() : null, child: const Text('증가'), ), ], ); } } ``` ## 데이터 모델 템플릿 ### Freezed 모델 템플릿 ```dart // user.dart import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart'; part 'user.freezed.dart'; part 'user.g.dart'; @freezed class User with _$User { const factory User({ required String id, required String name, @JsonKey(name: 'email_address') required String email, String? profileUrl, @Default(false) bool isPremium, }) = _User; factory User.fromJson(Map json) => _$UserFromJson(json); } ``` ### 상태를 가진 Freezed 모델 (AsyncValue 활용) ```dart // user_state.dart import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'user.dart'; part 'user_state.freezed.dart'; @freezed class UserState with _$UserState { const factory UserState({ required AsyncValue user, @Default(false) bool isEditing, }) = _UserState; const UserState._(); factory UserState.initial() => UserState( user: const AsyncValue.loading(), ); bool get isLoading => user.isLoading; bool get hasError => user.hasError; UserState copyWithUser(User? newUser) { return copyWith( user: newUser != null ? AsyncValue.data(newUser) : const AsyncValue.loading(), ); } } ``` ## Riverpod 프로바이더 템플릿 ### 기본 Provider ```dart final configProvider = Provider((ref) { return AppConfig( apiUrl: 'https://api.example.com', timeout: const Duration(seconds: 30), ); }); ``` ### StateNotifierProvider ```dart // counter_notifier.dart class CounterNotifier extends StateNotifier { CounterNotifier() : super(0); void increment() => state++; void decrement() { if (state > 0) state--; } void reset() => state = 0; } // counter_provider.dart final counterProvider = StateNotifierProvider((ref) { return CounterNotifier(); }); ``` ### AsyncNotifierProvider ```dart // users_notifier.dart @riverpod class UsersNotifier extends _$UsersNotifier { @override FutureOr> build() async { return _fetchUsers(); } Future> _fetchUsers() async { final apiService = ref.read(apiServiceProvider); return await apiService.getUsers(); } Future refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(_fetchUsers); } Future addUser(User user) async { state = const AsyncValue.loading(); final apiService = ref.read(apiServiceProvider); await apiService.addUser(user); state = await AsyncValue.guard(_fetchUsers); } } ``` ### FutureProvider 및 Stream 프로바이더 ```dart // 단순 FutureProvider final userProvider = FutureProvider.family((ref, userId) async { final apiService = ref.read(apiServiceProvider); return await apiService.getUserById(userId); }); // 스트림 프로바이더 final messagesProvider = StreamProvider>((ref) { final chatService = ref.read(chatServiceProvider); return chatService.getMessagesStream(); }); ``` ## 화면 레이아웃 템플릿 ### 기본 화면 구조 ```dart class ProfileScreen extends StatelessWidget { final String userId; const ProfileScreen({ super.key, required this.userId, }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('프로필'), actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () { // 설정 화면으로 이동 }, ), ], ), body: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 프로필 헤더 const ProfileHeader(), const SizedBox(height: 16), // 탭 컨트롤 const ProfileTabBar(), // 탭 콘텐츠 Expanded( child: ProfileTabView(userId: userId), ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { // 작업 수행 }, child: const Icon(Icons.add), ), bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: '홈', ), BottomNavigationBarItem( icon: Icon(Icons.person), label: '프로필', ), BottomNavigationBarItem( icon: Icon(Icons.settings), label: '설정', ), ], currentIndex: 1, onTap: (index) { // 탭 변경 처리 }, ), ); } } ``` ### 반응형 레이아웃 ```dart class ResponsiveLayout extends StatelessWidget { final Widget mobile; final Widget? tablet; final Widget? desktop; const ResponsiveLayout({ super.key, required this.mobile, this.tablet, this.desktop, }); static bool isMobile(BuildContext context) => MediaQuery.of(context).size.width < 650; static bool isTablet(BuildContext context) => MediaQuery.of(context).size.width >= 650 && MediaQuery.of(context).size.width < 1100; static bool isDesktop(BuildContext context) => MediaQuery.of(context).size.width >= 1100; @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; if (size.width >= 1100 && desktop != null) { return desktop!; } if (size.width >= 650 && tablet != null) { return tablet!; } return mobile; } } // 사용 예시 class MyResponsiveScreen extends StatelessWidget { const MyResponsiveScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('반응형 예제')), body: const ResponsiveLayout( mobile: MobileLayout(), tablet: TabletLayout(), desktop: DesktopLayout(), ), ); } } ``` ## 네트워크 요청 템플릿 ### Dio를 활용한 REST API 클라이언트 ```dart class ApiClient { final Dio _dio; ApiClient() : _dio = Dio() { _dio.options.baseUrl = 'https://api.example.com'; _dio.options.connectTimeout = const Duration(seconds: 5); _dio.options.receiveTimeout = const Duration(seconds: 3); _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, )); } Future get( String path, { Map? queryParameters, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.get( path, queryParameters: queryParameters, ); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Future post( String path, { required dynamic data, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.post( path, data: data, ); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Exception _handleError(DioException e) { switch (e.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: return TimeoutException('연결 시간이 초과되었습니다'); case DioExceptionType.badResponse: final statusCode = e.response?.statusCode; if (statusCode == 401) { return UnauthorizedException('인증이 필요합니다'); } else if (statusCode == 404) { return NotFoundException('요청한 리소스를 찾을 수 없습니다'); } return ServerException('서버 오류가 발생했습니다: $statusCode'); default: return Exception('네트워크 오류가 발생했습니다: ${e.message}'); } } } ``` ### GraphQL 요청 템플릿 ```dart class GraphQLClient { final HttpLink _httpLink; late final graphql.GraphQLClient _client; GraphQLClient() : _httpLink = HttpLink('https://api.example.com/graphql') { final AuthLink authLink = AuthLink( getToken: () async => 'Bearer $token', ); final Link link = authLink.concat(_httpLink); _client = graphql.GraphQLClient( link: link, cache: GraphQLCache(), ); } Future query({ required String queryDocument, Map? variables, required T Function(Map data) fromJson, }) async { final QueryOptions options = QueryOptions( document: gql.gql(queryDocument), variables: variables ?? {}, ); final QueryResult result = await _client.query(options); if (result.hasException) { throw _handleException(result.exception!); } return fromJson(result.data!); } Future mutate({ required String mutationDocument, Map? variables, required T Function(Map data) fromJson, }) async { final MutationOptions options = MutationOptions( document: gql.gql(mutationDocument), variables: variables ?? {}, ); final QueryResult result = await _client.mutate(options); if (result.hasException) { throw _handleException(result.exception!); } return fromJson(result.data!); } Exception _handleException(OperationException exception) { if (exception.linkException != null) { return NetworkException('네트워크 오류가 발생했습니다'); } if (exception.graphqlErrors.isNotEmpty) { final firstError = exception.graphqlErrors.first; return GraphQLException(firstError.message); } return Exception('알 수 없는 오류가 발생했습니다'); } } ``` ## 상태 관리 템플릿 ### Riverpod + Freezed를 활용한 상태 관리 ```dart // todo_state.dart @freezed class TodoState with _$TodoState { const factory TodoState({ required AsyncValue> todos, @Default('') String newTodoText, Todo? editingTodo, }) = _TodoState; factory TodoState.initial() => TodoState( todos: const AsyncValue.loading(), ); } // todo_notifier.dart @riverpod class TodoNotifier extends _$TodoNotifier { @override TodoState build() { _loadTodos(); return TodoState.initial(); } Future _loadTodos() async { state = state.copyWith(todos: const AsyncValue.loading()); try { final todoRepository = ref.read(todoRepositoryProvider); final todos = await todoRepository.getTodos(); state = state.copyWith(todos: AsyncValue.data(todos)); } catch (error, stackTrace) { state = state.copyWith(todos: AsyncValue.error(error, stackTrace)); } } void setNewTodoText(String text) { state = state.copyWith(newTodoText: text); } Future addTodo() async { if (state.newTodoText.trim().isEmpty) return; final todo = Todo( id: const Uuid().v4(), title: state.newTodoText, completed: false, ); final currentTodos = state.todos.valueOrNull ?? []; // 낙관적 업데이트 state = state.copyWith( todos: AsyncValue.data([...currentTodos, todo]), newTodoText: '', ); try { final todoRepository = ref.read(todoRepositoryProvider); await todoRepository.addTodo(todo); } catch (error) { // 실패 시 롤백 state = state.copyWith( todos: AsyncValue.data(currentTodos), ); // 오류 메시지 표시 } } } ``` ## 페이지 라우팅 템플릿 ### GoRouter 설정 ```dart // router.dart final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey shellNavigatorKey = GlobalKey(debugLabel: 'shell'); final routerProvider = Provider((ref) { final authState = ref.watch(authStateProvider); return GoRouter( navigatorKey: rootNavigatorKey, initialLocation: '/', debugLogDiagnostics: true, redirect: (context, state) { final isLoggedIn = authState.valueOrNull?.user != null; final isLoggingIn = state.matchedLocation == '/login'; if (!isLoggedIn && !isLoggingIn) return '/login'; if (isLoggedIn && isLoggingIn) return '/'; return null; }, routes: [ // 로그인 화면 GoRoute( path: '/login', builder: (context, state) => const LoginScreen(), ), // 쉘 라우트 (하단 탐색바) ShellRoute( navigatorKey: shellNavigatorKey, builder: (context, state, child) => ShellScreen(child: child), routes: [ // 홈 화면 GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ // 상세 화면 GoRoute( path: 'details/:id', builder: (context, state) { final id = state.params['id']!; return DetailsScreen(id: id); }, ), ], ), // 프로필 화면 GoRoute( path: '/profile', builder: (context, state) => const ProfileScreen(), ), // 설정 화면 GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), ], ), ], errorBuilder: (context, state) => ErrorScreen(error: state.error), ); }); ``` ## 단위 테스트 템플릿 ### 일반 클래스 테스트 ```dart void main() { group('Calculator 테스트', () { late Calculator calculator; setUp(() { calculator = Calculator(); }); test('더하기 테스트', () { expect(calculator.add(1, 2), 3); expect(calculator.add(-1, 1), 0); expect(calculator.add(0, 0), 0); }); test('나누기 테스트', () { expect(calculator.divide(6, 2), 3); expect(calculator.divide(5, 2), 2.5); expect( () => calculator.divide(1, 0), throwsA(isA()), ); }); }); } ``` ### Riverpod 테스트 ```dart // counter_test.dart void main() { group('CounterNotifier 테스트', () { late ProviderContainer container; setUp(() { container = ProviderContainer(); }); tearDown(() { container.dispose(); }); test('초기 상태는 0이다', () { expect(container.read(counterProvider), 0); }); test('increment 메서드는 상태를 1 증가시킨다', () { container.read(counterProvider.notifier).increment(); expect(container.read(counterProvider), 1); container.read(counterProvider.notifier).increment(); expect(container.read(counterProvider), 2); }); test('decrement 메서드는 상태를 1 감소시킨다', () { container.read(counterProvider.notifier).increment(); container.read(counterProvider.notifier).increment(); expect(container.read(counterProvider), 2); container.read(counterProvider.notifier).decrement(); expect(container.read(counterProvider), 1); }); test('decrement 메서드는 상태가 0일 때 감소시키지 않는다', () { expect(container.read(counterProvider), 0); container.read(counterProvider.notifier).decrement(); expect(container.read(counterProvider), 0); }); }); } ``` ## 위젯 테스트 템플릿 ```dart void main() { group('Counter 위젯 테스트', () { testWidgets('초기 카운터 값이 올바르게 표시된다', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: CounterWidget(initialValue: 5), ), ), ); expect(find.text('카운터: 5'), findsOneWidget); expect(find.text('증가'), findsOneWidget); }); testWidgets('버튼 클릭 시 카운터가 증가한다', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: CounterWidget(), ), ), ); expect(find.text('카운터: 0'), findsOneWidget); await tester.tap(find.text('증가')); await tester.pump(); expect(find.text('카운터: 1'), findsOneWidget); }); testWidgets('onChanged 콜백이 호출된다', (WidgetTester tester) async { int? newValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: CounterWidget( onChanged: (value) { newValue = value; }, ), ), ), ); await tester.tap(find.text('증가')); await tester.pump(); expect(newValue, 1); }); }); } ``` ## 예제 모음 링크 다음은 더 많은 코드 예제를 찾을 수 있는 유용한 리소스 모음입니다: ### 공식 예제 - [Flutter Gallery](https://github.com/flutter/gallery) - 공식 Flutter 위젯 및 기능 갤러리 - [Flutter Samples](https://github.com/flutter/samples) - 공식 Flutter 샘플 모음 - [Flutter 쿡북](https://docs.flutter.dev/cookbook) - 공식 Flutter 쿡북 레시피 ### 커뮤니티 예제 - [Flutter Awesome](https://flutterawesome.com/) - 커뮤니티가 제작한 Flutter 앱 예제 모음 - [Flutter Example Apps](https://github.com/iampawan/FlutterExampleApps) - 다양한 Flutter 앱 예제 - [Flutter Clean Architecture](https://github.com/ResoCoder/flutter-clean-architecture-course) - 클린 아키텍처 예제 - [The Flutter Way](https://github.com/abuanwar072) - UI 중심 Flutter 예제 모음 - [Riverpod Architecture](https://github.com/lucavenir/riverpod_architecture) - Riverpod 기반 아키텍처 예제 ### 디자인 별 예제 - [FlutterFolio](https://github.com/gskinnerTeam/flutterfolio) - 반응형 웹 포트폴리오 예제 - [Flutter UI Challenges](https://github.com/lohanidamodar/flutter_ui_challenges) - 다양한 UI 구현 예제 - [Flutter Movies](https://github.com/ibhavikmakwana/flutter_movies) - 영화 앱 UI 예제 - [Flutter Food Delivery](https://github.com/JideGuru/FlutterFoodybite) - 음식 배달 앱 UI ### 특정 기능 구현 예제 - [Local Auth](https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth/example) - 생체 인증 예제 - [Provider Shopper](https://github.com/flutter/samples/tree/main/provider_shopper) - Provider를 활용한 쇼핑 앱 - [Infinite List](https://github.com/felangel/bloc_examples/tree/master/flutter_infinite_list) - 무한 스크롤 구현 - [Firebase Chat](https://github.com/duytq94/flutter-chat-demo) - Firebase 기반 채팅 앱 ### 아키텍처 예제 - [Flutter TDD Clean Architecture](https://github.com/ResoCoder/flutter-tdd-clean-architecture-course) - 테스트 주도 개발 + 클린 아키텍처 - [Flutter BLoC Pattern](https://github.com/felangel/bloc) - BLoC 패턴 예제 - [Flutter Riverpod Architecture](https://github.com/rrousselGit/riverpod/tree/master/examples) - Riverpod 아키텍처 예제 이 리소스들은 다양한 Flutter 프로젝트와 코드 예제를 제공하여 개발자가 더 빠르게 학습하고 개발할 수 있도록 도와줍니다.