스파르타/Flutter

[Flutter] 마이메모 #1 CRUD - ( week 3 )

bakcoding_sparta 2023. 4. 22. 01:39

메모장 앱을 만든다.

주요 기능은 다음과 같다.

1. 메모 작성(Create)

2. 메모 조회(Read)

3. 메모 수정(Update)

4. 메모 삭제(Delete)

 

이렇게 가장 기본이 되는 데이터 처리 기능을 CRUD라고 부른다.

CRUD는 데이터의 종류만 바뀌면서 항상 기본적으로 구현하는 데이터 처리로 다른 것을 예로 들면 유저 정보의 경우 회원 가입(Create), 프로필 보여주기(Read), 회원 정보 수정(Update), 회원 탈퇴(Delete)를 기본 기능이라 볼 수 있다.

 

베이스 코드를 바탕으로 앱을 만들어 가본다.

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) {
            // 텍스트필드 안의 값이 변할 때
          },
        ),
      ),
    );
  }
}

하단의 플로팅 버튼을 클릭하면 메모를 입력하는 페이지가 나타나고 메모 페이지에서 뒤로 가기를 누르면 다시 첫 페이지로 돌아가는 기능만 구현되어 있다.

 

메모 목록 생성/조회(Create/Read)

메모가 없는 경우 '메모를 작성해 주세요' 문구를 보여주고 메모가 있는 경우 메모 목록을 보여주도록 한다.

따라서 첫 페이지는 메모의 유무 즉, 메모의 개수에 따라 상태를 갱신해야 하는 StatefulWidget이다.

 

HomePage를 StatefulWidget으로 변경한다.

// 홈 페이지
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

StatefulWidget으로 변경하게 되면 HomePage는 _HomePageState로 나누어지게 되고 실제 반영되는 부분인 _HomePageState 위젯에 기능을 구현한다.

 

우선 리스트를 보여주기 위해 List 변수를 추가해 주고 body에 리스트 유무를 조건문으로 분기한다.

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("메모를 작성해 주세요"))
          : Center(child: Text('메모가 존재합니다!')),
          ~~ 
          );
        },
      ),
    );
  }

List 변수를 선언했기 때문에 이제 '메모가 존재합니다'라는 문구로 변경된다.

 

이제 리스트를 ListView.builder로 만들어서 메모가 생성될 때 리스트로 보이도록 한다. 해당 리스트는 클릭되었을 때 메모가 보여야 하기 때문에 onPressed 이벤트가 필요하다.

: 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 : 클릭 됨');
                  },
                );
              },
            ),

리스트 앞에는 해당 아이템을 고정시키는 역할을 하게 될 pin 아이콘을 추가하고 아이콘은 클릭 시 '{메모 내용} : pin 클릭됨 '이라는 메시지를 출력하고 텍스트를 클릭 시 '{메모 내용} : 클릭됨' 메시지가 콘솔창에 출력되도록 한다.

 

 

ListTile 위젯은 박스 안에 영역마다 서로 다른 위젯을 배치할 수 있는 편리한 기능을 가지고 있다.

ListTile detail

 

 

목록에서 메모 텍스트를 클릭했을 때 메모 작성 페이지로 이동시켜 준다.

onTap: () {
                    // 아이템 클릭시
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (_) => DetailPage()),
                    );
                  },

메모 작성 페이지에서 메모를 볼 수 있어야 하기 때문에 정보를 전달하도록 한다.

memoList와 해당 메모의 index를 DetailPage로 전달한다.

 

DetailPage에서는 정보를 받을 수 있도록 변수를 선언해 준다.

class DetailPage extends StatelessWidget {
  DetailPage({super.key, required this.memoList, required this.index});

  final List<String> memoList;
  final int index;

선언한 변수는 final로 선언되었기 때문에 반드시 한 번의 초기화가 요구되는데 이를 생성자에서 초기화되도록 해준다.

 

생성자에 매개변수가 추가되었기 때문에 DetailPage가 인스턴스화되는 부분에 인자를 추가해주어야한다.

 

builder: (_) => DetailPage(
                                index: index,
                                memoList: memoList,
                              )),

플로팅 버튼을 클릭할 때도 DetailPage로 이동하도록 사용 중이기 때문에 여기도 인자를 추가해 준다.

                    String memo = ''; // 빈 메모 내용 추가
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (_) => DetailPage(
                          index: memoList.indexOf(memo),
                          memoList: memoList,
                        ),
                      ),
                    );

플로팅 버튼은 새로운 메모를 작성하는 기능이기 때문에 처음 메모의 내용은 비어있도록 한다.

 

현재는 목록에 있는 메모 텍스트를 클릭해도 전달해 주는 값이 없기 때문에 보이지 않는데 리스트의 선택한 인덱스 값으로 요소를 전달해 주도록 한다.

  @override
  Widget build(BuildContext context) {
    contentController.text = memoList[index];

 

이제 목록에서 텍스트를 클릭하면 해당 리스트의 텍스트를 DetailPage로 전달해서 메모 내용이 보이도록 한다.

 

빈메모를 추가하는 기능의 경우 새로 만든 빈 메모를 리스트에 넣어주는 동작이 필요하다.

        onPressed: () {
          // + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
          String memo = ''; // 빈 메모 내용 추가
          memoList.add(memo);
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (_) => DetailPage(
                index: memoList.indexOf(memo),
                memoList: memoList,
              ),
            ),
          );
        },

이제 플로팅 버튼을 클릭하면 빈 메모가 메모 목록에 추가되어야 하는데 뒤로 가기 시 화면을 갱신시켜 주어야 리스트에 반영이 된다. 즉 메모리스트에 추가될 때 setState로 화면이 다시그려지도록해야한다.

          String memo = ''; // 빈 메모 내용 추가
          setState(() {
            memoList.add(memo);
          });

 

메모 수정(Update)

메모가 작성될 때 memoList에 있는 값이 경신되도록 한다.

DetailPage의 onChanged가 값이 변경될 때마다 호출되는 곳임을 확인할 수 있다.

onChanged: (value) {
            // 텍스트필드 안의 값이 변할 때
            print(value);
          },

이제 입력된 텍스트가 리스트에 반영되도록 한다.

          onChanged: (value) {
            // 텍스트필드 안의 값이 변할 때
            memoList[index] = value;
          },

이 상태로는 변수의 값은 바뀌었지만 화면이 갱신되지 않아서 보이지 않는다. (새로고침시 보임)

하지만 이 부분은 다른 페이지의 변화를 또 다른 페이지에 반영시키는 것이기 때문에 기존에 했던 방식인 setState로는 불가능하다. 우선 여기까지 작업을 진행하고 메모 삭제 기능을 구현하면서 같이 해결하도록 한다.

 

메모 삭제(Delete)

DetailPage의 휴지통 아이콘을 클릭 시 해당 메모가 삭제되도록 한다.

우선 삭제 버튼 클릭 시 확인을 받는 다이어로그를 띄어본다.

IconButton(
            onPressed: () {
              // 삭제 버튼 클릭시
              showDialog(
                context: context,
                builder: (context) {
                  return AlertDialog(
                    title: Text("정말로 삭제하시겠습니까?"),
                  );
                },
              );
            },
            icon: Icon(Icons.delete),
          )

다이어로그 내부에는 선택할 수 있는 버튼을 추가해주어야 한다.

 

title: Text("정말로 삭제하시겠습니까?"),
                    actions: [
                      // 취소 버튼
                      TextButton(
                        onPressed: () {
                          Navigator.pop(context);
                        },
                        child: Text("취소"),
                      ),
                      // 확인 버튼
                      TextButton(
                        onPressed: () {
                          Navigator.pop(context);
                        },
                        child: Text(
                          "확인",
                          style: TextStyle(color: Colors.pink),
                        ),
                      ),
                    ],

다이어로그에 버튼을 그리는 것까지는 완료되었으니 이제 버튼에 기능을 추가해 주도록 한다. 

삭제 기능은 확인 버튼을 눌렀을 때 실행되어야 한다.

                       TextButton(
                        onPressed: () {
                          memoList.removeAt(index); // index에 해당하는 항목 삭제
                          Navigator.pop(context); // 팝업 닫기
                          Navigator.pop(context); // HomePage 로 가기
                        },
                        child: Text(
                          "확인",
                          style: TextStyle(color: Colors.pink),
                        ),

 

메모 삭제 역시 변수상태에서는 반영이 되었지만 화면에서 다시 그려주지 않았기 때문에 그대로이다.

 

이제 메모 작성과 삭제를 반영하기 위해서 상태 관리를 적용시켜야 한다.

 

다음장에 계속