마이메모 앱에 추가 기능을 구현한다.
1. Pin 기능 구현
Pin 버튼을 눌렀을 때 리스트가 정렬되도록 한다.
Pin이 눌려진 메모는 리스트 상단에 배치되도록 한다.
우선 Memo에 Pin상태를 체크할 새로운 변수를 선언한다.
class Memo {
Memo({
required this.content,
required this.isPin,
});
String content;
bool isPin;
변수가 추가됨에 따라 객체가 생성될 때 해당 변수를 추가로 값을 넣어준다.
그리고 로컬에서 데이터를 가져올 때 해당 값이 null이기 때문에 에러가 발생하는걸 예외처리해 준다.
return Memo(content: json['content'], isPin: json['isPin'] ?? false);
Pin상태에 따라 목록에서 위치가 이동되는데 이를 재정렬하는 함수를 만들어 버튼이 클릭되고 값이 변경될 때마다 리스트 정렬을 해주도록 한다.
sortingListWithPin() {
for (int i = 0; i < memoList.length; i++) {
if (memoList[i].isPin) {
for (int j = 0; j < memoList.length; j++) {
if (memoList[j].isPin) {
if (i == j) {
break;
}
continue;
} else {
Memo temp_memo = memoList.removeAt(i);
memoList.insert(j, temp_memo);
print('재정렬됨');
}
}
}
}
notifyListeners();
saveMemoList();
}
리스트가 정렬될 때 새로 화면을 그려주도록 하고 값도 로컬에 다시 저장해 준다.
이 부분을 Pin 아이콘이 클릭되는 곳에서 호출한다.
// main.dart
leading: IconButton(
icon: memo.isPin
? Icon(CupertinoIcons.pin_fill)
: Icon(CupertinoIcons.pin),
onPressed: () {
var isPin = memo.isPin;
print('$memo : pin 클릭 됨 $isPin');
memo.isPin = !memo.isPin;
memoService.sortingListWithPin();
},
),
아이콘 역시 isPin 값에 따라서 모양을 바꾸어준다.
2. 메모 수정 시간 저장
메모가 작성되거나 수정될 때마다 시간값을 저장한다.
Memo에 시간을 저장할 새로운 변수와 화면에 그려줄 새로운 텍스트 위젯도 필요하다.
먼저 각 리스트에 생성된 위젯은 ListTile이다. 이 위젯에서 가장 우측에 위젯을 생성하려면 trailing 속성을 사용하면 된다.
해당 속성으로 시간이 배치될 위젯을 마련해 준다.
그리고 시간을 가져오기 위해서 DateTime 클래스의 Now를 사용한다.
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
saveTime: json['saveTime'] ?? "null",
isPin: json['isPin'] ?? false);
}
이미 저장된 로컬 데이터 때문에 예외처리를 해주어야 한다.
DateTime을 가져오고 형식을 변경해주어야 할 것 같다. 그리고 시간이 변경되는 위치가 생성될 때와 메모 내용이 변경될 때이기 때문에 해당 함수에서 기능을 추가해야 되기 때문에 시간을 반환하는 새로운 함수를 생성한다.
String getDateTimeNow() {
DateTime now = DateTime.now();
String formattedDate =
'${now.year}-${twoDigits(now.month)}-${twoDigits(now.day)} ${twoDigits(now.hour)}:${twoDigits(now.minute)}:${twoDigits(now.second)}';
print('시간 저장 $formattedDate');
return formattedDate;
}
String twoDigits(int n) {
if (n >= 10) {
return '$n';
}
return '0$n';
}
현재 시간을 가져와서 가공한 다음에 문자열로 반환하는 함수이다. 이 함수를 create, update에서 호출해 주도록 한다.
createMemo(
{required String content,
required String saveTime,
required bool isPin}) {
Memo memo = Memo(content: content, saveTime: saveTime, isPin: isPin);
memoList.add(memo);
memo.saveTime = getDateTimeNow();
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
memo.saveTime = getDateTimeNow();
notifyListeners();
saveMemoList();
}
그리고 확인해 본다.
원하는 대로 저장된 시간이 메모되고 있다.
3. 빈 메모 삭제
메모 목록에서 메모의 내용을 비워놓거나 새로 생성한 메모가 비어있을 경우 리스트에서 삭제하도록 한다.
해당 기능은 삭제 시점이 중요한데 뒤로 가기 버튼을 눌렀을 때 content를 검사하고 내용이 없을 경우 제거하도록 한다.
우선 Navigator.Push에서 해당 기능이 구현되어야 한다. Push에서 await을 걸고 페이지를 옮길 때 내용을 검사하고 비어있으면 삭제 처리하면 된다.
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: '',
saveTime: DateTime.now().toString(),
isPin: false);
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
if (memoService
.memoList[memoService.memoList.length - 1].content.isEmpty) {
memoService.deleteMemo(index: memoService.memoList.length - 1);
}
},
플로팅 버튼의 경우 새로 생긴 메모를 따로 변수에 저장해서 처리하는 게 코드를 간소화할 수 있을 것 같다.
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(
content: '',
saveTime: DateTime.now().toString(),
isPin: false);
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
var newIndex = memoService.memoList.length - 1;
if (memoService.memoList[newIndex].content.isEmpty) {
memoService.deleteMemo(index: newIndex);
}
},
지역변수 newIndex로 코드 단축
완성
해설 비교
isPin 변수를 선언할 때 초기값은 false이니 이걸 설정해 주어도 된다.
class Memo {
Memo({
required this.content,
required this.saveTime,
//required this.isPin,
this.isPin = false,
});
isPin 값을 변경하는 것도 service 내부에서 처리해 주도록 한다.
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPin = !memo.isPin;
notifyListeners();
saveMemoList();
}
중요한 게 있다. 나는 리스트를 정렬할 때 이중반복문으로 요소를 검사하고 다시 재배치했는데 이걸 간단하게 처리할 수 있다.
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPin = !memo.isPin;
memoList = [
...memoList.where((element) => element.isPin),
...memoList.where((element) => !element.isPin)
];
notifyListeners();
saveMemoList();
}
... : spread operator로 리스트나 맵의 요소를 펼 처서 새로운 리스트나 맵에 추가하는 역할을 한다.
즉 각 문장에서 memoList를 평가하여 나온 결과를 새로운 리스트에 추가하게 되고 그 결과로 리스트가 재구성되게 된다.
더 간단하고 효율적인 방법이 있었다....
DateTime
애초에 DateTime 변수로 저장하면 된다.
String content;
//String saveTime;
bool isPin;
DateTime? updateAt;
? 를 통해서 null이어도 상관없게 해 준다.
그리고 json으로 DateTime을 저장하기 위해서 문자열로 변경해 준다.
Map toJson() {
return {
'content': content,
'isPin': isPin,
'updatedAt': updateAt?.toIso8601String(),
};
}
json에서 DateTime으로 변환시키는 방법은 다음과 같다.
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
//saveTime: json['saveTime'] ?? "null",
isPin: json['isPin'] ?? false,
updateAt:
json['updatedAt'] == null ? null : DateTime.parse(json['updateAt']),
);
DateTime.parse로 json을 DateTime으로 바꾸어준다.
이제 값은 updateMemo와 createeMemo에서 경신되도록 한다.
createMemo(
{required String content,
required String saveTime,
required bool isPin}) {
//Memo memo = Memo(content: content, saveTime: saveTime, isPin: isPin);
Memo memo = Memo(content: content, isPin: isPin, updateAt: DateTime.now());
memoList.add(memo);
//memo.saveTime = getDateTimeNow();
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
memo.updateAt = DateTime.now();
//memo.saveTime = getDateTimeNow();
notifyListeners();
saveMemoList();
}
텍스트에서 해당 값을 불러올 때 예외처리도 해준다.
trailing: Text(memo.updateAt == null
? ""
: memo.updateAt.toString()),
시간 형식을 변경하는 방법도 더 간단하게 그냥 subString으로 문자열을 잘라주면 된다.
: memo.updateAt.toString().substring(0, 19)),
빈 메모 삭제에서 메모리스트 인덱스 접근하는 방법은 다양하지만 강의에서 나온 방법이 더 간단한 거 같다.
// var newIndex = memoService.memoList.length - 1;
// if (memoService.memoList[newIndex].content.isEmpty) {
// memoService.deleteMemo(index: newIndex);
// }
if (memoList.last.content.isEmpty) {
memoService.deleteMemo(index: memoList.length - 1);
}