반응형
포스팅 참고 사항
포스팅에서 진행한 환경
- 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이름 정도만 바꾸셔도 됩니다.
다른 설명이 필요하면 아래의 링크를 참고해주세요.
- 01. Flutter - ListView, Card, Navigator - 스크롤 가능한 목록 표시, 목록 선택시 특정 목록 내용 보여주기
- 02. Flutter - StatefulWidget 활용 - 실시간 검색 기능 구현하기
- 03. Flutter - Floating Action Button, showDialog - 플로팅 액션 버튼으로 특정 작업 수행하기(리스트뷰 항목 추가하기)
- 04. Flutter - BottomNavigationBar - 다른 페이지로 이동하기(화면 전환)
- 05. Flutter - 파일 분리하기, class 나누기, 위젯 리소스화하기
- 06. Flutter - 로그인, 로그아웃 구현하기 (1/3) - MySQL DB 연동, 레이아웃 설계
- 06. Flutter - 로그인, 로그아웃 구현하기 (2/3) - 로그인, 로그아웃 기능 구현
- 06. Flutter - 로그인, 로그아웃 구현하기 (3/3) - 자동 로그인 기능 구현
- 07. Flutter - 메모 추가/수정/삭제하기 (1/2) - MySQL 연동, Provider 설정, 메모 조회
- 07. Flutter - 메모 추가/수정/삭제하기 (2/2) - 메모 앱 만들기 기능 구현
부가 설명 (이번 포스팅은 참고할 것이 꽤 많습니다.)
- MySQL 연동 관련
- Future 함수, 비동기 프로그래밍 관련
- 앱실행시 상태 초기화 관련 (액티비티 라이프 사이클)
- Provider 상태 관리 관련
메모 추가
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
- 메모 추가 절차
- 플로팅 액션 버튼 클릭
- 메모를 작성할 수 있는 작은 다이얼로그 출력
- 다이얼로그에서 제목과 내용을 입력하고 '추가' 버튼 클릭 시 메모 등록
- '취소' 버튼 클릭 시 아무 이벤트 발생하지 않음
- 메모 저장 사항
- 작성한 메모는 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
- 메모 수정 절차
- memoMainPage에서 리스트뷰의 목록에서 특정 인덱스의 메모(카드) 클릭
- 특정 메모를 보여주는 페이지로 이동
- 이동된 memoDetailPage에서 앱 바의 우측 상단에 글쓰기 버튼 클릭
- 다이얼로그에서 제목과 내용을 입력하고 '수정' 버튼 클릭 시 메모 수정
- '취소' 버튼 클릭 시 아무 이벤트 발생하지 않음
- 메모 수정 사항
- 작성한 메모는 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(),
);
}
}
참고
반응형