Flutter - 메모 추가/수정/삭제하기 (2/2) - 메모 앱 만들기 기능 구현

반응형

 

포스팅 참고 사항

포스팅에서 진행한 환경

  • OS : Windows 10
  • IDE : Visual Studio Code
  • Emulator : Pixel 5 API 27 (Android-x86 emulator), Windows (windows-x64)

 

포스팅에서의 예시 프로젝트 다운로드

  • 포스팅에서 다루는 예시 프로젝트는 아래의 깃허브 링크에서 다운로드 받을 수 있습니다.
  • '7_memo_CRUD_example' 폴더를 확인해주세요.
  • https://github.com/luvris2/flutter_memo_app

 

할 것

  • 이번 포스팅에서는 DB가 연동되었다는 가정하에 데이터 조회, 추가, 수정, 삭제 기능을 구현합니다.

 

생략한 것

  • MySQL 연동 및 쿼리 실행 부분은 이전 포스팅에서 진행하였으므로 생략합니다.
  • DB에서 데이터를 가져오는 함수를 사용할 경우 포스팅에서 DB 함수라고 기재해두었습니다.

 

확인할 것

  • 포스팅에서의 코드 설계는 이해를 돕기 위해 부분부분을 나눠 보여드린 것이므로 실질적인 코드 실행은 마지막 전체 소스 코드를 확인해주세요.
  • 이번 포스팅 (챕터7)부터는 AWS RDS가 아닌 MySQL Locahost를 기준으로 프로젝트를 진행하였습니다. 때문에 따라하다가 난해할 것 같은 DB 설정 부분의 파일을 배제하지 않고 그대로 깃허브에 포함하였습니다. 로컬DB로 해당 프로젝트를 따라하실 경우에는 사용자명과 비밀번호, DB이름 정도만 바꾸셔도 됩니다.

 

다른 설명이 필요하면 아래의 링크를 참고해주세요.

 

부가 설명 (이번 포스팅은 참고할 것이 꽤 많습니다.)


메모 추가

MySQL

  • 추가한 메모 확인 쿼리
select m.id, userIndex, u.userName, memoTitle, memoContent, createDate, updateDate  from memo as m
left join users as u on m.userIndex = u.id
where userIndex = 1;


요구사항

  • 메모를 추가할 소스 코드는 아래의 디렉토리의 파일에서 코드 작성
  • 파일명 : memoMainPage.dart

  • 메모 추가 절차
    1. 플로팅 액션 버튼 클릭
    2. 메모를 작성할 수 있는 작은 다이얼로그 출력
    3. 다이얼로그에서 제목과 내용을 입력하고 '추가' 버튼 클릭 시 메모 등록
    4. '취소' 버튼 클릭 시 아무 이벤트 발생하지 않음
  • 메모 저장 사항
    • 작성한 메모는 MySQL DB에 저장되어야 함
    • 테이블은 'memo'와 'users' 테이블을 사용함
    • memo 테이블 : 메모 정보를 저장하기 위한 테이블
    • users 테이블 : 메모가 누구것인지 식별하기 위한 users의 id를 식별하기 위함
  • 리스트뷰 새로고침
    • 추가한 메모를 실시간으로 확인할 수 있어야 함
    • 새로고침은 provider 패키지를 사용하여 상태 변경을 감지하고 업데이트 함

코드 설계

memoMainPage.dart

  • 메모 추가 버튼 (플로팅 액션 버튼)
    • 버튼을 누르면 추가할 메모의 제목과 내용 작성하기 위함
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
      	// 본문 코드 생략
      ),
	  // 플로팅 액션 버튼
      floatingActionButton: FloatingActionButton(
        onPressed: () => addItemEvent(context), // 버튼을 누를 경우 메모 추가 UI 표시
        tooltip: 'Add Item', // 플로팅 액션 버튼 설명
        child: Icon(Icons.add), // + 모양 아이콘
      ),
    );
  }
}

 

  • 메모 추가 다이얼로그 (플로팅 액션 버튼 클릭 시 수행 코드)
  • 특이사항
    • 메모 추가는 DB함수(addMemo)를 통해 DB에 메모 내용 등록
    • 메모 추가 클릭 이벤트는 비동기 프로그래밍으로 메모 리스트 출력
    • 이유 : DB의 데이터를 기반으로 메모 목록을 조회하기 때문
  // 플로팅 액션 버튼 클릭 이벤트
  Future<void> addItemEvent(BuildContext context) {
    // 다이얼로그 폼 열기
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('메모 추가'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                  labelText: '제목',
                ),
              ),
              TextField(
                controller: contentController,
                maxLines: null, // 다중 라인 허용
                decoration: InputDecoration(
                  labelText: '내용',
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text('취소'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('추가'),
              onPressed: () async {
                String title = titleController.text;
                String content = contentController.text;
                // 메모 추가
                await addMemo(title, content);

                setState(() {
                  // 메모 리스트 새로고침
                  print("MemoMainPage - addMemo/setState");
                  getMemoList();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

 

  • 모든 메모 조회 (메모 추가 다이얼로그에서 호출하는 함수)
  • 특이사항
    • 모든 메모를 조회하는 DB함수(selecetMemoAll) 호출
    •  Provider을 활용하여 상태 변경 값 저장(context.read<>~)
  // 메모 리스트 출력
  Future<void> getMemoList() async {
    List memoList = [];
    // DB에서 메모 정보 호출
    var result = await selectMemoALL();

    print(result?.numOfRows);

    // 메모 리스트 저장
    for (final row in result!.rows) {
      var memoInfo = {
        'id': row.colByName('id'),
        'userIndex': row.colByName('userIndex'),
        'userName': row.colByName('userName'),
        'memoTitle': row.colByName('memoTitle'),
        'memoContent': row.colByName('memoContent'),
        'createDate': row.colByName('createDate'),
        'updateDate': row.colByName('updateDate')
      };
      memoList.add(memoInfo);
    }

    print('MemoMainPage - getMemoList : $memoList');
    context.read<MemoUpdator>().updateList(memoList);
  }

메모 수정

MySQL

  • 수정한 메모 확인 쿼리
select m.id, userIndex, u.userName, memoTitle, memoContent, createDate, updateDate  from memo as m
left join users as u on m.userIndex = u.id
where m.id = 14;


요구사항

  • 메모를 추가할 소스 코드는 아래의 디렉토리의 파일에서 코드 작성
  • 파일명 : memoDetailPage.dart

  • 메모 수정 절차
    1. memoMainPage에서 리스트뷰의 목록에서 특정 인덱스의 메모(카드) 클릭
    2. 특정 메모를 보여주는 페이지로 이동
    3. 이동된 memoDetailPage에서 앱 바의 우측 상단에 글쓰기 버튼 클릭
    4. 다이얼로그에서 제목과 내용을 입력하고 '수정' 버튼 클릭 시 메모 수정
    5. '취소' 버튼 클릭 시 아무 이벤트 발생하지 않음
  • 메모 수정 사항
    • 작성한 메모는 MySQL DB에 저장되어야 함
    • 테이블은 'memo'와 테이블을 사용함
    • memo 테이블 : 메모 정보를 수정하기 위한 테이블
    • 식별은 글 번호(memo 테이블의 id)로 함
  • 리스트뷰 새로고침
    • 수정한 메모를 실시간으로 확인할 수 있어야 함
    • 뒤로 가기를 눌러 메모 목록을 확인 시 수정된 메모를 실시간으로 확인할 수 있어야 함
    • 새로고침은 provider 패키지를 사용하여 상태 변경을 감지하고 업데이트 함

코드 설계

memoMainPage.dart

  • 메모 목록에서의 특정 메모 선택 (memoMainPage의 ListView)
    • memoMainPage의 ListView의 Card의 인덱스 값과 함께 메모 상세 보기 페이지로 이동
    • 메모 상세 보기 페이지 이동을 위한 함수 : cardClickEvent 호출
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: Builder(
              builder: (context) {
                  return ListView.builder(
                    itemCount: items.length,
                    itemBuilder: (BuildContext context, int index) {
                      // 메모 정보 저장
                      dynamic memoInfo = items[index];
                      String userName = memoInfo['userName'];
                      String memoTitle = memoInfo['memoTitle'];
                      String memoContent = memoInfo['memoContent'];
                      String createDate = memoInfo['createDate'];
                      String updateDate = memoInfo['updateDate'];

                        return Card(
						  elevation: 3,
                          shape: RoundedRectangleBorder(
                              borderRadius:
                                  BorderRadius.all(Radius.elliptical(20, 20))),
                          child: ListTile(
                            leading: Text(userName),
                            title: Text(memoTitle),
                            subtitle: Text(memoContent),
                            trailing: Text(updateDate),
                            onTap: () => cardClickEvent(context, index),
                          ),
                        );
                      }
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

 

  • 메모 상세 보기 페이지 이동을 위한 함수(카드뷰 클릭 이벤트)
  • 특이사항
    • 비동기 프로그래밍으로 작성
    • 이유 : 메모 수정 페이지에서 메모 수정이 이뤄지고 memoMainPage로 돌아올 경우, 수정된 메모 리스트뷰에 업데이트되기 위함
    • 페이지 이동 시 메모 정보(content) 전달
  // 리스트뷰 카드 클릭 이벤트
  void cardClickEvent(BuildContext context, int index) async {
    dynamic content = items[index];
    print('content : $content');
    // 메모 리스트 업데이트 확인 변수 (false : 업데이트 되지 않음, true : 업데이트 됨)
    var isMemoUpdate = await Navigator.push(
      context,
      MaterialPageRoute(
        // 정의한 ContentPage의 폼 호출
        builder: (context) => ContentPage(content: content),
      ),
    );

	// 메모 수정이 일어날 경우, 메모 메인 페이지의 리스트 새로고침
    if (isMemoUpdate != null) {
      setState(() {
        getMemoList();
        items = Provider.of<MemoUpdator>(context, listen: false).memoList;
      });
    }
  }

 

memoDetailPage.dart

  • 앱 바 상단에 보여질 수정 버튼 아이콘
  • 특이사항
    • 수정 버튼을 누르면 updateItemEvent 함수가 실행되어 해당 함수에서 수정 진행
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
      // 좌측 상단의 뒤로 가기 버튼
        leading: IconButton(
          icon: Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.pop(context, 1);
          },
        ),
        title: Text('메모 상세 보기'),
        actions: [
          IconButton(
            onPressed: () => updateItemEvent(context),
            icon: Icon(Icons.edit),
            tooltip: "메모 수정",
          ),
        ],
      ),
      // 본문 코드 생략

 

  • 생성자 초기화
    • 전달받은 특정 메모 정보(content) 저장
class ContentPage extends StatefulWidget {
  // 생성자 초기화
  final dynamic content;
  const ContentPage({Key? key, required this.content}) : super(key: key);

  @override
  State<ContentPage> createState() => _ContentState(content: content);
}

class _ContentState extends State<ContentPage> {
  // 부모에게 받은 생성자 값 초기화
  final dynamic content;
  _ContentState({required this.content});
  
  // 코드 생략

 

  • 상태 초기화
    • 선택된 메모의 정보를 화면에 출력하기 위함
  • 특이사항
    • 처음 넘겨받은 생성자의 값을 이용하여 Provider에 값 저장
    • 저장된 값은 빌드가 끝나면 Provider에서 데이터 읽어 화면에 출력
    • 초기화에서 프로바이더를 사용하는 이유 : 수정 후 수정된 메모를 실시간 반영하여야 하기 때문
      • 쉽게 말하면 하나의 프로바이더 값으로 초기 값과 수정 후의 값을 모두 포괄하기 위함
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    var memo = {
      'id': content['id'],
      'userIndex': content['userIndex'],
      'userName': content['userName'],
      'memoTitle': content['memoTitle'],
      'memoContent': content['memoContent'],
      'createDate': content['createDate'],
      'updateDate': content['updateDate']
    };
    List memoList = [];
    memoList.add(memo);

    // 빌드가 완료된 후 Provider의 데이터 읽기
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<MemoUpdator>().updateList(memoList);
    });
  }

 

  • 선택한 메모 자세히 보기
    • 이전 페이지에서 넘겨받은 정보를 토대로 페이지에 출력
      • 이 부분이 프로바이더 상태 초기화의 값을 이용
    • 메모 수정 시, 수정된 정보를 토대로 페이지에 출력
      • 이 부분에서 프로바이더 업데이트 값을 이용
    • 위의 두 상황을 포괄하기 위하여 context.watch<>() 메서드를 memoInfo 변수에 저장하여 출력
  // 메모의 정보를 저장할 변수
  List memoInfo = [];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Builder(builder: (context) {
            // 특정 메모 정보 출력
            memoInfo = context.watch<MemoUpdator>().memoList;

            return Stack(
              children: <Widget>[
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    SizedBox(),
                    Text(
                      memoInfo[0]['memoTitle'],
                      style:
                          TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
                    ),
                  ],
                ),
                Column(
                  children: [
                    SizedBox(height: 35),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('작성자 : ${memoInfo[0]['userName']}')],
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('작성일 : ${memoInfo[0]['createDate']}')],
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('수정일 : ${memoInfo[0]['updateDate']}')],
                    ),
                    Expanded(
                      child: Padding(
                        padding: const EdgeInsets.all(15.0),
                        child: SizedBox(
                          height: double.infinity,
                          width: double.infinity,
                          child: Text(
                            memoInfo[0]['memoContent'],
                          ),
                        ),
                      ),
                    )
                  ],
                ),
              ],
            );
          }),
        ),
      ),
    );
  }
}

 

  • 메모 수정 버튼 클릭시 이벤트(updateItemEvent)
  • 특이사항
    • 앱 바의 수정 버튼을 클릭하면 메모를 수정하는 다이얼로그 폼 출력
    • 수정 내용은 텍스트 컨트롤러를 이용하여 값을 받아옴
    • 메모 수정 완료 시 DB함수(updateMemo)를 통해 데이터베이스의 메모를 수정
    • 업데이트 된 메모의 정보의 값을 다시 읽어 드리기 위한 updateRefresh함수 호출
    • 업데이트 된 내용을 상태 변경을 통해 위젯 업데이트 setState+Provider
  // 앱 바 메모 수정 버튼을 이용하여 메모를 수정할 제목과 내용
  final TextEditingController titleController = TextEditingController();
  final TextEditingController contentController = TextEditingController();

  // 앱 바 메모 수정 클릭 이벤트
  Future<void> updateItemEvent(BuildContext context) {
    // 앱 바 메모 수정 버튼을 이용하여 메모를 수정할 제목과 내용
    TextEditingController titleController =
        TextEditingController(text: memoInfo[0]['memoTitle']);
    TextEditingController contentController =
        TextEditingController(text: memoInfo[0]['memoContent']);

    // 다이얼로그 폼 열기
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('메모 수정'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                  labelText: '제목',
                ),
              ),
              TextField(
                controller: contentController,
                maxLines: null, // 다중 라인 허용
                decoration: InputDecoration(
                  labelText: '내용',
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text('취소'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('수정'),
              onPressed: () async {
                String memoTitle = titleController.text;
                String memoContent = contentController.text;

                Navigator.of(context).pop();

                print('memoTitle : $memoTitle');
                // 메모 수정
                await updateMemo(content['id'], memoTitle, memoContent);

                // 업데이트 된 메모 정보 호출
                updateRefresh();

                // 메모 내용 업데이트
                setState(() {
                  memoInfo = context.watch<MemoUpdator>().memoList;
                });
              },
            ),
          ],
        );
      },
    );
  }

 

  • 메모 수정시 새로 고침 (updateRefresh)
    • DB함수(selectMemo)를 통해 특정 메모의 정보 조회
    • 조회된 정보(수정된 메모) 반영을 위해 Provider의 값에 저장
  // 메모 수정시 화면 새로고침
  Future<void> updateRefresh() async {
    List memoList = [];
    // DB에서 메모 정보 호출
    var result = await selectMemo(content['id']);
    
    // 특정 메모 정보 저장
    for (final row in result!.rows) {
      var memo = {
        'id': row.colByName('id'),
        'userIndex': row.colByName('userIndex'),
        'userName': row.colByName('userName'),
        'memoTitle': row.colByName('memoTitle'),
        'memoContent': row.colByName('memoContent'),
        'createDate': row.colByName('createDate'),
        'updateDate': row.colByName('updateDate')
      };
      memoList.add(memo);
    }
    print("memo update : $memoList");
    context.read<MemoUpdator>().updateList(memoList);
  }

메모 삭제

MySQL

  • 메모 삭제 확인 쿼리
select m.id, userIndex, u.userName, memoTitle, memoContent, createDate, updateDate  from memo as m
left join users as u on m.userIndex = u.id
where m.id = 14;


요구사항

  • memoDetailPage에서 앱 바 우측 상단의 삭제 버튼이 있어야 함
  • 삭제 버튼을 누르면 메모가 DB에서 삭제 되어야 함
  • 삭제된 메모는 앱의 memoMainPage에서 리스트뷰에 실시간으로 반영이 되어야 함

코드 설계

  • 앱 바의 메모 삭제 버튼
  • 특이사항
    • 삭제 아이콘을 클릭하면 deleteItemEvent 함수 호출
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          IconButton(
            onPressed: () => deleteItemEvent(context),
            icon: Icon(CupertinoIcons.delete_solid),
            tooltip: "메모 삭제",
          ),
        ],
      ),
      
      // 코드 생략

 

  • 메모 삭제 버튼 클릭시 이벤트 (deleteItemEvent)
  • 특이사항
    • DB의 메모 id를 기준으로 DB함수(deleteMemo)를 통해 DB의 메모 삭제
    • 삭제 후 memoMainPage.dart의 페이지로 이동
  // 메모 삭제
  void deleteItemEvent(BuildContext context) {
    deleteMemo(memoInfo[0]['id']);
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => MyMemoPage(),
      ),
    );
  }

전체 소스 코드

  • 이번 포스팅에서 작성한 디렉토리 폴더(memoPage)

 

memoDB.dart

더보기
// ignore_for_file: avoid_print
// ignore_for_file: file_names

import 'package:flutter_memo_app/config/mySqlConnector.dart';
import 'package:mysql_client/mysql_client.dart';
import 'package:shared_preferences/shared_preferences.dart';

// 모든 메모 보기
Future<IResultSet?> selectMemoALL() async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 유저 식별 정보 호출
  SharedPreferences prefs = await SharedPreferences.getInstance();
  final String? token = prefs.getString('token');

  // DB에 저장된 메모 리스트
  IResultSet result;

  // 유저의 모든 메모 보기
  try {
    result = await conn.execute(
        "SELECT m.id, userIndex, u.userName, memoTitle, memoContent, createDate, updateDate FROM memo AS m LEFT JOIN users AS u ON m.userIndex = u.id WHERE userIndex = :token",
        {"token": token});
    if (result.numOfRows > 0) {
      return result;
    }
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
  // 메모가 없으면 null 값 반환
  return null;
}

// 메모 작성
Future<String?> addMemo(String title, String content) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 유저 식별 정보 호출
  SharedPreferences prefs = await SharedPreferences.getInstance();
  final String? token = prefs.getString('token');

  // 쿼리 수행 결과 저장 변수
  IResultSet? result;

  // 유저의 아이디를 저장할 변수
  String? userName;

  // 메모 추가
  try {
    // 유저 이름 확인
    result = await conn.execute(
      "SELECT userName FROM users WHERE id = :token",
      {"token": token},
    );

    // 유저 이름 저장
    for (final row in result.rows) {
      userName = row.colAt(0);
    }

    // 메모 추가
    result = await conn.execute(
      "INSERT INTO memo (userIndex, memoTitle, memoContent) VALUES (:userIndex, :title, :content)",
      {"userIndex": token, "title": title, "content": content},
    );
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
  // 예외처리용 에러코드 '-1' 반환
  return '-1';
}

// 메모 수정
Future<void> updateMemo(String id, String title, String content) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 유저 식별 정보 호출
  SharedPreferences prefs = await SharedPreferences.getInstance();
  final String? token = prefs.getString('token');

  // 쿼리 수행 결과 저장 변수
  IResultSet? result;

  // 메모 수정
  try {
    await conn.execute(
        "UPDATE memo SET memoTitle = :title, memoContent = :content where id = :id and userIndex = :token",
        {"id": id, "token": token, "title": title, "content": content});
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
}

// 특정 메모 조회
Future<IResultSet?> selectMemo(String id) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 유저 식별 정보 호출
  SharedPreferences prefs = await SharedPreferences.getInstance();
  final String? token = prefs.getString('token');

  // 쿼리 수행 결과 저장 변수
  IResultSet? result;

  // 메모 수정
  try {
    result = await conn.execute(
        "SELECT m.id, userIndex, u.userName, memoTitle, memoContent, createDate, updateDate FROM memo AS m LEFT JOIN users AS u ON m.userIndex = u.id WHERE userIndex = :token and m.id = :id",
        {"token": token, "id": id});
    return result;
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }

  return null;
}

// 특정 메모 삭제
Future<void> deleteMemo(String id) async {
  // MySQL 접속 설정
  final conn = await dbConnector();

  // 메모 수정
  try {
    await conn.execute("DELETE FROM memo WHERE id = :id", {"id": id});
  } catch (e) {
    print('Error : $e');
  } finally {
    await conn.close();
  }
}

memoListProvider.dart

더보기
import 'package:flutter/material.dart';

class MemoUpdator extends ChangeNotifier {
  List _memoList = [];
  List get memoList => _memoList;

  // 리스트 업데이트
  void updateList(List newList) {
    _memoList = newList;
    notifyListeners();
  }
}

memoMainPage.dart

더보기
// 메모 페이지
// 앱의 상태를 변경해야하므로 StatefulWidget 상속
// ignore_for_file: avoid_print, use_build_context_synchronously

import 'package:flutter/material.dart';
import 'package:flutter_memo_app/memoPage/memoDB.dart';
import 'package:flutter_memo_app/memoPage/memoListProvider.dart';
import 'package:provider/provider.dart';

import 'memoDetailPage.dart';

class MyMemoPage extends StatefulWidget {
  const MyMemoPage({super.key});

  @override
  MyMemoState createState() => MyMemoState();
}

class MyMemoState extends State<MyMemoPage> {
  // 검색어
  String searchText = '';

  // 플로팅 액션 버튼을 이용하여 항목을 추가할 제목과 내용
  final TextEditingController titleController = TextEditingController();
  final TextEditingController contentController = TextEditingController();

  // 메모 리스트 저장 변수
  List items = [];

  // 메모 리스트 출력
  Future<void> getMemoList() async {
    List memoList = [];
    // DB에서 메모 정보 호출
    var result = await selectMemoALL();

    print(result?.numOfRows);

    // 메모 리스트 저장
    for (final row in result!.rows) {
      var memoInfo = {
        'id': row.colByName('id'),
        'userIndex': row.colByName('userIndex'),
        'userName': row.colByName('userName'),
        'memoTitle': row.colByName('memoTitle'),
        'memoContent': row.colByName('memoContent'),
        'createDate': row.colByName('createDate'),
        'updateDate': row.colByName('updateDate')
      };
      memoList.add(memoInfo);
    }

    print('MemoMainPage - getMemoList : $memoList');
    context.read<MemoUpdator>().updateList(memoList);
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getMemoList();
  }

  // 리스트뷰 카드 클릭 이벤트
  void cardClickEvent(BuildContext context, int index) async {
    dynamic content = items[index];
    print('content : $content');
    // 메모 리스트 업데이트 확인 변수 (false : 업데이트 되지 않음, true : 업데이트 됨)
    var isMemoUpdate = await Navigator.push(
      context,
      MaterialPageRoute(
        // 정의한 ContentPage의 폼 호출
        builder: (context) => ContentPage(content: content),
      ),
    );

    // 메모 수정이 일어날 경우, 메모 메인 페이지의 리스트 새로고침
    if (isMemoUpdate != null) {
      setState(() {
        getMemoList();
        items = Provider.of<MemoUpdator>(context, listen: false).memoList;
      });
    }
  }

  // 플로팅 액션 버튼 클릭 이벤트
  Future<void> addItemEvent(BuildContext context) {
    // 다이얼로그 폼 열기
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('메모 추가'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                  labelText: '제목',
                ),
              ),
              TextField(
                controller: contentController,
                maxLines: null, // 다중 라인 허용
                decoration: InputDecoration(
                  labelText: '내용',
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text('취소'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('추가'),
              onPressed: () async {
                String title = titleController.text;
                String content = contentController.text;
                // 메모 추가
                await addMemo(title, content);

                setState(() {
                  // 메모 리스트 새로고침
                  print("MemoMainPage - addMemo/setState");
                  getMemoList();
                });
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(20.0),
            child: TextField(
              decoration: InputDecoration(
                hintText: '검색어를 입력해주세요.',
                border: OutlineInputBorder(),
              ),
              onChanged: (value) {
                setState(() {
                  searchText = value;
                });
              },
            ),
          ),
          Expanded(
            child: Builder(
              builder: (context) {
                // 메모 수정이 일어날 경우 메모 리스트 새로고침
                items = context.watch<MemoUpdator>().memoList;

                // 메모가 없을 경우의 페이지
                if (items.isEmpty) {
                  return Center(
                    child: Text(
                      "표시할 메모가 없습니다.",
                      style: TextStyle(fontSize: 20),
                    ),
                  );
                }
                // 메모가 있을 경우의 페이지
                else {
                  // items 변수에 저장되어 있는 모든 값 출력
                  return ListView.builder(
                    itemCount: items.length,
                    itemBuilder: (BuildContext context, int index) {
                      // 메모 정보 저장
                      dynamic memoInfo = items[index];
                      String userName = memoInfo['userName'];
                      String memoTitle = memoInfo['memoTitle'];
                      String memoContent = memoInfo['memoContent'];
                      String createDate = memoInfo['createDate'];
                      String updateDate = memoInfo['updateDate'];

                      // 검색 기능, 검색어가 있을 경우, 제목으로만 검색
                      if (searchText.isNotEmpty &&
                          !items[index]['memoTitle']
                              .toLowerCase()
                              .contains(searchText.toLowerCase())) {
                        return SizedBox.shrink();
                      }
                      // 검색어가 없을 경우
                      // 혹은 모든 항목 표시
                      else {
                        return Card(
                          elevation: 3,
                          shape: RoundedRectangleBorder(
                              borderRadius:
                                  BorderRadius.all(Radius.elliptical(20, 20))),
                          child: ListTile(
                            leading: Text(userName),
                            title: Text(memoTitle),
                            subtitle: Text(memoContent),
                            trailing: Text(updateDate),
                            onTap: () => cardClickEvent(context, index),
                          ),
                        );
                      }
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
      // 플로팅 액션 버튼
      floatingActionButton: FloatingActionButton(
        onPressed: () => addItemEvent(context), // 버튼을 누를 경우 메모 추가 UI 표시
        tooltip: 'Add Item', // 플로팅 액션 버튼 설명
        child: Icon(Icons.add), // + 모양 아이콘
      ),
    );
  }
}

memoDetailPage.dart

더보기
// 선택한 항목의 내용을 보여주는 추가 페이지
// ignore_for_file: use_build_context_synchronously, avoid_print

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_memo_app/memoPage/memoDB.dart';
import 'package:flutter_memo_app/memoPage/memoListProvider.dart';
import 'package:flutter_memo_app/memoPage/memoMainPage.dart';
import 'package:provider/provider.dart';

class ContentPage extends StatefulWidget {
  // 생성자 초기화
  final dynamic content;
  const ContentPage({Key? key, required this.content}) : super(key: key);

  @override
  State<ContentPage> createState() => _ContentState(content: content);
}

class _ContentState extends State<ContentPage> {
  // 부모에게 받은 생성자 값 초기화
  final dynamic content;
  _ContentState({required this.content});

  // 메모의 정보를 저장할 변수
  List memoInfo = [];

  // 앱 바 메모 수정 버튼을 이용하여 메모를 수정할 제목과 내용
  final TextEditingController titleController = TextEditingController();
  final TextEditingController contentController = TextEditingController();

  // 앱 바 메모 수정 클릭 이벤트
  Future<void> updateItemEvent(BuildContext context) {
    // 앱 바 메모 수정 버튼을 이용하여 메모를 수정할 제목과 내용
    TextEditingController titleController =
        TextEditingController(text: memoInfo[0]['memoTitle']);
    TextEditingController contentController =
        TextEditingController(text: memoInfo[0]['memoContent']);

    // 다이얼로그 폼 열기
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('메모 수정'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TextField(
                controller: titleController,
                decoration: InputDecoration(
                  labelText: '제목',
                ),
              ),
              TextField(
                controller: contentController,
                maxLines: null, // 다중 라인 허용
                decoration: InputDecoration(
                  labelText: '내용',
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              child: Text('취소'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
            TextButton(
              child: Text('수정'),
              onPressed: () async {
                String memoTitle = titleController.text;
                String memoContent = contentController.text;

                Navigator.of(context).pop();

                print('memoTitle : $memoTitle');
                // 메모 수정
                await updateMemo(content['id'], memoTitle, memoContent);

                // 업데이트 된 메모 정보 호출
                updateRefresh();

                // 메모 내용 업데이트
                setState(() {
                  memoInfo = context.watch<MemoUpdator>().memoList;
                });
              },
            ),
          ],
        );
      },
    );
  }

  // 메모 삭제
  void deleteItemEvent(BuildContext context) {
    deleteMemo(memoInfo[0]['id']);
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => MyMemoPage(),
      ),
    );
  }

  // 메모 수정시 화면 새로고침
  Future<void> updateRefresh() async {
    List memoList = [];
    // DB에서 메모 정보 호출
    var result = await selectMemo(content['id']);

    // 특정 메모 정보 저장
    for (final row in result!.rows) {
      var memo = {
        'id': row.colByName('id'),
        'userIndex': row.colByName('userIndex'),
        'userName': row.colByName('userName'),
        'memoTitle': row.colByName('memoTitle'),
        'memoContent': row.colByName('memoContent'),
        'createDate': row.colByName('createDate'),
        'updateDate': row.colByName('updateDate')
      };
      memoList.add(memo);
    }
    print("memo update : $memoList");
    context.read<MemoUpdator>().updateList(memoList);
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    var memo = {
      'id': content['id'],
      'userIndex': content['userIndex'],
      'userName': content['userName'],
      'memoTitle': content['memoTitle'],
      'memoContent': content['memoContent'],
      'createDate': content['createDate'],
      'updateDate': content['updateDate']
    };
    List memoList = [];
    memoList.add(memo);

    // 빌드가 완료된 후 Provider의 데이터 읽기
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<MemoUpdator>().updateList(memoList);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 좌측 상단의 뒤로 가기 버튼
        leading: IconButton(
          icon: Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.pop(context, 1);
          },
        ),
        title: Text('메모 상세 보기'),
        actions: [
          IconButton(
            onPressed: () => updateItemEvent(context),
            icon: Icon(Icons.edit),
            tooltip: "메모 수정",
          ),
          IconButton(
            onPressed: () => deleteItemEvent(context),
            icon: Icon(CupertinoIcons.delete_solid),
            tooltip: "메모 삭제",
          ),
        ],
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Builder(builder: (context) {
            // 특정 메모 정보 출력
            memoInfo = context.watch<MemoUpdator>().memoList;

            return Stack(
              children: <Widget>[
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    SizedBox(),
                    Text(
                      memoInfo[0]['memoTitle'],
                      style:
                          TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
                    ),
                  ],
                ),
                Column(
                  children: [
                    SizedBox(height: 35),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('작성자 : ${memoInfo[0]['userName']}')],
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('작성일 : ${memoInfo[0]['createDate']}')],
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [Text('수정일 : ${memoInfo[0]['updateDate']}')],
                    ),
                    Expanded(
                      child: Padding(
                        padding: const EdgeInsets.all(15.0),
                        child: SizedBox(
                          height: double.infinity,
                          width: double.infinity,
                          child: Text(
                            memoInfo[0]['memoContent'],
                          ),
                        ),
                      ),
                    )
                  ],
                ),
              ],
            );
          }),
        ),
      ),
    );
  }
}

main.dart

더보기
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';
import 'package:flutter_memo_app/loginPage/loginMainPage.dart';
import 'package:flutter_memo_app/memoPage/memoListProvider.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => MemoUpdator()),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MemoApp',
      home: TokenCheck(),
    );
  }
}

참고

반응형