3주차 - 패키지 사용법 익히기 & 앱의 기능 만들기 (마이메모)
PDF 파일
단축키 모음
- 새로고침
F5
- 저장
- Windows:
Ctrl
+S
- macOS:
command
+S
- Windows:
- 전체선택
- Windows:
Ctrl
+A
- macOS:
command
+A
- Windows:
- 잘라내기
- Windows:
Ctrl
+X
- macOS:
command
+X
- Windows:
- 콘솔창 줄바꿈
shift
+enter
- 코드정렬
- Windows:
Ctrl
+Alt
+L
- macOS:
option
+command
+L
- Windows:
- 들여쓰기
Tab
- 들여쓰기 취소 :
Shift
+Tab
- 주석
- Windows:
Ctrl
+/
- macOS:
command
+/
- Windows:
- 새로고침
[수업 목표]
- 패키지 사용법 익히기
- Create / Read / Update / Delete 기능 구현하기
- 상태 관리의 필요성 이해하기
- Provider 사용법 익히기
- shared_preferences 사용법 익히기
[목차]
01. 패키지(Package)
1) 패키지란?
Flutter는
pub.dev
라는 사이트에서 패키지들을 검색하실 수 있습니다. 코드스니펫을 복사해서 새 탭에서 띄워주세요.-
https://pub.dev/
-
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9e74b84f-b51e-4789-9a74-c38325a6f3c8/Untitled.png)
- 밑으로 조금 내려와 보면 Flutter Favorites라고 인증된 신뢰할 만한 패키지 목록이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/68298554-45dc-4a21-8796-49647462169c/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7a7eb553-ac5a-45dd-ab1e-5bf60e361959/Untitled.png)
2) 패키지 문서 보는 방법
코드스니펫을 복사해 새 탭에서 열어주세요.
-
https://pub.dev/packages/google_fonts
-
<aside>
💡 처음에는 패키지 설치 방법 및 사용 설명서를 읽는데 집중해주세요!
모든 문서가 영어로 되어 있지만, 계속 보다 보면 나오는 용어들이 계속 나오는 것을 보실 수 있습니다.
</aside>
![Pub.dev.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6eaff257-24ce-4f9f-957f-249dd7030249/Pub.dev.png)
<aside>
💡 Readme와 Example 탭을 보면 해당 패키지의 사용법을 익힐 수 있습니다.
</aside>
3) 패키지 추천 사이트
Awesome Flutter : Flutter에 관련된 모든 자료를 모아둔 Github 문서입니다.
Flutter Gems : 카테고리별 Flutter 패키지 모음
<aside>
💡 직접 프로젝트를 진행하면서 패키지 사용법을 배워보도록 하겠습니다.
</aside>
02. 마이메모 앱 만들기
완성본
- 글 쓰기(Create)
- 글 읽기(Read)
- 글 수정(Update)
- 글 삭제(Delete)
회원 가입(Create)
프로필 보여주기(Read)
회원 정보 수정(Update)
회원 탈퇴(Delete)
이와 같이 CRUD는 다루는 데이터의 종류만 바뀌고 항상 기본적으로 구현하는 데이터 처리 기능입니다.
1) 프로젝트 준비
Flutter 프로젝트 생성
VSCode에서 아래와 같이 네모 모양의
Stop
버튼을 눌러 기존에 실행한 앱을 종료해주세요.View
→Command Palette
를 선택해주세요.명령어를 검색하는 팝업창이 뜨면,
flutter
라고 입력한 뒤Flutter: New Project
를 선택해주세요.Application
을 선택해주세요.프로젝트를 저장할 폴더를 선택하는 화면이 나오면
flutter
폴더를 선택한 뒤Select a folder to create the project in
버튼을 눌러 주세요.프로젝트 이름을
mymemo
로 입력해주세요.만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)
다음과 같이 프로젝트가 생성됩니다.
불필요한 힌트 숨기기
코드스니펫을 복사해서
analysis_options.yaml
파일의 24번째 라인 뒤에 붙여 넣고 저장해주세요.[코드스니펫] analysis_options.yaml
prefer_const_constructors: false prefer_const_literals_to_create_immutables: false
CRUD는 가장 기본이 되는 데이터 처리 기능입니다.
게시판 기능을 만든다면 아래 CRUD 기능이 필수적으로 제공되어야 합니다.
유저 정보를 다루는 과정도 CRUD로 표현하면 다음과 같습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8470956b-ccc2-4d75-86d8-83042bafaf22/Untitled.png)
- 어떤 의미인지 궁금하신 분들을 위해
- `main.dart` 파일을 열어보시면 파란 실선이 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d1393a08-a362-4be3-bd9b-835e184f5ef3/Untitled.png)
- 파란 줄은, 개선할 여지가 있는 부분을 VSCode가 알려주는 표시입니다.
12번째 라인에 마우스를 올리면 아래와 같이 설명이 뜹니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7fd5cee4-d935-4d2d-9c35-e11d4f3c6cae/Untitled.png)
위젯이 변경될 일이 없기 때문에 `const`라는 키워드를 앞에 붙여 상수로 선언하라는 힌트입니다.
<aside>
💡 상수로 만들면 어떤 이점이 있나요?
상수로 선언된 위젯들은 화면을 새로 고침 할 때 해당 위젯들은 변경을 하지 않기 때문에 스킵하여 성능상 이점이 있습니다.
</aside>
아래와 같이 `Icon`앞에 `const` 키워드를 붙여주시면 됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2aef9ee8-c4ef-4c42-bd08-d358c4620341/Untitled.png)
- 지금은 학습 단계이니 눈에 띄지 않도록 해주도록 하겠습니다.
9. 아래 코드스니펫을 복사해서, `main.dart`의 기존 코드를 모두 지우고 붙여 넣은 뒤 저장해 주세요.
- **[코드스니펫] main.dart**
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: Center(child: Text("메모를 작성해주세요")),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
),
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key});
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
},
),
),
);
}
}
```
- 에뮬레이터 실행하기
1. VSCode 우측 하단에 `Chrome (web-javascript)`를 클릭해주세요.
(에뮬레이터가 이미 실행중이라면 3번으로 이동해 주세요.)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5d0db764-7e21-4261-8c19-94162c4d0ba8/Untitled.png)
2. `Start Pixel 2 API 29 mobile emulator`를 선택해주세요. macOS의 경우 iOS 에뮬레이터로 진행하셔도 무방합니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5e0e7243-7d95-4b57-99df-08fd28e1b4d7/Untitled.png)
잠시 기다리면 에뮬레이터가 실행됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cf5640d3-c9a9-4105-bd40-9dcb309412f5/Untitled.png)
3. VSCode 우측 상단에 **아래 화살표**를 눌러 `Run Without Debugging`을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fba923c5-b4b0-4ddb-b381-ae8686c0315c/Untitled.png)
<aside>
💡 `Start Debugging`으로 실행해도 무방합니다. 디버깅 모드는 특정 라인에서 앱 실행을 멈추고 해당 변수에 어떤 값이 들어있는지 볼 수 있지만 `Run without debugging`이 실행 속도가 더 빨라 안내를 위와 같이 드렸습니다 🙂
</aside>
에뮬레이터에 아래와 같이 **메모장** 화면이 나오면 완료!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1092421d-a876-4748-aa68-7370d5a1cb3b/Untitled.png)
<aside>
💡 위와 같이 메모를 보여주는 `HomePage`와 메모를 작성하는 `DetailPage` 두 페이지로 구성된 앱입니다.
기본적인 레이아웃과 페이지 이동 기능은 구현되어 있고, CRUD 기능을 함께 구현해 보도록 하겠습니다.
</aside>
2) 메모 목록 조회(Read)
HomePage
는 메모의 유무, 메모의 개수라는 상태에 따라 다른 화면을 갱신해야 하므로StatefulWidget
으로 변경해줍니다.21번째 라인에
StatelessWidget
을 클릭한 뒤 Quick Fix(Ctrl/Cmd + .
)를 누른 뒤Convert to StatefulWidget
을 선택해주세요.아래와 같이
_HomePageState
라는 상태 클래스가 추가되었습니다.메모 목록을 가지고 있을 상태 변수
memoList
를 추가해 줍니다.아래 코드스니펫을 복사해서 28번째 라인 맨 뒤에 붙여 넣어 주세요.
[코드스니펫] HomePage / memoList 상태 변수
List<String> memoList = ['장보기 목록: 사과, 양파']; // 전체 메모 목록
<aside>
💡 `memoList`는 `String`만 받는 배열로 선언하였습니다.
미리 `memoList`에 `'장보기 목록: 사과, 양파'` 라는 항목을 하나 넣어 두었습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46fc74b3-4b74-48d5-8eb8-f4530e9f546d/Untitled.png)
3. `memoList`에 항목이 있을 때 `body`에 해당 항목이 보이도록 해봅시다.
코드스니펫을 복사해서 37번째 라인을 교체해 주세요.
- **[코드스니펫] HomePage / memoList 조건문**
```dart
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: Center(child: Text('메모가 존재합니다!')),
```
<aside>
💡 `조건 ? true : false` 형태의 조건문을 사용하여 조건에 따라 위젯을 다르게 보여줄 수 있습니다. (이를 “삼항 연산자” 라고 합니다.)
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a52ec0e1-e3de-4ccb-affe-afc086f9a0c3/Untitled.png)
미리 `memoList`에 `'장보기 목록: 사과, 양파’`라는 항목을 하나 넣어 두었기 때문에 `memoList.isEmpty`는 `false`가 되고, `메모가 존재합니다!` 라는 문구가 에뮬레이터에 나오는 것을 보실 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/204c4470-92f2-4ebf-821b-6b5601a53f13/Untitled.png)
4. `memoList`가 존재하는 경우, `ListView`를 이용해 보여주도록 코드스니펫을 복사해서 39번째 라인을 교체해 주세요.
- **[코드스니펫] HomePage / ListView**
```dart
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
String memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
print('$memo : 클릭 됨');
},
);
},
),
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aa6d810c-cacf-42a1-8ec9-e3f71fd0c928/Untitled.png)
<aside>
💡 `ListTile` 위젯을 활용하면 박스 안에 여러 영역에 다른 위젯을 손쉽게 배치할 수 있습니다.
![ListTile.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2a86c971-bcf5-4882-a07f-37a674357946/ListTile.png)
좀 더 상세한 설명은 아래 공식 문서를 참고해 주세요.
- **[[코드스니펫] ListTile 공식문서 URL](https://api.flutter.dev/flutter/material/ListTile-class.html)**
```dart
https://api.flutter.dev/flutter/material/ListTile-class.html
```
</aside>
저장해주시면 에뮬레이터에 메모가 추가된 것을 볼 수 있습니다.
![simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2e67a7ca-f59f-4be8-b089-a5e4c0042c09/simulator_screenshot_5FAD2CD3-E9AC-4DCD-B347-58A12C2F36AB.png)
<aside>
💡 클릭시 print문은 `view` → `Debug Console`을 선택하셔서 보실 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d7d99dcc-642e-4282-80df-8b8331584a01/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/23627feb-d8e8-43e1-89b1-e1c313210f5c/Untitled.png)
5. `memoList` 상태 변수에 `, '새 메모'` 를 추가해 봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e886bb08-2a5d-4f4d-b75d-0d6591ac1217/Untitled.png)
<aside>
💡 저장을 해도 에뮬레이터에 반영이 안되실 겁니다.
아직 우리는 변경된 상태에 따라 화면을 새로고침 해주는 코드를 작성하지 않았습니다.
따라서 `Restart` 를 해야 앱에 반영됩니다.
</aside>
우측 상단에 `Restart` 버튼을 눌러주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fed3dbef-6b58-4dfd-a519-419a3db55550/Untitled.png)
앱에 새로 추가된 메모가 보이는 것을 보실 수 있습니다.
![Screen Shot 2022-09-07 at 11.02.35 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/31a55655-af59-48d9-9dde-f5ca2027ead9/Screen_Shot_2022-09-07_at_11.02.35_PM.png)
여기까지 메모 목록 조회 기능 완성!
3) 메모 개별 조회 (Read)
메모 목록에서
ListTile
을 눌렀을 때DetailPage
로 이동하도록 합니다.아래 코드스니펫을 복사해서 59번째 라인을 지우고 붙여넣어주세요.
[코드스니펫] ListTile onTap / DetailPage 로 이동
Navigator.push( context, MaterialPageRoute(builder: (_) => DetailPage()), );
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/61e61367-b85b-4169-8c80-811d65988e2f/Untitled.png)
<aside>
💡 메모 목록의 메모 하나를 누르면 DetailPage 로 잘 넘어가는 것을 확인할 수 있습니다.
</aside>
2. 이제 `DetailPage` 에서 해당 메모 내용을 볼 수 있도록 해야겠죠!
`memoList` 와 해당 메모의 `index` 를 `DetailPage` 로 넘겨주겠습니다.
(`memoList[index]` 와 같은 식으로 개별 메모의 내용을 불러오기 위함입니다)
우선 `DetailPage` 가 메모의 내용을 변수에 담아 받도록 해야합니다. 아래 코드스니펫을 복사해서 83번째 라인 뒤에 붙여넣어주세요.
- **[코드스니펫] ListTile onTap / DetailPage 로 이동**
```dart
final List<String> memoList;
final int index;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/89a8640a-f80b-4bd4-bdf8-4d77a5fa949a/Untitled.png)
83번째 라인에 빨간 밑줄이 그어졌네요! 이는 우리가 선언한 `memoList` 와 `index` 라는 변수에 아무것도 담지 않아 생긴 일입니다.
<aside>
💡 빨간 밑줄이 그어진 코드에 마우스를 올려보면 보다 자세한 에러 메시지를 확인할 수 있습니다
</aside>
![Screen Shot 2022-09-12 at 4.11.53 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0930cb3c-6dda-4117-ae2f-28964cb38360/Screen_Shot_2022-09-12_at_4.11.53_PM.png)
아래 사진처럼 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Add final field formal parameters` 를 클릭해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/62fc6605-6198-4b1c-a7a4-fe5892871acf/Untitled.png)
아래와 같이 생성자에 넘겨받는 변수를 추가해줌으로서 문제가 해결되었습니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3e76e8c3-b1b1-448b-bc8a-8a68611f6026/Untitled.png)
그러자 이번엔 `HomePage` 에서 에러가 발생했네요! (61번째 줄, 73번째 줄)
<aside>
💡 아래 사진처럼 VS Code 우측의 빨간 부분을 클릭해서 문제가 발생한 코드 줄을 빠르게 확인할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/03eae13b-dd60-4d91-9dea-74b5892b86e1/Untitled.png)
61번째 줄에서 마우스를 에러가 난 부분 위에 올려보면, `memoList` 와 `index` 변수를 받아야 하는데 넘겨주지 않아서 생긴 문제라는 것을 확인할 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/36c0f8f5-8e35-4196-880c-2a6cbfbbda46/Untitled.png)
`Quick Fix(Ctrl/Cmd + .)`를 누른 뒤 `Add required argument ‘index’` 와 `Add required argument ‘memoList’` 를 클릭해줍니다.
![Screen Shot 2022-09-12 at 4.16.56 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9d970a5c-3522-41e0-a626-9633f19c70dd/Screen_Shot_2022-09-12_at_4.16.56_PM.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fe8b75c-4a48-4f56-b68a-b37fbcb31588/Untitled.png)
index 뒤 `null` 이 들어간 자리에 `index` 라고 작성해 위에서 만든 `index` 변수를 대입해줍니다.
memoList 뒤에도 마찬가지로 `memoList` 변수를 그대로 대입해줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0fc0df8e-5ce3-495f-9a38-297be4098a8c/Untitled.png)
`DetailPage` 괄호가 닫히는 부분(위 체크 표시) 에 쉼표를 찍어서 코드 정렬도 맞춰줍시다.
![Screen Shot 2022-09-12 at 4.24.05 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1026ebcc-e93a-4bb0-a14c-1d9fce463ec5/Screen_Shot_2022-09-12_at_4.24.05_PM.png)
나머지 에러도 마저 해결해보겠습니다.
아래 코드스니펫을 복사해 76번째~79번째 줄을 지우고 붙여넣어줍니다.
- **[코드스니펫] 메모 생성 / floatingActionButton onPressed**
```dart
String memo = ''; // 빈 메모 내용 추가
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoList.indexOf(memo),
memoList: memoList,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b3199c26-e897-4f03-bd5f-1ad945c031a3/Untitled.png)
3. 넘겨받은 변수에 담긴 내용을 화면에 표시해줍니다.
`DetailPage` 에서 받은 `memoList` 의 `index` 번째에 있는 메모 내용을 화면에 띄워줘야겠죠!
아래 코드스니펫을 복사해 102번째 줄 맨 뒤에 붙여넣어줍니다.
- **[코드스니펫] commentController 의 text 값 설정**
```dart
contentController.text = memoList[index];
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7f66f71d-fa75-4aca-b5dd-3bd1a57c825e/Untitled.png)
이제 `DetailPage` 에 해당 메모의 내용이 보이는 것을 확인하실 수 있습니다!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c1d60e22-5141-4f8c-8aeb-d15d8e459ebd/Untitled.png)
4) 메모 작성(Create)
우측 하단의
FloatingActionButton
을 클릭할 때마다 앞에서 만들어준 memoList 변수에 빈 메모를 추가 해주겠습니다.지금은 해당 버튼을 누를 때 아래와 같은 에러가 발생할텐데요.
이는 우리가 새로운 메모를
memoList
에 추가하지 않았기 때문에memoList.indexOf(memo)
가-1
을 반환해서 생기는 문제입니다.아래 코드스니펫을 복사해 76번째 줄 맨 뒤에 붙여넣어주세요.
[코드스니펫] memoList 에 memo 추가
memoList.add(memo);
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d4e62b4f-5996-415b-a2d5-42cd96a3b83b/Untitled.png)
이제 해당 버튼을 누를 때마다 `memoList` 에 빈 메모가 추가되고, `DetailPage` 로 이동하겠죠!
<aside>
💡 생성한 메모의 index 를 `indexOf` 를 통해 찾아줍니다.
`memoList.length - 1` 로 작성해도 같은 결과가 나옵니다
(add 는 배열의 맨 뒤에 요소를 추가하므로)
</aside>
2. 화면 상에 잘 나타나는지 확인해봅시다.
우측 하단의 `FloatingActionButton` 을 누르면 내용이 빈 메모의 `DetailPage`로 잘 넘어갑니다.
다시 `HomePage` 로 돌아와 보세요.
![Screen Shot 2022-09-08 at 2.39.59 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/77174237-2dff-4d4c-beb1-9537d703ac33/Screen_Shot_2022-09-08_at_2.39.59_AM.png)
메모가 새로 생성이 되지 않는군요.
`memoList` 에는 변화가 생겼지만, 화면을 새로 그려주지 않아서 생기는 문제입니다.
(`Hot Reload` 를 한 번 실행해주면 새로 생성된 메모가 보일거에요)
<aside>
💡 이럴 때는 `setState` 를 활용해 변화한 상태(변수)에 맞게 화면을 새로 그려주면 됩니다.
</aside>
아래 코드스니펫을 복사해 77번째 줄을 지우고 붙여넣어주세요. `memoList` 에 `memo` 를 추가하는 코드를 `setState` 로 감싸줬습니다.
- **[코드스니펫] setState**
```dart
setState(() {
memoList.add(memo);
});
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4dce72fc-434e-4d94-8bc1-23332bd18ddb/Untitled.png)
이제 정상적으로 추가된 빈 메모가 화면에 나타나는 것을 볼 수 있습니다.
5) 메모 수정 (Update)
DetailPage
에 있는TextField
의onChange
에 들어가는 함수를 수정하면, 텍스트가 변할 때마다 특정한 동작을 할 수 있습니다.먼저, 우측 하단의
FloatingActionButton
을 클릭하여DetailPage
로 이동해 주세요.iOS 에뮬레이터에서 키보드를 보이게 하는 방법
133번째 줄에 `print(value)` 를 입력하고 `TextField` 의 값을 변경해보면
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c20d3969-7f49-452a-8b40-2e683b853962/Untitled.png)
`TextField` 에 내용을 적는대로 콘솔에 프린트 되는 것을 볼 수 있습니다. 이제 `memoList` 에 해당 내용을 추가해주면 되겠네요!
아래 코드스니펫을 복사해 133번째 줄을 지우고 붙여넣어줍니다.
- **[코드스니펫] TextField onChanged / memoList[index] 의 값 설정**
```dart
memoList[index] = value;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7b32a817-4ae7-49c8-85f4-d0aa474152c5/Untitled.png)
자 그럼 메모를 작성하고 돌아와볼까요? `HomePage` 에 변화가 생겼는지 확인해봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/12182fc3-bc81-49f3-a1c8-f8cf8e98b266/Untitled.png)
메모 내용이 변경되지 않는군요!
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cce61e92-1a2c-4056-b048-8b36d97e7d3c/Untitled.png)
`Hot Reload` 를 실행해야 정상적으로 변경된 메모 내용이 뜨는 것을 확인할 수 있습니다. 이는 HomePage 의 화면이 갱신되지 않았다는 것인데요.
<aside>
💡 기존에 우리는 `StatefulWidget` 의 `setState` 를 활용해 **해당 위젯의 화면**을 갱신할 수 있었습니다. 그러나 위 경우는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
</aside>
<aside>
💡 `HomePage` 의 `setState` 함수를 통으로 `DetailPage` 로 넘겨주는 방법도 있지만, 보다 편리하게 코드를 작성하기 위해서는 **상태 관리(State Management)** 라는 것을 사용하게 됩니다.
우선 메모 삭제를 구현한 후, 아래에서 이에 대해 보다 자세히 다뤄보겠습니다.
</aside>
6) 메모 삭제 (Delete)
삭제 버튼 클릭 이벤트는 112번째 줄,
DetailPage
의 우상단에 있는IconButton
위젯의onPressed
의 함수로 받을 수 있습니다.먼저 삭제 버튼을 누르는 경우
Dialog
를 띄워보도록 하겠습니다.코드스니펫을 복사해 113번째 라인 맨 뒤에 붙여넣어주세요.
[코드스니펫] DetailPage / showDialog
showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), ); }, );
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f0333d17-7044-46c2-88c2-6db40c8e8496/Untitled.png)
삭제 버튼을 눌러서 `Dialog`를 확인해보세요
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f84a2a93-7d23-4b9e-80d0-6463fdf86384/Untitled.png)
<aside>
💡 Dialog 배경을 클릭하여 끌 수 있습니다.
</aside>
2. `AlertDialog`에 `취소`와 `확인` 버튼을 추가해 보도록 하겠습니다.
코드스니펫을 복사해 118번째 라인 뒤에 붙여 넣어주세요.
- **[코드스니펫] DetailPage / Dialog actions**
```dart
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
```
<aside>
💡 `AlertDialog`의 `actions`는 배열로 원하는 위젯을 넣을 수 있는 파라미터 입니다.
</aside>
<aside>
💡 버튼을 클릭하는 경우 `Navigator.pop(context);`를 호출하여 `Dialog`를 종료할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/661d9525-c0d1-49f7-a577-a1d7a6d16029/Untitled.png)
![Screen Shot 2022-09-12 at 6.31.28 PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/da8304ff-d203-404c-8f18-11931aec5805/Screen_Shot_2022-09-12_at_6.31.28_PM.png)
3. `확인` 버튼 클릭 시 `MemoList`에서 항목을 삭제해 보도록 하겠습니다.
코드스니펫을 복사해 130번째 줄을 지우고 붙여넣어주세요.
- **[코드스니펫] HomePage / delete memo**
```dart
memoList.removeAt(index); // index에 해당하는 항목 삭제
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
```
<aside>
💡 `List.removeAt(index)`를 이용하면 배열에서 index에 해당하는 항목을 삭제할 수 있습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ef4c648c-8295-44ae-9585-dd7787622b4f/Untitled.png)
4. 저장 후 다이얼로그를 다시 띄우고 `확인` 버튼을 누르면..
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9da3d5a0-cc5c-49bb-af18-ee65ee35478a/Untitled.png)
이번에도 “메모 수정” 때와 마찬가지로, Hot Reload 를 활용해 화면을 새로고침 해줘야 변경사항이 반영되는 것을 확인하실 수 있습니다.
<aside>
💡 위와 같은 상황에서는 `DetailPage` 에서 발생한 변화에 따라 `HomePage` 의 화면을 갱신해줘야 합니다.
서로 다른 페이지에서 변수를 공유하고, 변수의 값을 바꾸며 여러 페이지의 화면을 한꺼번에 갱신해주기 위해 **상태 관리(State Management)** 라는 것을 배우고 적용해보겠습니다.
</aside>
main.dart
완성 코드import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { List<String> memoList = ['장보기 목록: 사과, 양파', '새 메모']; // 전체 메모 목록 @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { String memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(CupertinoIcons.pin), onPressed: () { print('$memo : pin 클릭 됨'); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo, maxLines: 3, overflow: TextOverflow.ellipsis, ), onTap: () { // 아이템 클릭시 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, memoList: memoList, ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 String memo = ''; // 빈 메모 내용 추가 setState(() { memoList.add(memo); }); Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoList.indexOf(memo), memoList: memoList, ), ), ); }, ), ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.memoList, required this.index}); final List<String> memoList; final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { contentController.text = memoList[index]; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoList.removeAt(index); // index에 해당하는 항목 삭제 Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoList[index] = value; }, ), ), ); } }
03. 상태 관리 패키지 Provider 준비하기
상태 관리(State Management)의 필요성
1) Provider 패키지 추가
코드스니펫을 복사해서 새 탭에서 열어주세요.
-
https://pub.dev/packages/provider/install
-
Provider 패키지 Install 탭이 아래와 같이 나오면,
flutter pub add provider
명령어 옆에 아이콘을 눌러 복사해 주세요.View
→Terminal
을 열어 주세요.아래와 같이 복사한 코드를 붙여넣고 엔터를 눌러주세요.
그러면 아래와 같이 실행이 됩니다.
pubspec.yaml
파일에 39번째 라인에 아래와 같이 Provider가 있으면 정상 설치 완료입니다.
2) MemoService 만들기
lib
폴더에서 우클릭을 해주신 뒤New File
을 선택해주세요.파일 이름은
memo_service.dart
로 짓도록 하겠습니다.코드스니펫을 복사해
memo_service.dart
파일에 붙여 넣어주세요.[코드스니펫] MemoService / init
import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, }); String content; } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; }
<aside>
💡 **MemoService**의 `memoList`가 변경되는 경우 해당 값을 보여주는 화면들을 갱신시켜 주는 기능을 구현하기 위해 `ChangeNotifier` 클래스의 기능을 물려 받았습니다.
`ChangeNotifier`를 상속 받은 경우 `notifylisteners();`를 호출하여 위젯들을 갱신하는 기능을 사용할 수 있습니다. 자세한 사용법은 뒤쪽에서 알아보도록 하겠습니다.
</aside>
<aside>
💡 앞으로 `Memo` 관련 데이터는 모두 **MemoService** 에서 담당할 예정이므로, 이전에 HomePage에서 구현했던 `memoList` 배열을 **MemoService** 에 추가했습니다.
17, 18번째 `Memo`는 바로 테스트를 진행할 수 있도록 넣은 가짜(dummy) 데이터입니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/831f2d05-82e5-4307-a4ff-095cf682151a/Untitled.png)
3) Provider 패키지 설정
코드스니펫을 복사해
main.dart
파일의 5번째 라인을 지우고 붙여넣어주세요.[코드스니펫] Provider init
runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), );
<aside>
💡 `MultiProvider`로 `MyApp`을 감싸서 모든 위젯들의 최상단에 `provider`들을 등록해줍니다. `MultiProvider`는 위젯트리 꼭대기에 여러 `Service` 들을 등록할 수 있도록 만들 때 사용합니다.
</aside>
<aside>
💡 **MemoService**는 `memoList` 값이 변하는 경우 `HomePage`에 변경사항을 알려주도록 구현해야 하므로 `ChangeNotifierProvider`로 **Provider**에 등록해줍니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/33619a81-8d46-402f-ac51-c5fe41edc020/Untitled.png)
2. **Provider**와 **MemoService**가 `Import` 되지 않아 에러가 발생합니다.
6번째 라인에 `MultiProvider`를 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 눌르고 `Import 'package:provider/provider.dart';`를 선택해주세요.
<aside>
💡 만약 `import 'package:provider/provider.dart';`가 안보이신다면 `pubspec.yaml` 파일을 열고 저장 단축키를 눌러주세요.
저장 단축키
window : `Ctrl + S`
macOS : `Cmd + S`
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/8580c991-b81d-4001-90d3-7e21e45d081e/Untitled.png)
3. 마찬가지로 9번째 라인 **MemoService**를 클릭하신 뒤 `import 'memo_service.dart';`를 선택해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0135ea61-101f-4f4d-8de6-305f851db553/Untitled.png)
**Provider** 설정이 완료 되었습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b44f89cb-ce78-4833-b8f8-cb8ee4ef104f/Untitled.png)
4. `Restart`를 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
이제 `Provider` 를 사용할 준비가 되었습니다!
04. 마이메모 앱에 Provider 적용하기
1) 메모 목록 조회(Read)
HomePage 전체 위젯을
Consumer
로 감싸주겠습니다.main.dart
파일에 43번째 라인에Scaffold
를 우클릭하여Refactor(Ctrl + Shift + R)
을 선택하고,Wrap with Builder
를 선택해주세요.아래와 같이
Scaffold
위젯을Builder
위젯이 감싸집니다.Consumer
위젯과 형식이 비슷한Builder
를 사용해 형식을 잡아줬습니다.아래 이미지와 같이 43번째 라인에
Builder
를Consumer<MemoService>
로 변경해주세요.에러를 해결하겠습니다.
아래 이미지와 같이
,memoService, child
를 43번째 라인의 context 뒤에 추가해주세요.그리고 줄 정렬을 예쁘게 해주기 위해 103번째 라인의 중괄호와 소괄호 사이에 콤마(
,
)를 찍은 뒤 저장해주세요.저장하면 코드가 정렬됩니다.
코드스니펫을 복사해 44번째 라인 뒤에 붙여넣어 주세요.
[코드스니펫] MemoService에서 memoList 가져오기
// memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList;
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f52d5fa9-d3bf-4749-97c2-84112f9b6c87/Untitled.png)
5. `Provider` 를 사용해 모든 `Memo` 데이터는 `MemoService` 에서 관리하기로 했습니다.
39번째 줄에 있는 `memoList` 는 더이상 사용하지 않으므로 지워줍니다.
지우고 난 후 코드는 아래와 같습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a4375405-ca68-4ff3-95cd-40ff0df47381/Untitled.png)
6. 에러들을 해결하겠습니다.
<aside>
💡 에러를 모두 해결하고 나면 자연스럽게 **메모 목록 조회(Read)** 기능이 구현되어 있을 겁니다!
</aside>
이전에는 `String` 자료형의 메모 데이터를 사용했다면, 이번엔 `memo_service.dart` 에서 `Memo` 라는 클래스로 데이터의 틀을 잡고, 각각의 메모 데이터를 객체의 형태로 만들어 활용한다는 점을 유의해주세요.
이렇게 데이터의 형식(자료형)이 바뀌었기 때문에 코드상에 에러가 발생합니다. 55번째 줄의 에러 위에 마우스를 올려봅시다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ff555c78-1f86-42c0-a36b-f27a37fc52ee/Untitled.png)
`String` 자료형의 변수인 `memo` 에 `Memo` 자료형의 값이 들어갈 수 없다는 뜻이군요!
Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `‘Change ‘String’ to ‘Memo’ type annotation` 을 눌러 변수의 타입(자료형)을 바꿔줍니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d556b38b-9f54-4db7-ae9f-7556f474eba9/Untitled.png)
코드가 아래와 같이 바뀌면서 에러가 사라지는 것을 볼 수 있습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5978d022-8b8e-4df5-81d2-81cd5152713a/Untitled.png)
66번째 줄의 `memo` 를 `memo.content` 로 바꿔줍니다.
<aside>
💡 `Text` 위젯은 `String` 자료형의 값을 받아서 보여주는 위젯입니다. 메모의 내용인 `content`를 넘겨줘야겠죠.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b2e67e54-d29e-44e2-b7a1-c343738c3f4b/Untitled.png)
72-80 번째 줄을 지워줍니다. **메모 개별 조회 (Read)** 기능은 아래에서 다시 구현하겠습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c6b4bba1-52df-44af-81f8-cefb933930d5/Untitled.png)
80-92 번째 줄을 지워줍니다. **메모 생성 (Create)** 기능도 아래에서 새로 구현하겠습니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ecce9a27-36f2-4061-b729-56fb45871976/Untitled.png)
7. `Restart`를 클릭해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c26d3381-5b05-433e-8429-a25e46733820/Untitled.png)
지금 화면에 보이는 메모 목록은 우리가 생성해준 더미 데이터들입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0c74725c-5ac3-41d4-89a7-cb243aa72de8/Untitled.png)
`Provider` 를 활용한 **메모 목록 조회 기능(Read)** 을 구현했습니다!
2) 메모 개별 조회 (Read)
이제 메모 데이터를
MemoService
에서 관리하니,DetailPage
가memoList
를 따로 가지고 있을 필요가 없겠죠.아래 코드스니펫을 복사해
main.dart
90-92번째 라인을 모두 지우고 붙여넣습니다.[코드스니펫] DetailPage / 파라미터
DetailPage({super.key, required this.index});
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fd990739-5a05-4fbe-bdf9-42dff52aabaf/Untitled.png)
2. `memoService` 에서 `memoList` 를 가져와 `index` 번째에 있는 메모를 뽑아보겠습니다.
코드스니펫을 복사해 `main.dart` 98번째 라인을 지우고 붙여넣어주세요.
- **[코드스니펫] DetailPage / context.read 로 memo 1개 조회**
```dart
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/87501087-8236-4773-b4ac-87f54e46141b/Untitled.png)
<aside>
💡 `context.read<클래스명>();`를 이용하면 위젯 트리 상단에 있는 Provider로 등록한 클래스에 접근할 수 있습니다.
변화에 따라 화면을 새로 그려줄 필요가 있을 때는 `Consumer` 를 사용하며, 화면을 새로고침할 필요없이 MemoService 의 변수나 함수만 이용하고자 한다면 `context.read<클래스명>()` 를 사용해도 됩니다.
</aside>
3. `main.dart` 125번째 라인, 157번째 라인에 있는 에러를 각각 없애보겠습니다.
아래에서 메모의 수정 및 삭제를 따로 구현할 것이기 때문에 지금은 코드를 모두 주석처리 해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b82a2e83-02c0-4217-bd24-6200214e89bd/Untitled.png)
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/733b6f3d-b91d-42b2-b0b4-61e530c90434/Untitled.png)
4. `ListTile` 을 클릭시 `DetailPage` 로 이동하도록 하겠습니다.
`main.dart` 의 71번째 라인 뒤에 아래 코드스니펫을 복사해 붙여넣어주세요.
- **[코드스니펫] HomePage 에서 DetailPage 로 이동하는 Navigator**
```dart
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aabfe72e-6353-4771-b545-54fd4db6ee19/Untitled.png)
**메모 개별 조회(Read)** 기능이 구현되었습니다. `ListTile` 을 눌러 상세페이지로 이동해보세요!
![Screen Shot 2022-09-13 at 4.41.46 AM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/20af2c23-98bd-4290-ba7b-55f93ea7be28/Screen_Shot_2022-09-13_at_4.41.46_AM.png)
3) 메모 작성 (Create)
먼저 코드스니펫을 복사해
MemoService
에Memo
를 추가하는 함수를 구현해봅시다.memo_service.dart
의 19번째 라인 맨 뒤에 붙여넣습니다.[코드스니펫] MemoService / createMemo
createMemo({required String content}) { Memo memo = Memo(content: content); memoList.add(memo); }
<aside>
💡 **MemoService**에 `memoList`에 대한 CRUD 기능을 담당하는 함수를 추가할 예정입니다. 그 중 Create를 담당하는 `createMemo` 함수를 생성하였습니다.
</aside>
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c584b852-baa2-4db3-84a2-cfb725acc092/Untitled.png)
<aside>
💡 `String` 자료형의 `content` 변수를 받아 `Memo`를 생성하고, 이를 `memoList` 맨 뒤에 추가하는 로직을 작성했습니다.
</aside>
2. `HomePage` 에서 `FloatingActionButton` 을 누를 때, 방금 만든 `createMemo` 함수를 호출해 내용이 비어있는 메모가 추가되도록 하겠습니다.
아래 코드스니펫을 복사해 `main.dart` 의 87번째 라인 맨 뒤에 붙여넣습니다
- **[코드스니펫] HomePage / createMemo 호출**
```dart
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b7306c0c-3194-4004-97c0-3f562b5b79cb/Untitled.png)
<aside>
💡 `memoList` 의 맨 뒤에 memo가 추가되었으므로, `memoList.length - 1` 의 인덱스로 해당 memo에 접근합니다.
</aside>
3. 저장한 뒤 에뮬레이터에서 `FloatingActionButton` 를 눌러 메모를 추가해봅시다.
<aside>
💡 메모를 추가했지만 `HomePage` 에는 아무런 변화가 없습니다.
`Hot Reload` 를 해보면 비로소 메모가 추가되는 것을 확인할 수 있는데요. 이는 `memoList` 에 memo 가 추가되었지만 화면의 갱신이 일어나지 않았다는 것을 의미합니다.
</aside>
4. 이를 해결하기 위해 코드스니펫을 복사해 **MemoService**에 23번째 라인 맨 뒤에 붙여 넣어주세요.
- **[코드스니펫] notifyListeners**
```dart
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/66f3a459-46a5-42d7-b186-247a49975361/Untitled.png)
<aside>
💡 **MemoService**에서 `notifyListeners();`를 호출해야 `Consumer<MemoService>`의 `builder` 에 있는 함수를 호출해 화면이 갱신됩니다.
</aside>
이제 에뮬레이터에서 `FloatingActionButton` 를 누를 때마다 `HomePage` 에 빈 메모가 추가되는 것을 확인할 수 있습니다.
4) 메모 수정 (Update)
먼저
MemoService
에 메모를 수정하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해memo_service.dart
의 25번째 라인 맨 뒤에 붙여넣습니다[코드스니펫] MemoService / updateMemo
updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; notifyListeners(); }
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b88fd71-7b53-40d9-a126-b25730ecb281/Untitled.png)
<aside>
💡 `index` 와 `content` 변수를 받아 `memoList` 의 index 번째 memo 의 `content` 를 바꿔주는 로직을 작성했습니다.
또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
</aside>
2. `DetailPage` 에서 `TextField` 에 텍스트를 입력할 때마다 메모 수정이 일어나도록 하겠습니다.
아래 코드스니펫을 복사해 `main.dart` 의 174번째 라인을 지우고 붙여넣습니다
- **[코드스니펫] DetailPage / TextField onChange**
```dart
memoService.updateMemo(index: index, content: value);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/1debffca-b607-4396-b4ca-4fc9af942580/Untitled.png)
<aside>
💡 `TextField` 의 값이 변할 때, 즉 타이핑할때마다 `memoService` 의 `updateMemo` 를 호출하도록 했습니다.
</aside>
3. `DetailPage` 에서 메모를 작성하고 `HomePage` 로 돌아오면 변경 내역이 잘 반영되어있는 것을 확인할 수 있습니다.
5) 메모 삭제 (Delete)
먼저
MemoService
에 메모를 삭제하는 로직을 추가하겠습니다. 아래 코드스니펫을 복사해memo_service.dart
의 31번째 라인 맨 뒤에 붙여넣습니다.[코드스니펫] MemoService / deleteMemo
deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); }
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/532b94be-5756-4b30-89ab-b22b4be58437/Untitled.png)
<aside>
💡 `index` 변수를 받아 `memoList` 의 index 번째 memo 를 삭제하는 로직을 작성했습니다.
또, `notifyListeners()` 를 호출해 `Consumer<MemoService>` 의 `builder` 부분에 있는 위젯을 다시 그려줍니다.
</aside>
2. `DetailPage` 에서 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나도록 해봅시다. 아래 코드스니펫을 복사해 `main.dart` 의 142번째 라인을 지우고 붙여넣습니다
- **[코드스니펫] DetailPage / TextButton onPressed**
```dart
memoService.deleteMemo(index: index);
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6ec414aa-b3a9-4cbb-97b9-14f85dbcca75/Untitled.png)
<aside>
💡 삭제 버튼을 누르면 뜨는 `Dialog` 가 뜨고, 여기서 `확인`을 누르면 삭제가 일어나며, 뒤로가기를 두번 (Dialog 닫기 → DetailPage 닫고 HomePage 로 이동) 실행합니다.
</aside>
3. `DetailPage` 에서 삭제 버튼을 눌러 구현이 잘 되었는지 확인합니다.
6) Provider 사용법 정리
전역적으로 사용되는 데이터를 담당할 서비스로 만들고, 해당 데이터에 대한 CRUD를 모두 해당 서비스에서 구현합니다.
class MemoService extends ChangeNotifier { ... void deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); // Consumer의 builder 함수를 호출하여 화면 갱신 } }
Provider 패키지를 이용하여 최상단 위젯 서비스를 등록해 줍니다.
위젯트리 꼭대기에 있는 Provider로 등록한 클래스에 접근 방법
Consumer<클래스명>
: 클래스 정보 갱신시 함께 새로고침 할 때 사용context.read<클래스명>()
: 화면을 새로고침할 필요 없이, 일회성으로 서비스의 변수나 함수에 접근하고 싶은 경우에 사용
7) 리팩토링
showDeleteDialog
메소드(함수)로 분리하기126번째 줄에
showDialog
를 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택하고,Extract Method
를 선택해 주세요.메소드(함수)의 이름을 입력하라고 뜨면
showDeleteDialog
라고 입력한 뒤 엔터를 누르고 저장(Ctrl/Cmd + S
)를 눌러주세요.그러면 아래와 같이 126번째 줄이 생성된 함수를 호출하도록 바뀝니다.
그리고 153번째 라인으로 내려가보면
showDeleteDialog
라는 함수가 생성되어 있는 것을 볼 수 있습니다.153번째 줄을 보면 함수의 반환 타입이
Future<dynamic>
이라고 되어있는데, 이 부분을void
로 변경하고, 155번째 줄에 있는return
을 삭제해주세요.저장한 뒤, 메모를 하나 삭제해보면 정상적으로 작동하는 것을 확인할 수 있습니다.
최종 코드
main.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'memo_service.dart'; void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Consumer<MemoService>( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList; return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { Memo memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(CupertinoIcons.pin), onPressed: () { print('$memo : pin 클릭 됨'); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo.content, maxLines: 3, overflow: TextOverflow.ellipsis, ), onTap: () { // 아이템 클릭시 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 memoService.createMemo(content: ''); Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoService.memoList.length - 1, ), ), ); }, ), ); }, ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.index}); final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { MemoService memoService = context.read<MemoService>(); Memo memo = memoService.memoList[index]; contentController.text = memo.content; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDeleteDialog(context, memoService); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoService.updateMemo(index: index, content: value); }, ), ), ); } void showDeleteDialog(BuildContext context, MemoService memoService) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoService.deleteMemo(index: index); Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); } }
memo_service.dart
import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, }); String content; } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content); memoList.add(memo); notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침 } updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; notifyListeners(); } deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); } }
05. shared_preferences 를 이용해 메모 데이터 기기에 저장하기
shared_preferences 패키지를 사용하는 이유
1) shared_preferences 패키지 설치
패키지를 설치해 보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.
[코드스니펫] pub.dev shared_preferences
https://pub.dev/packages/shared_preferences/install
`flutter pub add shared_preferences` 우측에 아이콘을 눌러 복사해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3c859621-73d6-42cb-86eb-99d54bfeac18/Untitled.png)
2. `View` → `Terminal`을 열고 복사한 내용을 붙여 넣은 뒤 엔터를 눌러 실행해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9ad5c331-063e-4fce-81d8-0148093ce7b1/Untitled.png)
`pubspec.yaml`파일을 열어서 40번째 라인에 `shared_preferences`를 확인해 주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c4342608-4cf6-4955-a4c6-c7dd417fc167/Untitled.png)
3. 패키지 설치가 끝나셨다면 우측 상단의 정지 버튼을 눌러 앱을 끈 후
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ed82c5f1-f39f-48a9-a11f-5ea54ca69fa5/Untitled.png)
`Run without debugging` 을 눌러 앱을 다시 설치해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c40bd256-eaf2-48ff-ba16-2f51c5da6701/Untitled.png)
앱이 재설치가 된 후에 `shared_preferences` 를 사용할 수 있습니다.
2) shared_preferences 사용법
사용 준비
SharedPreferences
를 이용하려면 먼저SharedPreferences
인스턴스를 가져와야합니다.// 인스턴스 생성 SharedPreferences prefs = await SharedPreferences.getInstance();
값 저장하기
SharedPreferences는 데이터를 Key와 Value로 구성된 Map 형태로 데이터를 저장합니다. 저장시 사용되는Key
는 원하는 이름으로 정하시면 됩니다.prefs.setString("username", "John Doe");
값 불러오기
저장할 때 사용한username
이라는Key
를 이용해 다시 값을 가져올 수 있습니다. 저장된 값이 없는 경우null
을 반환하는 점 유의해 주세요.String? value = prefs.getString("username");
3) 기기에 memoList
데이터 저장하기
먼저 전역으로
prefs
라는 변수를 선언해 코드 어디서든SharedPreferences
객체를 사용할 수 있도록 합니다. 아래 코드스니펫을 복사해main.dart
의 7번째 줄을 지우고 붙여넣어주세요.[코드스니펫] 전역 변수 prefs 선언 / main 함수 수정
late SharedPreferences prefs; void main() async { WidgetsFlutterBinding.ensureInitialized(); prefs = await SharedPreferences.getInstance();
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/67734020-ec2c-41f2-990d-419ef1f0cc28/Untitled.png)
7번째 라인에 `SharedPreferences`을 클릭한 뒤 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library…` 를 선택해주세요. `shared_preferences` 패키지를 import 하지 않아 발생하는 문제입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fb816b4-9425-4df6-828b-a8f9e60b5305/Untitled.png)
아래와 같이 import 가 되면 에러가 해결됩니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a31272db-f17e-48b6-abc0-e2e027d5851b/Untitled.png)
2. `Memo` 객체를 `Map` 자료형으로 바꿔주는 함수 **toJson**, `Map` 자료형에서 다시 `Memo` 객체를 복원하는 함수 **fromJson** 을 `Memo` 클래스 내에 작성해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 11번째 라인 맨 마지막에 붙여넣어주세요.
- **[코드스니펫] Memo toJson, fromJson**
```dart
Map toJson() {
return {'content': content};
}
factory Memo.fromJson(json) {
return Memo(content: json['content']);
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c8132838-be7a-42de-8395-f5e01ff2cbfc/Untitled.png)
3. **MemoService** 에 `memoList` 를 기기에 저장하는 함수 **saveMemoList** 와, 저장된 `memoList` 를 다시 복원하는 함수 **loadMemoList** 를 추가합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 44번째 라인 뒤에 붙여넣어주세요.
- **[코드스니펫] saveMemoList / loadMemoList**
```dart
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e500d5a7-5c03-45c9-a223-9dcaae341ef9/Untitled.png)
50번째 라인에 에러가 발생한 부분에서 Quick Fix(`Ctrl/Cmd + .`)를 누른 뒤 `Import library 'dart:convert'` 를 눌러 에러를 해결해주세요.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c9fe2dbe-b948-4bca-bc8c-edfbf1af9b8b/Untitled.png)
<aside>
💡 각 과정을 거치며 데이터가 어떻게 변하는지 주석으로 작성해뒀습니다. 대략적인 흐름은 아래와 같습니다.
1) `saveMemoList`
`**List<Memo>**` ⇒ (toJson) ⇒ `**List<Map>**` ⇒ (jsonEncode) ⇒ **`String`**
2) `loadMemoList`
`**String**` ⇒ (jsonDecode) ⇒ `**List<Map>**` ⇒ (fromJson) ⇒ `**List<Memo>**`
</aside>
4. 메모를 생성, 변경, 삭제할 때마다 메모를 저장하도록 합니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 `MemoService` 클래스 함수들에 있는 `notifyListeners()` 뒤에 **모두** 붙여넣어줍니다.
- **[코드스니펫] memoList 기기에 저장**
```dart
saveMemoList();
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/49bda047-bee9-4cd3-9919-c6d3e7e98da2/Untitled.png)
5. **MemoService** 를 시작할 때 저장된 메모를 불러와서 `memoList` 변수에 담아주도록 생성자를 수정해줍니다. 아래 코드스니펫을 복사해 `memo_service.dart` 의 25번째 줄 맨 뒤에 붙여넣어주세요.
- **[코드스니펫] memoList 불러오기**
```dart
MemoService() {
loadMemoList();
}
```
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d0034163-f49b-4ab4-9769-27c1c4a932ba/Untitled.png)
6. 자 이제 메모를 생성하고, 우측 상단의 `Restart` 버튼을 눌러주세요. 앱을 재부팅하는 것과 동일한 동작입니다.
![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a3804f24-5072-4989-9dae-d34b77c9b50d/Untitled.png)
메모가 삭제되지 않고 잘 남아있는 것을 확인할 수 있습니다. 이는 메모 데이터가 `shared_preferences` 를 통해 기기에 저장되고, 불러와지기 때문입니다.
최종 코드
main.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'memo_service.dart'; late SharedPreferences prefs; void main() async { WidgetsFlutterBinding.ensureInitialized(); prefs = await SharedPreferences.getInstance(); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Consumer<MemoService>( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList; return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { Memo memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(CupertinoIcons.pin), onPressed: () { print('$memo : pin 클릭 됨'); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo.content, maxLines: 3, overflow: TextOverflow.ellipsis, ), onTap: () { // 아이템 클릭시 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 memoService.createMemo(content: ''); Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoService.memoList.length - 1, ), ), ); }, ), ); }, ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.index}); final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { MemoService memoService = context.read<MemoService>(); Memo memo = memoService.memoList[index]; contentController.text = memo.content; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDeleteDialog(context, memoService); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoService.updateMemo(index: index, content: value); }, ), ), ); } void showDeleteDialog(BuildContext context, MemoService memoService) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoService.deleteMemo(index: index); Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); } }
memo_service.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, }); String content; Map toJson() { return {'content': content}; } factory Memo.fromJson(json) { return Memo(content: json['content']); } } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { MemoService() { loadMemoList(); } List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content); memoList.add(memo); notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침 saveMemoList(); } updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; notifyListeners(); saveMemoList(); } deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); saveMemoList(); } saveMemoList() { List memoJsonList = memoList.map((memo) => memo.toJson()).toList(); // [{"content": "1"}, {"content": "2"}] String jsonString = jsonEncode(memoJsonList); // '[{"content": "1"}, {"content": "2"}]' prefs.setString('memoList', jsonString); } loadMemoList() { String? jsonString = prefs.getString('memoList'); // '[{"content": "1"}, {"content": "2"}]' if (jsonString == null) return; // null 이면 로드하지 않음 List memoJsonList = jsonDecode(jsonString); // [{"content": "1"}, {"content": "2"}] memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList(); } }
06. 숙제 - 마이메모 추가 기능 구현
1) 메모 핀 기능 구현
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'memo_service.dart'; late SharedPreferences prefs; void main() async { WidgetsFlutterBinding.ensureInitialized(); prefs = await SharedPreferences.getInstance(); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Consumer<MemoService>( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList; return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { Memo memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(memo.isPinned ? CupertinoIcons.pin_fill : CupertinoIcons.pin), onPressed: () { memoService.updatePinMemo(index: index); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo.content, maxLines: 3, overflow: TextOverflow.ellipsis, ), onTap: () { // 아이템 클릭시 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 memoService.createMemo(content: ''); Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoService.memoList.length - 1, ), ), ); }, ), ); }, ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.index}); final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { MemoService memoService = context.read<MemoService>(); Memo memo = memoService.memoList[index]; contentController.text = memo.content; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDeleteDialog(context, memoService); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoService.updateMemo(index: index, content: value); }, ), ), ); } void showDeleteDialog(BuildContext context, MemoService memoService) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoService.deleteMemo(index: index); Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); } }
memo_service.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, this.isPinned = false, }); String content; bool isPinned; Map toJson() { return { 'content': content, 'isPinned': isPinned, }; } factory Memo.fromJson(json) { return Memo( content: json['content'], isPinned: json['isPinned'] ?? false, ); } } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { MemoService() { loadMemoList(); } List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content); memoList.add(memo); notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침 saveMemoList(); } updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; notifyListeners(); saveMemoList(); } updatePinMemo({required int index}) { Memo memo = memoList[index]; memo.isPinned = !memo.isPinned; memoList = [ ...memoList.where((element) => element.isPinned), ...memoList.where((element) => !element.isPinned) ]; notifyListeners(); saveMemoList(); } deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); saveMemoList(); } saveMemoList() { List memoJsonList = memoList.map((memo) => memo.toJson()).toList(); // [{"content": "1"}, {"content": "2"}] String jsonString = jsonEncode(memoJsonList); // '[{"content": "1"}, {"content": "2"}]' prefs.setString('memoList', jsonString); } loadMemoList() { String? jsonString = prefs.getString('memoList'); // '[{"content": "1"}, {"content": "2"}]' if (jsonString == null) return; // null 이면 로드하지 않음 List memoJsonList = jsonDecode(jsonString); // [{"content": "1"}, {"content": "2"}] memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList(); } }
2) 메모 수정 시간 저장
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'memo_service.dart'; late SharedPreferences prefs; void main() async { WidgetsFlutterBinding.ensureInitialized(); prefs = await SharedPreferences.getInstance(); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Consumer<MemoService>( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList; return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { Memo memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(memo.isPinned ? CupertinoIcons.pin_fill : CupertinoIcons.pin), onPressed: () { memoService.updatePinMemo(index: index); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo.content, maxLines: 3, overflow: TextOverflow.ellipsis, ), trailing: Text(memo.updatedAt == null ? "" : memo.updatedAt.toString().substring(0, 19)), onTap: () { // 아이템 클릭시 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, ), ), ); }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 memoService.createMemo(content: ''); Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoService.memoList.length - 1, ), ), ); }, ), ); }, ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.index}); final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { MemoService memoService = context.read<MemoService>(); Memo memo = memoService.memoList[index]; contentController.text = memo.content; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDeleteDialog(context, memoService); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoService.updateMemo(index: index, content: value); }, ), ), ); } void showDeleteDialog(BuildContext context, MemoService memoService) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoService.deleteMemo(index: index); Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); } }
memo_service.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, this.isPinned = false, this.updatedAt, }); String content; bool isPinned; DateTime? updatedAt; Map toJson() { return { 'content': content, 'isPinned': isPinned, 'updatedAt': updatedAt?.toIso8601String(), }; } factory Memo.fromJson(json) { return Memo( content: json['content'], isPinned: json['isPinned'] ?? false, updatedAt: json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']), ); } } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { MemoService() { loadMemoList(); } List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content, updatedAt: DateTime.now()); memoList.add(memo); notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침 saveMemoList(); } updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; memo.updatedAt = DateTime.now(); notifyListeners(); saveMemoList(); } updatePinMemo({required int index}) { Memo memo = memoList[index]; memo.isPinned = !memo.isPinned; memoList = [ ...memoList.where((element) => element.isPinned), ...memoList.where((element) => !element.isPinned) ]; notifyListeners(); saveMemoList(); } deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); saveMemoList(); } saveMemoList() { List memoJsonList = memoList.map((memo) => memo.toJson()).toList(); // [{"content": "1"}, {"content": "2"}] String jsonString = jsonEncode(memoJsonList); // '[{"content": "1"}, {"content": "2"}]' prefs.setString('memoList', jsonString); } loadMemoList() { String? jsonString = prefs.getString('memoList'); // '[{"content": "1"}, {"content": "2"}]' if (jsonString == null) return; // null 이면 로드하지 않음 List memoJsonList = jsonDecode(jsonString); // [{"content": "1"}, {"content": "2"}] memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList(); } }
3) 빈 메모 삭제
1) 구현 목표
2) 숙제 답안
main.dart
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'memo_service.dart'; late SharedPreferences prefs; void main() async { WidgetsFlutterBinding.ensureInitialized(); prefs = await SharedPreferences.getInstance(); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MemoService()), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: HomePage(), ); } } // 홈 페이지 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return Consumer<MemoService>( builder: (context, memoService, child) { // memoService로 부터 memoList 가져오기 List<Memo> memoList = memoService.memoList; return Scaffold( appBar: AppBar( title: Text("mymemo"), ), body: memoList.isEmpty ? Center(child: Text("메모를 작성해 주세요")) : ListView.builder( itemCount: memoList.length, // memoList 개수 만큼 보여주기 itemBuilder: (context, index) { Memo memo = memoList[index]; // index에 해당하는 memo 가져오기 return ListTile( // 메모 고정 아이콘 leading: IconButton( icon: Icon(memo.isPinned ? CupertinoIcons.pin_fill : CupertinoIcons.pin), onPressed: () { memoService.updatePinMemo(index: index); }, ), // 메모 내용 (최대 3줄까지만 보여주도록) title: Text( memo.content, maxLines: 3, overflow: TextOverflow.ellipsis, ), trailing: Text(memo.updatedAt == null ? "" : memo.updatedAt.toString().substring(0, 19)), onTap: () async { // 아이템 클릭시 await Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: index, ), ), ); if (memo.content.isEmpty) { memoService.deleteMemo(index: index); } }, ); }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () async { // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동 memoService.createMemo(content: ''); await Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoService.memoList.length - 1, ), ), ); if (memoList[memoService.memoList.length - 1].content.isEmpty) { memoService.deleteMemo(index: memoList.length - 1); } }, ), ); }, ); } } // 메모 생성 및 수정 페이지 class DetailPage extends StatelessWidget { DetailPage({super.key, required this.index}); final int index; TextEditingController contentController = TextEditingController(); @override Widget build(BuildContext context) { MemoService memoService = context.read<MemoService>(); Memo memo = memoService.memoList[index]; contentController.text = memo.content; return Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () { // 삭제 버튼 클릭시 showDeleteDialog(context, memoService); }, icon: Icon(Icons.delete), ) ], ), body: Padding( padding: const EdgeInsets.all(16), child: TextField( controller: contentController, decoration: InputDecoration( hintText: "메모를 입력하세요", border: InputBorder.none, ), autofocus: true, maxLines: null, expands: true, keyboardType: TextInputType.multiline, onChanged: (value) { // 텍스트필드 안의 값이 변할 때 memoService.updateMemo(index: index, content: value); }, ), ), ); } void showDeleteDialog(BuildContext context, MemoService memoService) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text("정말로 삭제하시겠습니까?"), actions: [ // 취소 버튼 TextButton( onPressed: () { Navigator.pop(context); }, child: Text("취소"), ), // 확인 버튼 TextButton( onPressed: () { memoService.deleteMemo(index: index); Navigator.pop(context); // 팝업 닫기 Navigator.pop(context); // HomePage 로 가기 }, child: Text( "확인", style: TextStyle(color: Colors.pink), ), ), ], ); }, ); } }
memo_service.dart
import 'dart:convert'; import 'package:flutter/material.dart'; import 'main.dart'; // Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다. class Memo { Memo({ required this.content, this.isPinned = false, this.updatedAt, }); String content; bool isPinned; DateTime? updatedAt; Map toJson() { return { 'content': content, 'isPinned': isPinned, 'updatedAt': updatedAt?.toIso8601String(), }; } factory Memo.fromJson(json) { return Memo( content: json['content'], isPinned: json['isPinned'] ?? false, updatedAt: json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt']), ); } } // Memo 데이터는 모두 여기서 관리 class MemoService extends ChangeNotifier { MemoService() { loadMemoList(); } List<Memo> memoList = [ Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터 Memo(content: '새 메모'), // 더미(dummy) 데이터 ]; createMemo({required String content}) { Memo memo = Memo(content: content, updatedAt: DateTime.now()); memoList.add(memo); notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침 saveMemoList(); } updateMemo({required int index, required String content}) { Memo memo = memoList[index]; memo.content = content; memo.updatedAt = DateTime.now(); notifyListeners(); saveMemoList(); } updatePinMemo({required int index}) { Memo memo = memoList[index]; memo.isPinned = !memo.isPinned; memoList = [ ...memoList.where((element) => element.isPinned), ...memoList.where((element) => !element.isPinned) ]; notifyListeners(); saveMemoList(); } deleteMemo({required int index}) { memoList.removeAt(index); notifyListeners(); saveMemoList(); } saveMemoList() { List memoJsonList = memoList.map((memo) => memo.toJson()).toList(); // [{"content": "1"}, {"content": "2"}] String jsonString = jsonEncode(memoJsonList); // '[{"content": "1"}, {"content": "2"}]' prefs.setString('memoList', jsonString); } loadMemoList() { String? jsonString = prefs.getString('memoList'); // '[{"content": "1"}, {"content": "2"}]' if (jsonString == null) return; // null 이면 로드하지 않음 List memoJsonList = jsonDecode(jsonString); // [{"content": "1"}, {"content": "2"}] memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList(); } }