learn-flutter/content/docs/part5/multi-screen.md
ChangJoo Park(박창주) 900b0ab68b update markdown
2025-05-14 09:34:13 +09:00

29 KiB

실습: 복수 화면 전환

이 장에서는 go_router를 사용하여 복수 화면 전환을 구현하는 실습을 진행하겠습니다. 구체적으로는 할 일 관리 앱(Todo App)을 만들면서, 여러 화면 간의 네비게이션과 데이터 전달 방법을 익혀보겠습니다.

실습 개요

우리가 만들 할 일 관리 앱은 다음과 같은 화면들로 구성됩니다:

  1. 홈 화면: 할 일 목록 표시 및 카테고리별 필터링
  2. 할 일 상세 화면: 선택한 할 일의 세부 정보 표시
  3. 할 일 추가/편집 화면: 새 할 일 추가 또는 기존 할 일 편집
  4. 프로필 화면: 사용자 정보 및 설정
  5. 통계 화면: 할 일 완료율 등 통계 정보

1. 프로젝트 설정

먼저 새 Flutter 프로젝트를 생성하고 필요한 패키지를 추가합니다:

flutter create todo_app
cd todo_app

pubspec.yaml 파일에 필요한 패키지를 추가합니다:

dependencies:
  flutter:
    sdk: flutter
  go_router: ^10.0.0
  provider: ^6.0.5
  uuid: ^3.0.7

패키지를 설치합니다:

flutter pub get

2. 모델 정의

할 일 항목을 표현하는 모델 클래스를 정의합니다:

// lib/models/todo.dart
import 'package:uuid/uuid.dart';

class Todo {
  final String id;
  final String title;
  final String description;
  final bool isCompleted;
  final DateTime createdAt;
  final String category;

  Todo({
    String? id,
    required this.title,
    this.description = '',
    this.isCompleted = false,
    DateTime? createdAt,
    this.category = 'general',
  })  : id = id ?? const Uuid().v4(),
        createdAt = createdAt ?? DateTime.now();

  Todo copyWith({
    String? title,
    String? description,
    bool? isCompleted,
    String? category,
  }) {
    return Todo(
      id: id,
      title: title ?? this.title,
      description: description ?? this.description,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt,
      category: category ?? this.category,
    );
  }
}

3. 상태 관리

Provider를 사용하여 할 일 목록 상태를 관리합니다:

// lib/providers/todo_provider.dart
import 'package:flutter/foundation.dart';
import '../models/todo.dart';

class TodoProvider extends ChangeNotifier {
  final List<Todo> _todos = [];
  String _filter = 'all';
  final List<String> _categories = ['general', 'work', 'personal', 'shopping'];

  // 게터
  List<Todo> get todos => _filter == 'all'
      ? _todos
      : _filter == 'completed'
          ? _todos.where((todo) => todo.isCompleted).toList()
          : _filter == 'active'
              ? _todos.where((todo) => !todo.isCompleted).toList()
              : _todos
                  .where((todo) => todo.category == _filter)
                  .toList();

  List<String> get categories => _categories;
  String get filter => _filter;

  // 필터 설정
  void setFilter(String filter) {
    _filter = filter;
    notifyListeners();
  }

  // 할 일 추가
  void addTodo(Todo todo) {
    _todos.add(todo);
    notifyListeners();
  }

  // 할 일 업데이트
  void updateTodo(Todo todo) {
    final index = _todos.indexWhere((t) => t.id == todo.id);
    if (index >= 0) {
      _todos[index] = todo;
      notifyListeners();
    }
  }

  // 할 일 삭제
  void deleteTodo(String id) {
    _todos.removeWhere((todo) => todo.id == id);
    notifyListeners();
  }

  // 할 일 토글 (완료/미완료)
  void toggleTodo(String id) {
    final index = _todos.indexWhere((todo) => todo.id == id);
    if (index >= 0) {
      final todo = _todos[index];
      _todos[index] = todo.copyWith(isCompleted: !todo.isCompleted);
      notifyListeners();
    }
  }

  // ID로 할 일 찾기
  Todo? getTodoById(String id) {
    try {
      return _todos.firstWhere((todo) => todo.id == id);
    } catch (e) {
      return null;
    }
  }
}

4. 라우터 설정

go_router를 사용하여 앱의 라우팅을 설정합니다:

// lib/router/app_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';
import '../screens/home_screen.dart';
import '../screens/todo_detail_screen.dart';
import '../screens/todo_edit_screen.dart';
import '../screens/profile_screen.dart';
import '../screens/stats_screen.dart';

class AppRouter {
  final TodoProvider todoProvider;

  AppRouter(this.todoProvider);

  late final GoRouter router = GoRouter(
    initialLocation: '/',
    debugLogDiagnostics: true,
    routes: [
      // 메인 쉘 라우트 (바텀 네비게이션 바 포함)
      StatefulShellRoute.indexedStack(
        builder: (context, state, navigationShell) {
          return ScaffoldWithNavBar(navigationShell: navigationShell);
        },
        branches: [
          // 홈 탭
          StatefulShellBranch(
            routes: [
              GoRoute(
                path: '/',
                builder: (context, state) => const HomeScreen(),
                routes: [
                  // 할 일 상세 화면 (홈 탭 내 중첩 라우트)
                  GoRoute(
                    path: 'todo/:id',
                    builder: (context, state) {
                      final id = state.pathParameters['id']!;
                      return TodoDetailScreen(todoId: id);
                    },
                  ),
                  // 할 일 추가 화면
                  GoRoute(
                    path: 'add',
                    builder: (context, state) => const TodoEditScreen(),
                  ),
                  // 할 일 편집 화면
                  GoRoute(
                    path: 'edit/:id',
                    builder: (context, state) {
                      final id = state.pathParameters['id']!;
                      final todo = todoProvider.getTodoById(id);
                      return TodoEditScreen(todo: todo);
                    },
                  ),
                ],
              ),
            ],
          ),
          // 통계 탭
          StatefulShellBranch(
            routes: [
              GoRoute(
                path: '/stats',
                builder: (context, state) => const StatsScreen(),
              ),
            ],
          ),
          // 프로필 탭
          StatefulShellBranch(
            routes: [
              GoRoute(
                path: '/profile',
                builder: (context, state) => const ProfileScreen(),
              ),
            ],
          ),
        ],
      ),
    ],
    errorBuilder: (context, state) => Scaffold(
      appBar: AppBar(title: const Text('페이지를 찾을 수 없음')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('요청한 페이지를 찾을 수 없습니다.'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('홈으로 돌아가기'),
            ),
          ],
        ),
      ),
    ),
  );
}

// 바텀 네비게이션 바가 있는 스캐폴드
class ScaffoldWithNavBar extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const ScaffoldWithNavBar({
    Key? key,
    required this.navigationShell,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: '홈'),
          BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: '통계'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: '프로필'),
        ],
        onTap: (index) => navigationShell.goBranch(index),
      ),
    );
  }
}

5. 화면 구현

이제 각 화면을 구현해 보겠습니다:

5.1 홈 화면

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';
import '../widgets/todo_item.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);
    final todos = todoProvider.todos;
    final categories = todoProvider.categories;

    return Scaffold(
      appBar: AppBar(
        title: const Text('할 일 목록'),
        actions: [
          // 필터 메뉴
          PopupMenuButton<String>(
            icon: const Icon(Icons.filter_list),
            onSelected: (value) {
              todoProvider.setFilter(value);
            },
            itemBuilder: (context) => [
              const PopupMenuItem(
                value: 'all',
                child: Text('모든 할 일'),
              ),
              const PopupMenuItem(
                value: 'completed',
                child: Text('완료된 할 일'),
              ),
              const PopupMenuItem(
                value: 'active',
                child: Text('미완료 할 일'),
              ),
              const PopupMenuItem(
                value: 'general',
                child: Text('일반'),
              ),
              const PopupMenuItem(
                value: 'work',
                child: Text('업무'),
              ),
              const PopupMenuItem(
                value: 'personal',
                child: Text('개인'),
              ),
              const PopupMenuItem(
                value: 'shopping',
                child: Text('쇼핑'),
              ),
            ],
          ),
        ],
      ),
      body: todos.isEmpty
          ? const Center(
              child: Text('할 일이 없습니다. 새 할 일을 추가해보세요!'),
            )
          : ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                final todo = todos[index];
                return TodoItem(
                  todo: todo,
                  onTap: () => context.go('/todo/${todo.id}'),
                  onToggle: () => todoProvider.toggleTodo(todo.id),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.go('/add'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

5.2 할 일 항목 위젯

// lib/widgets/todo_item.dart
import 'package:flutter/material.dart';
import '../models/todo.dart';

class TodoItem extends StatelessWidget {
  final Todo todo;
  final VoidCallback onTap;
  final VoidCallback onToggle;

  const TodoItem({
    Key? key,
    required this.todo,
    required this.onTap,
    required this.onToggle,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(
        todo.title,
        style: TextStyle(
          decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
          color: todo.isCompleted ? Colors.grey : null,
        ),
      ),
      subtitle: Text(
        '카테고리: ${todo.category}',
        style: TextStyle(
          color: todo.isCompleted ? Colors.grey : Colors.black54,
        ),
      ),
      leading: Checkbox(
        value: todo.isCompleted,
        onChanged: (_) => onToggle(),
      ),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }
}

5.3 할 일 상세 화면

// lib/screens/todo_detail_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';
import '../models/todo.dart';

class TodoDetailScreen extends StatelessWidget {
  final String todoId;

  const TodoDetailScreen({
    Key? key,
    required this.todoId,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);
    final todo = todoProvider.getTodoById(todoId);

    if (todo == null) {
      return Scaffold(
        appBar: AppBar(title: const Text('할 일 없음')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('요청한 할 일을 찾을 수 없습니다.'),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: () => context.go('/'),
                child: const Text('홈으로 돌아가기'),
              ),
            ],
          ),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: const Text('할 일 상세'),
        actions: [
          // 편집 버튼
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => context.go('/edit/${todo.id}'),
          ),
          // 삭제 버튼
          IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () {
              todoProvider.deleteTodo(todo.id);
              context.go('/');
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 제목
            Text(
              todo.title,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 8),

            // 카테고리
            Chip(
              label: Text(todo.category),
              backgroundColor: Colors.blue.shade100,
            ),
            const SizedBox(height: 16),

            // 생성 날짜
            Text(
              '생성일: ${_formatDate(todo.createdAt)}',
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: 16),

            // 상태
            Row(
              children: [
                const Text('상태: '),
                Checkbox(
                  value: todo.isCompleted,
                  onChanged: (_) {
                    todoProvider.toggleTodo(todo.id);
                  },
                ),
                Text(todo.isCompleted ? '완료' : '미완료'),
              ],
            ),
            const SizedBox(height: 16),

            // 설명
            const Text(
              '설명:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey.shade300),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                todo.description.isEmpty ? '(설명 없음)' : todo.description,
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _formatDate(DateTime date) {
    return '${date.year}${date.month}${date.day}일';
  }
}

5.4 할 일 추가/편집 화면

// lib/screens/todo_edit_screen.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import '../models/todo.dart';
import '../providers/todo_provider.dart';

class TodoEditScreen extends StatefulWidget {
  final Todo? todo;

  const TodoEditScreen({Key? key, this.todo}) : super(key: key);

  @override
  _TodoEditScreenState createState() => _TodoEditScreenState();
}

class _TodoEditScreenState extends State<TodoEditScreen> {
  final _formKey = GlobalKey<FormState>();
  late TextEditingController _titleController;
  late TextEditingController _descriptionController;
  String _category = 'general';
  bool _isCompleted = false;

  @override
  void initState() {
    super.initState();
    _titleController = TextEditingController(text: widget.todo?.title ?? '');
    _descriptionController = TextEditingController(text: widget.todo?.description ?? '');
    _category = widget.todo?.category ?? 'general';
    _isCompleted = widget.todo?.isCompleted ?? false;
  }

  @override
  void dispose() {
    _titleController.dispose();
    _descriptionController.dispose();
    super.dispose();
  }

  void _saveTodo() {
    if (_formKey.currentState!.validate()) {
      final todoProvider = Provider.of<TodoProvider>(context, listen: false);

      if (widget.todo == null) {
        // 새 할 일 추가
        final newTodo = Todo(
          title: _titleController.text,
          description: _descriptionController.text,
          category: _category,
          isCompleted: _isCompleted,
        );
        todoProvider.addTodo(newTodo);
      } else {
        // 기존 할 일 수정
        final updatedTodo = Todo(
          id: widget.todo!.id,
          title: _titleController.text,
          description: _descriptionController.text,
          category: _category,
          isCompleted: _isCompleted,
          createdAt: widget.todo!.createdAt,
        );
        todoProvider.updateTodo(updatedTodo);
      }

      context.go('/');
    }
  }

  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);
    final categories = todoProvider.categories;

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.todo == null ? '할 일 추가' : '할 일 편집'),
      ),
      body: Form(
        key: _formKey,
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 제목 입력
              TextFormField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: '제목',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return '제목을 입력해주세요';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),

              // 설명 입력
              TextFormField(
                controller: _descriptionController,
                decoration: const InputDecoration(
                  labelText: '설명',
                  border: OutlineInputBorder(),
                ),
                maxLines: 3,
              ),
              const SizedBox(height: 16),

              // 카테고리 선택
              DropdownButtonFormField<String>(
                value: _category,
                decoration: const InputDecoration(
                  labelText: '카테고리',
                  border: OutlineInputBorder(),
                ),
                items: categories.map((category) {
                  return DropdownMenuItem(
                    value: category,
                    child: Text(category),
                  );
                }).toList(),
                onChanged: (value) {
                  setState(() {
                    _category = value!;
                  });
                },
              ),
              const SizedBox(height: 16),

              // 완료 상태 토글
              CheckboxListTile(
                title: const Text('완료 상태'),
                value: _isCompleted,
                onChanged: (value) {
                  setState(() {
                    _isCompleted = value!;
                  });
                },
                controlAffinity: ListTileControlAffinity.leading,
              ),
              const SizedBox(height: 32),

              // 저장 버튼
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _saveTodo,
                  style: ElevatedButton.styleFrom(
                    padding: const EdgeInsets.symmetric(vertical: 16),
                  ),
                  child: Text(
                    widget.todo == null ? '추가하기' : '수정하기',
                    style: const TextStyle(fontSize: 16),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5.5 통계 화면

// lib/screens/stats_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';

class StatsScreen extends StatelessWidget {
  const StatsScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);
    final todos = todoProvider.todos;
    final completedTodos = todos.where((todo) => todo.isCompleted).toList();

    final completionRate = todos.isEmpty
        ? 0.0
        : (completedTodos.length / todos.length) * 100;

    // 카테고리별 할 일 수
    final categoryStats = <String, int>{};
    for (final category in todoProvider.categories) {
      categoryStats[category] = todos
          .where((todo) => todo.category == category)
          .length;
    }

    return Scaffold(
      appBar: AppBar(title: const Text('통계')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 총계
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '전체 요약',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                    const Divider(),
                    _buildStatItem('전체 할 일', todos.length.toString()),
                    _buildStatItem('완료된 할 일', completedTodos.length.toString()),
                    _buildStatItem('진행 중인 할 일',
                        (todos.length - completedTodos.length).toString()),
                    _buildStatItem('완료율', '${completionRate.toStringAsFixed(1)}%'),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 20),

            // 카테고리별 통계
            Text(
              '카테고리별 통계',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Expanded(
              child: ListView.builder(
                itemCount: categoryStats.length,
                itemBuilder: (context, index) {
                  final category = todoProvider.categories[index];
                  final count = categoryStats[category] ?? 0;
                  return Card(
                    child: ListTile(
                      title: Text(category),
                      trailing: Text(
                        count.toString(),
                        style: Theme.of(context).textTheme.titleMedium,
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatItem(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(
            value,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }
}

5.6 프로필 화면

// lib/screens/profile_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todoProvider = Provider.of<TodoProvider>(context);

    return Scaffold(
      appBar: AppBar(title: const Text('프로필')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const CircleAvatar(
              radius: 50,
              child: Icon(Icons.person, size: 50),
            ),
            const SizedBox(height: 16),
            const Text(
              '사용자',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            const Text('user@example.com'),
            const SizedBox(height: 32),

            // 사용 통계
            const Text(
              '사용 통계',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildStatItem(context, '총 할 일 수',
                todoProvider.todos.length.toString()),
            _buildStatItem(context, '완료한 할 일',
                todoProvider.todos.where((todo) => todo.isCompleted).length.toString()),

            const SizedBox(height: 32),

            // 설정 섹션
            const Text(
              '설정',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildSettingItem(
              context,
              Icons.notifications,
              '알림 설정',
              () => _showNotImplementedSnackBar(context),
            ),
            _buildSettingItem(
              context,
              Icons.color_lens,
              '테마 설정',
              () => _showNotImplementedSnackBar(context),
            ),
            _buildSettingItem(
              context,
              Icons.language,
              '언어 설정',
              () => _showNotImplementedSnackBar(context),
            ),
            _buildSettingItem(
              context,
              Icons.info,
              '앱 정보',
              () => _showNotImplementedSnackBar(context),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatItem(BuildContext context, String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(
            value,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }

  Widget _buildSettingItem(
    BuildContext context,
    IconData icon,
    String label,
    VoidCallback onTap,
  ) {
    return ListTile(
      leading: Icon(icon),
      title: Text(label),
      trailing: const Icon(Icons.chevron_right),
      onTap: onTap,
    );
  }

  void _showNotImplementedSnackBar(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('이 기능은 아직 구현되지 않았습니다.'),
        duration: Duration(seconds: 2),
      ),
    );
  }
}

6. 메인 앱 통합

마지막으로 모든 구성 요소를 메인 앱에 통합합니다:

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'providers/todo_provider.dart';
import 'router/app_router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => TodoProvider(),
      child: Builder(
        builder: (context) {
          final todoProvider = Provider.of<TodoProvider>(context);
          final appRouter = AppRouter(todoProvider);

          return MaterialApp.router(
            title: '할 일 관리 앱',
            theme: ThemeData(
              primarySwatch: Colors.blue,
              useMaterial3: true,
            ),
            routerConfig: appRouter.router,
          );
        },
      ),
    );
  }
}

7. 실행 및 테스트

이제 앱을 실행하고 다양한 화면 전환 시나리오를 테스트해 봅시다:

  1. 홈 화면에서 할 일 목록 확인
  2. 새 할 일 추가
  3. 할 일 상세 정보 확인
  4. 할 일 편집
  5. 할 일 삭제
  6. 카테고리별 필터링
  7. 바텀 네비게이션 바를 통한 화면 전환
  8. 통계 화면에서 완료율 확인
  9. 프로필 화면 확인

요약

이 실습을 통해 우리는 다음과 같은 개념을 학습했습니다:

  1. go_router를 사용한 라우팅 설정: 다양한 경로와 매개변수를 정의하고 관리하는 방법
  2. StatefulShellRoute를 활용한 바텀 네비게이션 바: 탭 기반 UI에서 네비게이션 상태를 유지하는 방법
  3. 화면 간 데이터 전달: 경로 매개변수와 상태 관리를 통한 데이터 전달 방법
  4. 중첩 라우트: 탭 내부에서 다른 화면으로 이동하는 방법
  5. Provider와 함께 사용: 상태 관리와 라우팅을 통합하는 방법

이러한 기술을 활용하면 복잡한 네비게이션 패턴을 가진 앱도 효과적으로 구현할 수 있습니다. 다음 섹션에서는 Drawer, BottomNavigationBar, TabBar와 같은 다양한 네비게이션 위젯에 대해 알아보겠습니다.