[Flutter 기초] 개인과제 - To Do App UI 만들기
과제 시나리오 링크
간단한 필드: TextField + TextEditingController로 isEmpty 체크.
복합적인 폼: Form + TextFormField + validator 조합 (권장).
사용자 경험: autovalidateMode로 즉각적인 피드백 제공.
class TodoBottomsheet 별도 클래스를 만들어서 showModalBottomSheet를 static메서드에 담은 뒤 HomePage의 플로팅액션버튼 내에서 호츨하였다.
지금 작성한 코드는 엔터를 눌러서 바로 저장하는 방식인데
// 엔터를 눌렀을 때 저장되는 로직
onSubmitted: (value) {
// 텍스트가 비어있을 땐 저장이 되지 않도록 구현
if (value.trim().isEmpty) {
print('할 일을 입력해주세요.');
return;
}
saveToDo();
print('새 할 일이 저장되었습니다.');
},
텍스트가 입력될 때 상태를 실시간으로 감시하려면 StatefulWidget으로 변경해야 하고
UI / 기능 로직 / 저장 로직 으로 상태관리를 위해 파일을 분리해야겠다.
// setState()로 Todo 저장버튼 활성화
우선은 기능별로 파일을 분리하지 말고 기초과제에서 요구하는 부분들을 최대한 빠르게 구현하는 것을 목표로 잡았다..
기초문제 4번 타이틀 입력하는 텍스트 필드 아래에 설명을 적는 기능과 즐겨찾기, 저장 로직을 구현해야 한다.
Entity 클래스 내 아이콘 상태를 관리하는 것은 클린 아키텍처를 적용할 때 UI와 비즈니스 로직을 분리하는 핵심적인 부분
주로 엔티티(데이터) -> Bloc/Riverpod(상태 관리) -> UI(Icon) 순으로 데이터가 흐르며, 아이콘의 변경(상태)은 불변 객체를 통해 관리된다.
지금은 상태관리를 위해 별도의 라이브러리를 사용하고 있지는 않고
바텀시트 스테이트풀위젯은 별도로 만들고,
홈페이지에서 바텀시트 리턴해주고,
바텀시트에서 네비게이터팝 구현하고 인자값을 받아줄 때 (context, todoEntity()) 를 반환하게 구현하기
쇼모달바텀시트도 제네릭함수로 구현해서 공통으로 사용할 수 있도록 하기
(StatefulBuilder를 사용해서 구현하려고 했는데 난이도가 더 높아져서 setState()를 사용하는 것으로 변경)
저장버튼을 구현한 뒤 실시간으로 title에 입력값이 있을 때 저장버튼의 색상이 활성화 되게끔 설정하려고 했더니 바로 적용이 되지 않았다.
해결법.
1. 컨트롤러를 사용해야 된다?
2. setState()를 사용한 부분을 다시 살펴본다.
TextButton(
onPressed: () {
// 저장이 작동되면 ToDO객체를 반환하고 창닫기
newTodo.title.trim().isEmpty
? null
: Navigator.pop(context, newTodo);
print('${newTodo.title}반환완료');
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 12,
),
),
child: Text(
'저장',
style: TextStyle(
// 삼항연산자. 입력요소 따라서 활성화.
color: newTodo.title.trim().isEmpty
? Colors.black54
: Colors.blueAccent,
fontWeight: FontWeight.bold,
),
),
),
TextField에서 onChanged만 사용했을 때 버튼이 바로 활성화되지 않는 이유는 newTodo.title 값은 바뀌고 있지만,
그 변화가 화면을 다시 그리라는 신호(setState)로 이어지지 않았기 때문일 가능성이 있다!
TextField(
autofocus: true,
textInputAction: TextInputAction.done,
maxLines: 1,
// 엔터를 눌렀을 때 저장되는 로직
onChanged: (value) {
newTodo.title = value;
},
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '새 할 일',
hintStyle: TextStyle(fontSize: 16, color: Colors.black54),
),
),
어떤 실수가 있는지 잘 발견하지 못했었는데
setState에서 값을 받도록 변경하였더니 정상적으로 저장버튼이 활성화가 되고 입력을 완료하였을 때 정상적으로 텍스트필드까지 종료가 되었다.
onChanged: (value) {
// 값을 입력하면 빈 엔티티 타이틀이 업데이트 됨!
setState(() {
newTodo.title = value;
});
},
description 입력용 TextField에서는 줄바꿈이 적용되도록 구현
세부사항 줄이 늘어났을 때, view가 깨지지 않도록 expanded로 감싸주었더니 전체 화면을 다 채워버리는 문제가 생겼다.
문제의 조건에는 expended를 사용하라고 되어 있었지만 사용하지 않았을 때 문제가 발생하지 않아서 expended를 제거하였다.

onSubmitted: () {} / onChanged: () {} 차이점
onSubmitted - 엔터 눌렀을 때 작동
onChanged - 셋스테이트 반영되서 바뀜
-> title이 입력될 때 실시간 변동되려면 onChanged 사용
컨트롤러는 말 그대로 '사용자가 글자를 입력하거나 수정할 때' 그 글자를 관리하는 로직
(만약 뒤의 디테일페이지에서도 할 일의 제목이나 내용을 수정하고 싶다는 기능이 추가된다면, 그때는 컨트롤러가 필요함!)
TextField의 입력 값을 제어하기 위해서 TextEditingController를 사용
StatefulWidget 내에서 컨트롤러를 초기화(initState)하고, TextField에 연결한 후, 사용이 끝나면 반드시 메모리에서 해제(dispose)해야 한다!
[ 문제발생 ]
컨트롤러를 1개만 사용했다가 동시에 동작이 되는 문제가 생겼다.
각각 타이틀과 부가설명이 입력되는 텍스트필드에서 사용할 컨트롤러를 분리해 주었더니 문제가 해결되었다.

정리. 계속 기본개념을 익히느라 todo앱 만들기를 3번이나 도전했는데빠른 구현과 학습을 우선적으로 하기 위해서 로직들을 분리하지 않고 우선 구현을 했지만 UI를 구성하고 기능넣는 방식과 흐름을 조금씩 이해하고 있다.
저장이 되는 부분은 엔티티에 있는 정보를 가지고 빈 리스트를 만들어서 그곳에 저장을 하면 되는 것였고,
컨트롤러를 사용하는 이유와 방식, 성능최적화를 위한 코드 구현에 대해서도 배웠다.
5번. todo가 추가된 화면 만들기
목표: 앱의 기본 사이클(입력-저장-표시)를 이해하자
데이터는 Navigator.pop으로 던지고, await로 받는다.

홈페이지에서 투두리스트 내용을 두투뷰로 보내서 저장한 부분을 출력을 해주었다.
사용자가 터치해서 즐겨찾기와 완료를 변경하는 로직은 홈페이지에서 반영을 하고,
투두뷰에서는 하나의 리스트에 대해 구현하고 기능 로직을 넣어주면 된다.
클래스 투두뷰
할 일 리스트가 등록이 되고, 이제 이 리스트에서 즐겨찾기와 할일 체크 아이콘을 터치시 홈페이지에서 받은 그 동작값을 투두뷰에서 받아와서 상태를 반영하는 것
StatefulWidget의 경우, 데이터는 Widget 객체에 전달되고, State 객체에서는 widget.변수명으로 접근한다.
아이콘을 눌렀을 때 행동을 해야 하므로 ()를 붙여서 호출해줘야 한다!!
return으로 값을 돌려주면 안되는걸까?
2. return을 사용하는 경우
함수는 return을 만나는 순간 그 즉시 종료되고 값을 뱉어낸다.
하지만 onTap은 동작을 시키는 곳이지 값을 받는 곳이 아니기 때문에, 여기서 무언가를 return 해도 그 값을 받아서 쓰지 못한다.
비유: 점원에게 "이거 눌리면 '네'라고 대답해(return)"라고 시켰는데, 점원이 혼자 벽 보고 "네!"라고 하고 상황이 끝나버리는 것과 같다.
child: ListTile(
horizontalTitleGap: 20,
leading: GestureDetector(
onTap: () {
// 부모인 홈페이지에서 클릭 시 넘겨준 값을 받는것
widget.onToggleDone();
},
child: Icon(Icons.circle),
),
역할
홈페이지에서 사용자가 버튼을 클릭해서 상태가 변경되는 값을 투두뷰에서 받게 되는거고,
투두뷰에서는 그 상태에 따라 변경되는 부분에 대한 로직을 구현해서 반영하는 것
"소통의 흐름" 정리
지금 짜신 코드가 어떻게 작동하는지 머릿속으로 그려보세요. 이 흐름이 이해된다면 혜림 님은 이제 초보 탈출입니다!
사용자: TodoView의 써클 아이콘 클릭!
자식(TodoView): widget.onToggleDone(); 호출 (부모님! 눌렸어요!)
부모(HomePage): setState 실행 → todoList[index].isDone 반전!
플러터: "오, 데이터가 변했네? HomePage 다시 그려!"
결과: 변신한 데이터를 들고 TodoView가 다시 그려지면서 체크 표시와 취소선이 슥 나타남.
6번 문제
커스터마이징 앱 바 만들기
뒤로가기가 작동하는 원리 (Stack 구조)
플러터는 페이지를 종이 더미(Stack)처럼 쌓아요.
처음에 HomePage가 바닥에 깔립니다.
리스트를 누르면 Navigator.push를 통해 TodoDetailPage가 그 위에 슥 올라옵니다.
이때 앱 바의 뒤로가기(Navigator.pop)를 누르면, 위에 있던 상세 페이지 종이를 휙 치워버리는 거예요. 그러면 자연스럽게 바닥에 있던 HomePage가 다시 보이게 되죠.
TodoView에서 클릭하면 홈페이지의 toDo의 item데이터 값을 가지고 디테일페이지로 넘어감
Navigator.push는 새로운 화면(Route)을 스택(Stack) 위에 쌓아 올리는 방식으로 화면을 전환, 이전 화면으로 돌아갈 때는 Navigator.pop(context)을 사용
Navigator.of(context).push(MaterialPageRoute(...)) 형식을 사용
TodoDetailPaged에서 UI를 구현하고, 리스트(TodoView)와 연결하기
1. 리스트를 클릭하면 디테일페이지로 넘어가야 한다.
2. 내용을 수정하고 뒤로가기 시 변경된 내용이 반영되어야 한다.
3. 즐겨찾기를 디테일페이지에서 적용한 값이 홈페이지로 돌아갔을 때에도 적용이 되어야 한다.
4. 홈 화면에서도 변경된 데이터를 받아서 리스트를 업데이트 해주어야 한다.
1-1.
리스트의 타이틀과 세부내용이 똑같이 출력이 되어야 한다.
TodoEntity를 받아서 화면컨텐츠를 채우기
[ 즐겨찾기 ]
데이터의 일관성
어떤 화면에서 데이터를 바꾸더라도 모든 곳에 똑같이 반영되는 흐름을 정리해보았다.
[추가 단계: 탄생과 동시에 설정]
흐름: TodoEndtity의 빈 리스트(newTodo)에 isFavorite: true 입력값이 담기고


핵심: Navigator.pop(context, newTodo)를 통해 홈페이지에 던져주고, 홈페이지에서는 setState()로 이 리스트를 todoList에 넣고
UI에 리스트가 그려지게 된다.

[전달 단계: 리스트 그대로 넘겨주기]
흐름: 리스트의 항목(TodoView)을 누르면, 홈페이지가 가지고 있던 그 item(리스트 (final item = todoList[index];))을 TodoDetailPage로 복사해서 보내주는 게 아니라 리스트 그대로 들고가서 보여줌
(TodoView클래스에서 toDo는 TodoEntity의 변수)

핵심: 디테일페이지는 홈페이지와 같은 리스트를 보고 있기 때문에, 들어가자마자 즐겨찾기가 설정되어 있는 것을 확인가능함
[동기화 단계: 바뀐 내용 새로고침]
흐름: 디테일페이지에서 즐겨찾기를 누르면 메모리에 있는 그 리스트의 내용이 즉시 바뀐다.
이후 뒤로가기를 눌러 홈페이지로 돌아왔을 때, TodoView가 onUpdate() 신호를 홈페이지에 보냄
(onUpdate: 바뀐 값을 화면에 보여주기만 할 때 쓰는 것 (상세 페이지 갔다 온 후))

핵심: 홈페이지는 이 신호를 받고 setState를 실행해 이미 바뀌어 있는 리스트를 리스트에 다시 그래냄

도전과제
테마 적용하기
테마적용 참고