본문 바로가기
스파르타/Flutter

[Flutter] (숙제) 추가 기능 구현 - ( week 3 )

by bakcoding_sparta 2023. 4. 23.

마이메모 앱에 추가 기능을 구현한다.

 

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);
              }