{
"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