3주차 - 패키지 사용법 익히기 & 앱의 기능 만들기 (마이메모)

  • PDF 파일

    3주차-패키지사용법_익히기__앱의_기능_만들기(마이메모).pdf

  • 단축키 모음

    • 새로고침 F5
    • 저장
      • Windows: Ctrl + S
      • macOS: command + S
    • 전체선택
      • Windows: Ctrl + A
      • macOS: command + A
    • 잘라내기
      • Windows: Ctrl + X
      • macOS: command + X
    • 콘솔창 줄바꿈
      • shift + enter
    • 코드정렬
      • Windows: Ctrl + Alt + L
      • macOS: option + command + L
    • 들여쓰기
      • Tab
      • 들여쓰기 취소 : Shift + Tab
    • 주석
      • Windows: Ctrl + /
      • macOS: command + /

[수업 목표]

  • 패키지 사용법 익히기
  • Create / Read / Update / Delete 기능 구현하기
  • 상태 관리의 필요성 이해하기
  • Provider 사용법 익히기
  • shared_preferences 사용법 익히기

[목차]


01. 패키지(Package)

  • 1) 패키지란?

    • Flutter는 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)
<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 문서입니다.

      Untitled

    • Flutter Gems : 카테고리별 Flutter 패키지 모음

      Untitled

<aside>
💡 직접 프로젝트를 진행하면서 패키지 사용법을 배워보도록 하겠습니다.

</aside>

02. 마이메모 앱 만들기

  • 완성본

    Untitled

  • CRUD는 가장 기본이 되는 데이터 처리 기능입니다.

    게시판 기능을 만든다면 아래 CRUD 기능이 필수적으로 제공되어야 합니다.

    1. 글 쓰기(Create)
    2. 글 읽기(Read)
    3. 글 수정(Update)
    4. 글 삭제(Delete)

    유저 정보를 다루는 과정도 CRUD로 표현하면 다음과 같습니다.

    1. 회원 가입(Create)

    2. 프로필 보여주기(Read)

    3. 회원 정보 수정(Update)

    4. 회원 탈퇴(Delete)

      이와 같이 CRUD는 다루는 데이터의 종류만 바뀌고 항상 기본적으로 구현하는 데이터 처리 기능입니다.

  • 1) 프로젝트 준비

    • Flutter 프로젝트 생성

      1. VSCode에서 아래와 같이 네모 모양의 Stop 버튼을 눌러 기존에 실행한 앱을 종료해주세요.

        Untitled

      2. ViewCommand Palette를 선택해주세요.

        Untitled

      3. 명령어를 검색하는 팝업창이 뜨면, flutter라고 입력한 뒤 Flutter: New Project를 선택해주세요.

        Untitled

      4. Application을 선택해주세요.

        Untitled

      5. 프로젝트를 저장할 폴더를 선택하는 화면이 나오면 flutter 폴더를 선택한 뒤 Select a folder to create the project in 버튼을 눌러 주세요.

      6. 프로젝트 이름을 mymemo 로 입력해주세요.

        Screen Shot 2022-09-07 at 8.04.46 PM.png

        만약 중간에 아래와 같은 팝업이 뜬다면, 체크박스를 선택한 뒤 파란 버튼을 클릭해주세요. (팝업이 안보이시면 넘어가주세요!)

        Untitled

      7. 다음과 같이 프로젝트가 생성됩니다.

        Screen Shot 2022-09-07 at 8.06.51 PM.png

      8. 불필요한 힌트 숨기기

        코드스니펫을 복사해서 analysis_options.yaml 파일의 24번째 라인 뒤에 붙여 넣고 저장해주세요.

        • [코드스니펫] analysis_options.yaml

          
                prefer_const_constructors: false
                prefer_const_literals_to_create_immutables: false
        ![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)

  1. HomePage메모의 유무, 메모의 개수라는 상태에 따라 다른 화면을 갱신해야 하므로 StatefulWidget으로 변경해줍니다.

    21번째 라인에 StatelessWidget을 클릭한 뒤 Quick Fix(Ctrl/Cmd + .)를 누른 뒤 Convert to StatefulWidget을 선택해주세요.

    Untitled

    아래와 같이 _HomePageState라는 상태 클래스가 추가되었습니다.

    Untitled

  2. 메모 목록을 가지고 있을 상태 변수 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)

  1. 메모 목록에서 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)

  1. 우측 하단의 FloatingActionButton 을 클릭할 때마다 앞에서 만들어준 memoList 변수에 빈 메모를 추가 해주겠습니다.

    지금은 해당 버튼을 누를 때 아래와 같은 에러가 발생할텐데요.

    Screen Shot 2022-09-12 at 4.34.50 PM.png

    이는 우리가 새로운 메모를 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)

  1. DetailPage 에 있는 TextFieldonChange 에 들어가는 함수를 수정하면, 텍스트가 변할 때마다 특정한 동작을 할 수 있습니다.

    먼저, 우측 하단의 FloatingActionButton을 클릭하여 DetailPage로 이동해 주세요.

    Untitled

    • iOS 에뮬레이터에서 키보드를 보이게 하는 방법

      Untitled

      simulator_screenshot_7A27686D-DC81-4E6A-A974-50A4A89576A0.png

    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)

  1. 삭제 버튼 클릭 이벤트는 112번째 줄, DetailPage 의 우상단에 있는 IconButton 위젯의 onPressed의 함수로 받을 수 있습니다.

    Untitled

    먼저 삭제 버튼을 누르는 경우 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)의 필요성

    Untitled

  • 1) Provider 패키지 추가

    1. 코드스니펫을 복사해서 새 탭에서 열어주세요.

    2. Provider 패키지 Install 탭이 아래와 같이 나오면, flutter pub add provider 명령어 옆에 아이콘을 눌러 복사해 주세요.

      Untitled

    3. ViewTerminal을 열어 주세요.

      Untitled

      아래와 같이 복사한 코드를 붙여넣고 엔터를 눌러주세요.

      Untitled

      그러면 아래와 같이 실행이 됩니다.

      Untitled

    4. pubspec.yaml 파일에 39번째 라인에 아래와 같이 Provider가 있으면 정상 설치 완료입니다.

      Untitled

  • 2) MemoService 만들기

    1. lib 폴더에서 우클릭을 해주신 뒤 New File을 선택해주세요.

      Untitled

    2. 파일 이름은 memo_service.dart로 짓도록 하겠습니다.

      Untitled

    3. 코드스니펫을 복사해 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 패키지 설정

    1. 코드스니펫을 복사해 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)

  1. HomePage 전체 위젯을 Consumer로 감싸주겠습니다. main.dart 파일에 43번째 라인에 Scaffold를 우클릭하여 Refactor(Ctrl + Shift + R)을 선택하고, Wrap with Builder를 선택해주세요.

    Untitled

    아래와 같이 Scaffold 위젯을 Builder 위젯이 감싸집니다.

    Consumer 위젯과 형식이 비슷한 Builder 를 사용해 형식을 잡아줬습니다.

    Untitled

  2. 아래 이미지와 같이 43번째 라인에 BuilderConsumer<MemoService>로 변경해주세요.

    Untitled

  3. 에러를 해결하겠습니다.

    아래 이미지와 같이 ,memoService, child 를 43번째 라인의 context 뒤에 추가해주세요.

    Untitled

    그리고 줄 정렬을 예쁘게 해주기 위해 103번째 라인의 중괄호와 소괄호 사이에 콤마(,)를 찍은 뒤 저장해주세요.

    Untitled

    저장하면 코드가 정렬됩니다.

  4. 코드스니펫을 복사해 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)

    1. 이제 메모 데이터를 MemoService 에서 관리하니, DetailPagememoList 를 따로 가지고 있을 필요가 없겠죠.

      아래 코드스니펫을 복사해 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)

    1. 먼저 코드스니펫을 복사해 MemoServiceMemo 를 추가하는 함수를 구현해봅시다. 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)

    1. 먼저 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)

    1. 먼저 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 패키지를 이용하여 최상단 위젯 서비스를 등록해 줍니다.

      Untitled

    • 위젯트리 꼭대기에 있는 Provider로 등록한 클래스에 접근 방법

    • Consumer<클래스명> : 클래스 정보 갱신시 함께 새로고침 할 때 사용

      Untitled

    • context.read<클래스명>() : 화면을 새로고침할 필요 없이, 일회성으로 서비스의 변수나 함수에 접근하고 싶은 경우에 사용

      Untitled

  • 7) 리팩토링

    1. showDeleteDialog 메소드(함수)로 분리하기

      126번째 줄에 showDialog를 클릭한 뒤 왼쪽에 전구(💡) 아이콘을 선택하고, Extract Method를 선택해 주세요.

      Untitled

      메소드(함수)의 이름을 입력하라고 뜨면 showDeleteDialog라고 입력한 뒤 엔터를 누르고 저장(Ctrl/Cmd + S)를 눌러주세요.

      Untitled

      그러면 아래와 같이 126번째 줄이 생성된 함수를 호출하도록 바뀝니다.

      Untitled

      그리고 153번째 라인으로 내려가보면 showDeleteDialog라는 함수가 생성되어 있는 것을 볼 수 있습니다.

      Untitled

    2. 153번째 줄을 보면 함수의 반환 타입이 Future<dynamic> 이라고 되어있는데, 이 부분을 void로 변경하고, 155번째 줄에 있는 return을 삭제해주세요.

      Untitled

    3. 저장한 뒤, 메모를 하나 삭제해보면 정상적으로 작동하는 것을 확인할 수 있습니다.

  • 최종 코드

    • 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 패키지 설치

    1. 패키지를 설치해 보도록 하겠습니다. 코드스니펫을 복사해 새 탭에서 열어주세요.

    `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 사용법

    1. 사용 준비
      SharedPreferences를 이용하려면 먼저 SharedPreferences 인스턴스를 가져와야합니다.

       // 인스턴스 생성
       SharedPreferences prefs = await SharedPreferences.getInstance();
    2. 값 저장하기
      SharedPreferences는 데이터를 Key와 Value로 구성된 Map 형태로 데이터를 저장합니다. 저장시 사용되는 Key원하는 이름으로 정하시면 됩니다.

       prefs.setString("username", "John Doe");
    3. 값 불러오기
      저장할 때 사용한 username 이라는 Key를 이용해 다시 값을 가져올 수 있습니다. 저장된 값이 없는 경우 null을 반환하는 점 유의해 주세요.

       String? value = prefs.getString("username"); 
  • 3) 기기에 memoList 데이터 저장하기

    1. 먼저 전역으로 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) 메모 핀 기능 구현

    • 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();
          }
        }
  • + Recent posts