Flutter - 메모 추가/수정/삭제하기 (1/2) - MySQL 연동, Provider 설정, 메모 조회

 

메모 관리를 위한 MySQL 설정

MySQL - 테이블 생성

  • memo 테이블 생성
  • 컬럼은 아래와 같음
  • 특이사항
    • 메모 추가 시간 자동 등록 : createDate의 Deafault/Expression : now()
    • 메모 수정 시간 자동 업데이트 : updateDate의 Default/Expression : now() on update now()
      • Default/Expression에 위와 같이 입력하면 자동으로 그림처럼 변경 됨

 

  • FK 설정
    • 이 포스팅은 로그인된 유저에게만 메모를 작성하도록 설계되어 있음
    • 로그인되어 있는 유저의 인덱스 번호가 필요

 

  • 참고 : users 테이블의 정보는 다음과 같음


Flutter - MySQL 연동

포스팅의 내용이 매우 길어질 것 같아서 아래의 포스팅의 링크로 대처 합니다.


Flutter - 메모 관리를 위한 DB 설계

  • 메모 DB 쿼리 관련 코드를 두기 위한 파일 생성
  • memoDB.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();
  }
}

 

전체 코드(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();
  }
}

메모 업데이트를 위한 Provider 설정

참고 : 이 포스팅에서 말하는 메모 업데이트는 메모가 추가되거나 변경, 삭제될 경우 리스트 뷰에 출력되는 메모가 정적이지 않고 동적으로 데이터베이스의 데이터에 맞게 변동되는 것을 의미합니다.


프로바이더를 사용하는 이유?

메모가 추가, 변경, 삭제 될 경우 화면은 정적으로 보여집니다.

우리는 메모의 상황에 따라 삭제 될 경우에는 삭제 된 메모 리스트가 출력되며,

수정될 경우에는 수정된 메모의 내용이 리스트 뷰에 출력되기를 원합니다.

그러기 때문에 상태 StatefulWidget으로 상태관리를 하여야 하는데,

이를 편리하게 상태를 관리하게 해주는 패키지가 Provider입니다.

기본 개념만 잘 알고 있다면 Provider없이 해줄 수 있습니다만...

이상하게 그냥 쓰면 자꾸 빌더에서 오류나서... 이 포스팅에서는 프로바이더를 사용합니다.


Flutter 메인 함수에 Provider 래핑

  • main.dart
    • 참고 : 아래의 소스 코드 사용시 오류가 발생한다면, MaterialApp이 최상위에 선언되지 않아서 오류가 날 가능성이 큽니다. 자신의 프로젝트에서 오류가 발생한다면 하위 요소가 MaterialApp인지 확인해주세요.
    • 이 포스팅에서는 MyApp의 하위 위젯은 MaterialApp입니다.
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => MemoUpdator()),
      ],
      child: const MyApp(),
    ),
  );
}

메모 상태 관리를 위한 Provider Class 정의

  • 메모 라는 변수 리스트에 상태 관리를 위한 프로바이더 클래스 정의
  • 파일명 : 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


코드 설계 (memoMainPage.dart)

  • 메모 정보를 저장할 변수 선언 및 초기화
  // 메모 리스트 저장 변수
  List items = [];

 

  • DB에 있는 메모를 불러오기 위한 함수 선언
  • 비동기로 메모 리스트를 받아와서 메모가 존재하면 리스트뷰에 메모를 보여주기 위함
  // 메모 리스트 출력
  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(memoList);
    context.read<MemoUpdator>().updateList(memoList);
  }

 

  • 앱 실행시 메모가 보여지기 위해 위에서 정의한 getMemoList 함수 호출
  • getMemoList 함수는 MySQL DB의 메모 정보를 불러와 변수의 값을 Provider로 상태 변경을 하는 기능을 함
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    getMemoList();
  }

 

  • 페이지 본문
    • 빌더 위젯을 상위에 래핑하여 메모의 유무에 따라 보여지는 페이지 설계
    • context.watch<T>() 메서드를 이용하여 데이터 읽기
      • DB에 메모 정보가 없으면 '표시할 메모가 없습니다.' 라는 페이지 표시
      • DB에 메모 정보가 있으면 메모 리스트를 리스트뷰에 표시
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          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'];

					  // 각각의 메모 카드에 표시
                      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),
                        ),
                      );
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

전체 소스 코드

// 메모 페이지
// 앱의 상태를 변경해야하므로 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> {
  // 메모 리스트 저장 변수
  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(memoList);
    context.read<MemoUpdator>().updateList(memoList);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          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'];

                      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), // + 모양 아이콘
      ),
    );
  }
}

참고