
{
	"userId": 1,
	"id": 1,
	"title": "delectus aut autem",
	"completed": false
}import 'package:dio/dio.dart';
class Todo {
  int userId;
  int id;
  String title;
  bool completed;
  Todo({
    required this.userId,
    required this.id,
    required this.title,
    required this.completed,
  });
  factory Todo.fromMap(Map<String, dynamic> map) {
    return Todo(userId: map['userId'], id: map['id'], title: map['title'], completed: map['completed']);
  }
  
  String toString() => 'Todo($title)';
}
Future<Todo?> getData(int todoNumber) async {
  Dio dio = Dio();
  var url = 'https://jsonplaceholder.typicode.com/todos/$todoNumber';
  var res = await dio.get(url);
  if (res.statusCode == 200) {
    return Todo.fromMap(res.data);
  }
  return null;
}
void main() async {
  var todoNumber = 5;
  var data = await getData(todoNumber);
  print(data);
}[
	{
		"userId": 1,
		"id": 1,
		"title": "delectus aut autem",
		"completed": false
	},
	{
		"userId": 1,
		"id": 2,
		"title": "quis ut nam facilis et officia qui",
		"completed": false
	},
	{
		"userId": 1,
		"id": 3,
		"title": "fugiat veniam minus",
		"completed": false
	},
	... 200개
]import 'package:dio/dio.dart';
class Todo {
  int userId;
  int id;
  String title;
  bool completed;
  Todo({
    required this.userId,
    required this.id,
    required this.title,
    required this.completed,
  });
  factory Todo.fromMap(Map<String, dynamic> map) {
    return Todo(userId: map['userId'], id: map['id'], title: map['title'], completed: map['completed']);
  }
  
  String toString() => 'Todo($title)';
}
Future<Todo?> getData(int todoNumber) async {
  Dio dio = Dio();
  var url = 'https://jsonplaceholder.typicode.com/todos/$todoNumber';
  var res = await dio.get(url);
  if (res.statusCode == 200) {
    return Todo.fromMap(res.data);
  }
  return null;
}
Future<List<Todo>> readData() async {
  Dio dio = Dio();
  var url = 'https://jsonplaceholder.typicode.com/todos';
  var res = await dio.get(url);
  if (res.statusCode == 200) {
    var data = List<Map<String, dynamic>>.from(res.data); // List<dynamic> -> List<Map<dynamic>>
    return data.map((e) => Todo.fromMap(e)).toList();
  }
  return [];
}
void main() async {
  var allTodos = await readData();
  print(allTodos);
  print(allTodos[1].title);
}1. Todo 활용
다음의 공개된 API를 분석하고, 클래스를 활용하여 적용 후
해야할 일을 보여주는 앱을 다음과 같이 만드시오.
https://jsonplaceholder.typicode.com/todos
- 반드시 Todo 클래스를 만들고 Serialization을 진행할 수 있도록 하시오.
 - AppBar는 다음의 조건을 따라 만들도록 하시오
 
- Blur 효과를 넣어 body의 내용이 흐릿하게 보여질 수 있도록 디자인하시오.
 - Actions에는 다음의 기능이 포함되어있는 아이콘을 제작하시오
 
- Filter 아이콘 :
 
- 클릭시 아래서 필터를 설정할 수 있도록 시트 위젯이 켜진다.
 - 필터가 적용되면 화면에 보이는 데이터의 종류가 바뀐다.
 - (필터선택시 아래에서 올라오는 안내문구는 선택사항임)
 - Refresh 아이콘 :
 
- 클릭시 네트워크에 데이터를 한 번 더 요청하여 리스트에 재적용한다.
 - 각 Post를 보여주는 Widget은 다음의 조건을 따라 만들도록 하시오
 
- 완료된 상태의 Post라면, 초록색 배경에 체크버튼의 아이콘이 보여지도록 한다.
 - Dismissable 위젯을 활용하여 옆으로 슬라이드 했을 때, 리스트에서 사라지도록 한다.
 
- 추가적으로, Dismissable 위젯의 key 속성이 의미하는 바를 정리하시오.
 - 제공되는 소스코드를 활용할 수 있도록 하시오.
 
- widget/filter_bottom_sheet.dart
 
필터 아이콘 누를 시 하단에 출력되는 위젯입니다.
enum에 대해 학습을 따로 진행하는 것을 추천드립니다.
import 'package:flutter/material.dart'; enum TodoFilter { all, completed, incompleted } class FilterBottomSheet extends StatefulWidget { const FilterBottomSheet( {Key? key, required this.filter, required this.onApply}) : super(key: key); final TodoFilter filter; final Function(TodoFilter) onApply; @override State<FilterBottomSheet> createState() => _FilterBottomSheetState(); } class _FilterBottomSheetState extends State<FilterBottomSheet> { onApply(TodoFilter filter) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Filter applied: $filter'), ), ); widget.onApply(filter); Navigator.pop(context); } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: const Text('All'), trailing: Checkbox( value: widget.filter == TodoFilter.all, onChanged: (value) { if (value == true) onApply(TodoFilter.all); }, ), ), ListTile( title: const Text('Completed'), trailing: Checkbox( value: widget.filter == TodoFilter.completed, onChanged: (value) { if (value == true) onApply(TodoFilter.completed); }, ), ), ListTile( title: const Text('InCompleted'), trailing: Checkbox( value: widget.filter == TodoFilter.incompleted, onChanged: (value) { if (value == true) onApply(TodoFilter.incompleted); }, ), ), ], ), ); } }- widget/todo_card.dart
 
(model 폴더에 todo클래스를 만들어놓을 것)
import 'package:flutter/material.dart'; import '../model/todo.dart'; class TodoCard extends StatelessWidget { const TodoCard({super.key, required this.todo}); final Todo todo; Widget build(BuildContext context) { return Dismissible( key: Key(todo.id.toString()), child: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( color: todo.completed ? Colors.green.shade100 : null, border: todo.completed ? Border.all( color: Colors.green, ) : null, borderRadius: BorderRadius.circular(8), ), child: ListTile( title: Text( todo.title, style: TextStyle( color: todo.completed ? Colors.green : null, fontWeight: FontWeight.bold, ), ), trailing: todo.completed ? const Icon( Icons.check_circle, color: Colors.green, ) : null, ), ), ); } }
main.dart
import 'package:flutter/material.dart';
import 'page/main_page.dart';
void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainPage(),
    );
  }
}
todo.dart
class Todo {
  int userId;
  int id;
  String title;
  bool completed;
  Todo({
    required this.userId,
    required this.id,
    required this.title,
    required this.completed,
  });
  factory Todo.fromMap(Map<String, dynamic> map) {
    return Todo(userId: map['userId'], id: map['id'], title: map['title'], completed: map['completed']);
  }
}
main_page.dart
import 'dart:developer';
import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:first_app/homework/week6/day26/widget/todo_card.dart';
import 'package:flutter/material.dart';
import '../model/todo.dart';
import '../widget/filter_bottom_sheet.dart';
class MainPage extends StatefulWidget {
  const MainPage({Key? key}) : super(key: key);
  
  State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
  TodoFilter _filter = TodoFilter.all;
  // 데이터 불러오기
  Future<List<Todo>> getData() async {
    Dio dio = Dio();
    String url = 'https://jsonplaceholder.typicode.com/todos';
    var res = await dio.get(url);
    if (res.statusCode == 200) {
      var data = List<Map<String, dynamic>>.from(res.data);
      return data.map((e) => Todo.fromMap(e)).toList();
    }
    return [];
  }
  
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        foregroundColor: Colors.black,
        elevation: 0,
        title: Text('Todo App'),
        // blur
        flexibleSpace: ClipRRect(
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
            child: Container(color: Colors.transparent)
          )
        ),
        actions: [
          IconButton(
            onPressed: (){
              showModalBottomSheet(
                context: context,
                builder: (context) => FilterBottomSheet(
                  filter: _filter, onApply: (filter) {
                    setState(() {
                      _filter = filter;
                    });
                  },
                ),
              );
            },
            icon: Icon(Icons.filter_list)
          ),
          IconButton(
            onPressed: (){
              setState((){});
            },
            icon: Icon(Icons.restart_alt)
          ),
        ],
      ),
      body: FutureBuilder(
        future: getData(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            var todos = snapshot.data!;
            return ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                var todo = todos[index];
                switch (_filter) {
                  case TodoFilter.completed:
                    return todo.completed ? TodoCard(todo: todo) : Container();
                  case TodoFilter.incompleted:
                    return !todo.completed ? TodoCard(todo: todo) : Container();
                  default:
                    return TodoCard(todo: todo);
                }
              },
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}
filter_bottom_sheet.dart
import 'package:flutter/material.dart';
// 필터링 옵션
enum TodoFilter { all, completed, incompleted }
class FilterBottomSheet extends StatefulWidget {
  const FilterBottomSheet(
      {Key? key, required this.filter, required this.onApply})
      : super(key: key);
  final TodoFilter filter;            // 현재 선택된 필터링 옵션 저장
  final Function(TodoFilter) onApply; // 선택된 필터링 옵션 적용
  
  State<FilterBottomSheet> createState() => _FilterBottomSheetState();
}
class _FilterBottomSheetState extends State<FilterBottomSheet> {
  onApply(TodoFilter filter) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Filter applied: $filter'),
      ),
    );
    widget.onApply(filter);   // 선택된 필터링 옵션 적용
    Navigator.pop(context);   // bottom sheet 닫기
  }
  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            title: const Text('All'),
            trailing: Checkbox(
              value: widget.filter == TodoFilter.all,
              onChanged: (value) {
                if (value == true) onApply(TodoFilter.all);
              },
            ),
          ),
          ListTile(
            title: const Text('Completed'),
            trailing: Checkbox(
              value: widget.filter == TodoFilter.completed,
              onChanged: (value) {
                if (value == true) onApply(TodoFilter.completed);
              },
            ),
          ),
          ListTile(
            title: const Text('InCompleted'),
            trailing: Checkbox(
              value: widget.filter == TodoFilter.incompleted,
              onChanged: (value) {
                if (value == true) onApply(TodoFilter.incompleted);
              },
            ),
          ),
        ],
      ),
    );
  }
}
todo_card.dart
import 'package:flutter/material.dart';
import '../model/todo.dart';
class TodoCard extends StatelessWidget {
  const TodoCard({super.key, required this.todo});
  final Todo todo;
  
  Widget build(BuildContext context) {
    return Dismissible(
      key: Key(todo.id.toString()),
      child: Container(
        margin: const EdgeInsets.all(8),
        decoration: BoxDecoration(
          color: todo.completed ? Colors.green.shade100 : null,
          border: todo.completed
              ? Border.all(
            color: Colors.green,
          )
              : null,
          borderRadius: BorderRadius.circular(8),
        ),
        child: ListTile(
          title: Text(
            todo.title,
            style: TextStyle(
              color: todo.completed ? Colors.green : null,
              fontWeight: FontWeight.bold,
            ),
          ),
          trailing: todo.completed
              ? const Icon(
            Icons.check_circle,
            color: Colors.green,
          )
              : null,
        ),
      ),
    );
  }
}

Dismissable 위젯을 사용할 때 각 항목을 고유하게 식별하는 데 사용
위젯의 상태를 관리하고, 위젯이 생성 및 소멸될 때 어떤 작업을 수행할지 결정할 수 있음
각 항목은 고유한 key를 가지고 있으며, 이를 사용하여 해당 항목의 상태를 유지하고 관리
GlobalKey
UniqueKey
