코드 템플릿
개발 시간을 단축하고 일관된 코드 스타일을 유지하기 위해 코드 템플릿을 활용하는 것은 매우 유용합니다. 이 페이지에서는 Flutter 개발에 도움이 되는 다양한 코드 템플릿과 예제를 제공합니다.
프로젝트 구조 템플릿
Section titled “프로젝트 구조 템플릿”기능별 폴더 구조
Section titled “기능별 폴더 구조”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
간소화된 구조 (소규모 프로젝트)
Section titled “간소화된 구조 (소규모 프로젝트)”lib/├── common/│ ├── constants.dart│ ├── theme.dart│ └── utils.dart├── data/│ ├── models/│ └── repositories/├── providers/├── screens/│ ├── home/│ ├── details/│ └── settings/├── widgets/│ ├── common/│ └── specialized/├── main.dart└── app.dart
클래스 및 위젯 템플릿
Section titled “클래스 및 위젯 템플릿”StatelessWidget 템플릿
Section titled “StatelessWidget 템플릿”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 템플릿
Section titled “StatefulWidget 템플릿”class CounterWidget extends StatefulWidget { final int initialValue; final ValueChanged<int>? onChanged;
const CounterWidget({ super.key, this.initialValue = 0, this.onChanged, });
@override State<CounterWidget> createState() => _CounterWidgetState();}
class _CounterWidgetState extends State<CounterWidget> { 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)
Section titled “HookWidget 템플릿 (flutter_hooks)”class HookCounter extends HookWidget { final ValueChanged<int>? 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)
Section titled “ConsumerWidget 템플릿 (Riverpod)”// 프로바이더 정의@riverpodclass 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)
Section titled “HookConsumerWidget 템플릿 (hooks_riverpod)”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('증가'), ), ], ); }}
데이터 모델 템플릿
Section titled “데이터 모델 템플릿”Freezed 모델 템플릿
Section titled “Freezed 모델 템플릿”import 'package:freezed_annotation/freezed_annotation.dart';import 'package:json_annotation/json_annotation.dart';
part 'user.freezed.dart';part 'user.g.dart';
@freezedclass 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<String, dynamic> json) => _$UserFromJson(json);}
상태를 가진 Freezed 모델 (AsyncValue 활용)
Section titled “상태를 가진 Freezed 모델 (AsyncValue 활용)”import 'package:freezed_annotation/freezed_annotation.dart';import 'package:flutter_riverpod/flutter_riverpod.dart';import 'user.dart';
part 'user_state.freezed.dart';
@freezedclass UserState with _$UserState { const factory UserState({ required AsyncValue<User> 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 프로바이더 템플릿
Section titled “Riverpod 프로바이더 템플릿”기본 Provider
Section titled “기본 Provider”final configProvider = Provider<AppConfig>((ref) { return AppConfig( apiUrl: 'https://api.example.com', timeout: const Duration(seconds: 30), );});
StateNotifierProvider
Section titled “StateNotifierProvider”class CounterNotifier extends StateNotifier<int> { CounterNotifier() : super(0);
void increment() => state++; void decrement() { if (state > 0) state--; } void reset() => state = 0;}
// counter_provider.dartfinal counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) { return CounterNotifier();});
AsyncNotifierProvider
Section titled “AsyncNotifierProvider”@riverpodclass UsersNotifier extends _$UsersNotifier { @override FutureOr<List<User>> build() async { return _fetchUsers(); }
Future<List<User>> _fetchUsers() async { final apiService = ref.read(apiServiceProvider); return await apiService.getUsers(); }
Future<void> refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(_fetchUsers); }
Future<void> addUser(User user) async { state = const AsyncValue.loading(); final apiService = ref.read(apiServiceProvider); await apiService.addUser(user);
state = await AsyncValue.guard(_fetchUsers); }}
FutureProvider 및 Stream 프로바이더
Section titled “FutureProvider 및 Stream 프로바이더”// 단순 FutureProviderfinal userProvider = FutureProvider.family<User, String>((ref, userId) async { final apiService = ref.read(apiServiceProvider); return await apiService.getUserById(userId);});
// 스트림 프로바이더final messagesProvider = StreamProvider<List<Message>>((ref) { final chatService = ref.read(chatServiceProvider); return chatService.getMessagesStream();});
화면 레이아웃 템플릿
Section titled “화면 레이아웃 템플릿”기본 화면 구조
Section titled “기본 화면 구조”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) { // 탭 변경 처리 }, ), ); }}
반응형 레이아웃
Section titled “반응형 레이아웃”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(), ), ); }}
네트워크 요청 템플릿
Section titled “네트워크 요청 템플릿”Dio를 활용한
Section titled “Dio를 활용한”REST API 클라이언트
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<T> get<T>( String path, { Map<String, dynamic>? 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<T> post<T>( 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 요청 템플릿
Section titled “GraphQL 요청 템플릿”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<T> query<T>({ required String queryDocument, Map<String, dynamic>? variables, required T Function(Map<String, dynamic> 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<T> mutate<T>({ required String mutationDocument, Map<String, dynamic>? variables, required T Function(Map<String, dynamic> 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('알 수 없는 오류가 발생했습니다'); }}
상태 관리 템플릿
Section titled “상태 관리 템플릿”Riverpod + Freezed를 활용한 상태 관리
Section titled “Riverpod + Freezed를 활용한 상태 관리”@freezedclass TodoState with _$TodoState { const factory TodoState({ required AsyncValue<List<Todo>> todos, @Default('') String newTodoText, Todo? editingTodo, }) = _TodoState;
factory TodoState.initial() => TodoState( todos: const AsyncValue.loading(), );}
// todo_notifier.dart@riverpodclass TodoNotifier extends _$TodoNotifier { @override TodoState build() { _loadTodos(); return TodoState.initial(); }
Future<void> _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<void> 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), ); // 오류 메시지 표시 } }}
페이지 라우팅 템플릿
Section titled “페이지 라우팅 템플릿”GoRouter 설정
Section titled “GoRouter 설정”final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'shell');
final routerProvider = Provider<GoRouter>((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), );});
단위 테스트 템플릿
Section titled “단위 테스트 템플릿”일반 클래스 테스트
Section titled “일반 클래스 테스트”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<DivisionByZeroException>()), ); }); });}
Riverpod 테스트
Section titled “Riverpod 테스트”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); }); });}
위젯 테스트 템플릿
Section titled “위젯 테스트 템플릿”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); }); });}
예제 모음 링크
Section titled “예제 모음 링크”다음은 더 많은 코드 예제를 찾을 수 있는 유용한 리소스 모음입니다:
- Flutter Gallery - 공식 Flutter 위젯 및 기능 갤러리
- Flutter Samples - 공식 Flutter 샘플 모음
- Flutter 쿡북 - 공식 Flutter 쿡북 레시피
커뮤니티 예제
Section titled “커뮤니티 예제”- Flutter Awesome - 커뮤니티가 제작한 Flutter 앱 예제 모음
- Flutter Example Apps - 다양한 Flutter 앱 예제
- Flutter Clean Architecture - 클린 아키텍처 예제
- The Flutter Way - UI 중심 Flutter 예제 모음
- Riverpod Architecture - Riverpod 기반 아키텍처 예제
디자인 별 예제
Section titled “디자인 별 예제”- FlutterFolio - 반응형 웹 포트폴리오 예제
- Flutter UI Challenges - 다양한 UI 구현 예제
- Flutter Movies - 영화 앱 UI 예제
- Flutter Food Delivery - 음식 배달 앱 UI
특정 기능 구현 예제
Section titled “특정 기능 구현 예제”- Local Auth - 생체 인증 예제
- Provider Shopper - Provider를 활용한 쇼핑 앱
- Infinite List - 무한 스크롤 구현
- Firebase Chat - Firebase 기반 채팅 앱
아키텍처 예제
Section titled “아키텍처 예제”- Flutter TDD Clean Architecture - 테스트 주도 개발 + 클린 아키텍처
- Flutter BLoC Pattern - BLoC 패턴 예제
- Flutter Riverpod Architecture - Riverpod 아키텍처 예제
이 리소스들은 다양한 Flutter 프로젝트와 코드 예제를 제공하여 개발자가 더 빠르게 학습하고 개발할 수 있도록 도와줍니다.