Flutter

[Project] '만난지 며칠' 날짜 앱 만들기 - Chp 4. 메모 기능

모리선생 2023. 5. 26. 01:42
728x90

목표

만난지 며칠이 지났는지 확인할 수 있는 날짜 앱을 만들어보자. 이를 응용하여 100, 200, 300, 1년, 2주년까지 확인할 수 있도록 확장해보자.


본 내용을 전현직 마케터이자 현직 개발자 및 지망생이 개인공부를 하면서 배운 내용을 정리해놓은 글입니다. 해당내용에 있어서 내용상의 문의 사항 혹은 코드상에 수정사항이 발견이 되면 알려주시면 감사하겠습니다. 같이 고민해보고 수정할 수 있도록 하겠습니다.


화면은 구성하였으니 이제 기능을 조금 추가 해보려고 한다. 이번에는 메모기능을 넣을것이라 가장 편하게 사용할 수 있는 SharedPreferences를 사용한다. 

 

SharedPreference를 간단하게 설명하자면 다음과 같다. 안드로이드나 아이폰에서 보면 키-값 쌍을 영구적으로 저장해서 불러올 수있는 간단한 데이터 저장소라고 보면 된다. 주로 애플리케이션의 상태 및 저장등 소량의 데이터를 저장할때 사용한다.

 

해당 내용을 더 알고 싶다면? 이 글을 참고해보자.

https://riris01.tistory.com/16

 

[Practice] shared preference

목표 상대적으로 적은 양의 키-값의 데이터를 저장하기 위한 shared_preferences 플러그인의 의미를 알고 예제를 통해 활용법을 확인해보자. 디스크에 키-값의 데이터를 저장하는 이 플러그인은 iOS의

riris01.tistory.com

 

결과화면

자 그럼 오늘까지의 내용을 만들면 어떤 화면이 만들어질까? 사이드바에 있는 메뉴를 선택하면 다음과 같은 페이지로 이동을 하게 해놓았다. 

  • 첫번째 menu: 내용을 기록을 할 수 있는 페이지
  • 두번째 menu: 기록된 내용을 반환하여 List 형태로 보여주는 페이지

내용을 이렇게 분리 해놓은 이유는 사실 뭐 특별한 이유는 없고, 한 화면에 입력과 List가 모두 보여지고 있으면 내용이 지저분해질 것 같아서, 이렇게 구분을 해놓았다. 하지만 해당 내용을 통합을 할지 말지 일단은 고민중에 있다. 왜냐면 보통의 앱들은 상단에 내용을 적을 부분을 배치를 하고 그 밑에 리스트를 반환하는 식으로 만들어 놓은것도 더러 있기 때문이다.

 

MemoPage

해당 페이지는 title과 content를 적을 수 있는 페이지를 작성하기 위한 장소이다. 여기서 중요한 것이라고 하면 아무래도 SharedPreferenced와 ElevatedButton을 누르면 발생하는 SharedPreference 내 저장이 아닐까 싶다.

 

전체적인 코드는 다음과 같다.

@ memo_page.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';

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

  @override
  State<MemoPage> createState() => _MemoPageState();
}

class _MemoPageState extends State<MemoPage> {
  String title = '';
  String content = '';

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    title = prefs.getString('title') ?? '';
    content = prefs.getString('content') ?? '';
  }

  // Future<void> _clearSharedPreferences() async {
  //   final SharedPreferences prefs = await SharedPreferences.getInstance();
  //   await prefs.clear();
  // }

  void addNewMemory() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    DateTime currentTime = DateTime.now();
    DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");

    String formattedDateString = dateFormat.format(currentTime);
    String key = 'memory_$formattedDateString';
    Map<String, dynamic> memoryData = {
      'title': title,
      'content': content,
    };

    await prefs.setString(key, json.encode(memoryData));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('우리가 만난날'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              decoration: InputDecoration(
                hintText: '기억의 제목',
              ),
              onChanged: (value) {
                setState(() {
                  title = value;
                });
              },
            ),
            SizedBox(height: 32),
            TextField(
              decoration: InputDecoration(
                hintText: '상세 내용을 알려주세요',
              ),
              onChanged: (value) {
                setState(() {
                  content = value;
                });
              },
            ),
            SizedBox(
                height:
                    16), // Add some spacing between TextField and ElevatedButton
            ElevatedButton(
              onPressed: () async {
                addNewMemory();
              },
              child: Text('Save'),
            ),
          ],
        ),
      ),
    );
  }
}

addNewMemory는 SharedPreference를 사용하여 기기 내에 내용을 저장하기 위해서 작성을 하였다. 조금 더 자세하게 보자면, 저장이 되고 난 후에도 어떠한 값이 저장되었는지 보여주는 _loadData()의 void를 선언을 하여 주고 addNewMemory()에서는 현재 시간의 값 / 제목 / 그리고 내용 등을 저장할 수 있게 하였다. 시간의 경우에는 년-월-일 시간:분:초까지만 표시할 수 있도록 형식을 지정하였으며, 제목과 내용의 경우에는 Map 타입으로 선언하여 key-value 매칭을 가능하게 하였다. 

 

SharedPreference에 저장이 된 값은 아시다시피, 기기 내에 저장된 값이므로 다른 사람이 볼 수 있다는 단점이 있다. 물론 기기를 다른 사람이 보면은 가능하긴한 상황이지만, 여튼 물리적인 '강탈(?)'에 의한 정보 유출의 위험성은 존재하고 있으니 이렇게 간단한 값들만 저장할 수 있게 해야한다. 그리고 SharedPreference에 저장된 값은 앱을 삭제시 같이 삭제가 되므로 주의해야한다.

 (...)
 
 Future<void> _loadData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    title = prefs.getString('title') ?? '';
    content = prefs.getString('content') ?? '';
  }

  void addNewMemory() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    DateTime currentTime = DateTime.now();
    DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");

    String formattedDateString = dateFormat.format(currentTime);
    String key = 'memory_$formattedDateString';
    Map<String, dynamic> memoryData = {
      'title': title,
      'content': content,
    };

    await prefs.setString(key, json.encode(memoryData));
  }

(...)

 

MemoryNote

해당 부분의 경우에는 이제 입력한 내용들을 리스트 형태로 보여주기 위해 만들었다. 

 

전체 코드는 다음과 같으며, 여기서 주요한 몇개의 부분을 보자면

  • 전체적인 SharedPreference에 저장된 내용을 불러오는 부분
  • 필요없는 내용은 삭제 하는 부분
  • 전체 내용 삭제 부분
  • 내용을 리스트로 반환하는 부분

이렇게 총 4가지 부분이 있을 것이다.

 

전체코드

@memories_page.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:date_counter/memo_page.dart';

class MemoryNote extends StatefulWidget {
  MemoryNote({Key? key}) : super(key: key);

  @override
  State<MemoryNote> createState() => _MemoryNoteState();
}

class _MemoryNoteState extends State<MemoryNote> {
  List<Map<String, dynamic>> savedMemoryList = [];

  @override
  void initState() {
    super.initState();
    getAllSharedPrefs().then((value) {
      setState(() {
        savedMemoryList = value;
      });
    });
  }

  Future<void> _clearSharedPreferences() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.clear();
    setState(() {
      savedMemoryList = [];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: <Widget>[
          Row(
            children: [
              IconButton(
                onPressed: () async {
                  await _clearSharedPreferences();
                },
                icon: Icon(Icons.delete_forever),
              )
            ],
          )
        ],
      ),
      body: ListView.builder(
          itemCount: savedMemoryList.length,
          itemBuilder: (BuildContext context, int index) {
            String key = savedMemoryList[index].keys.first;
            DateTime timestamp = DateTime.parse(key.split('_').last);
            Map<String, dynamic> memoryData =
                json.decode(savedMemoryList[index].values.first);

            DateFormat dateFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
            String formattedDateString = dateFormat.format(timestamp.toLocal());

            return ListTile(
              title: Text(memoryData['title']), // 기억 제목 표시
              subtitle: Text(
                  "${memoryData['content']}-$formattedDateString"), // 기억 컨텐츠 표시
              trailing: IconButton(
                icon: Icon(Icons.delete),
                onPressed: () async {
                  await removeSharedPref(savedMemoryList[index].keys.first);
                  setState(() {
                    savedMemoryList.removeAt(index);
                  });
                },
              ),
            );
          }),
    );
  }
}

Future<List<Map<String, dynamic>>> getAllSharedPrefs() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  Set<String> allKeys = prefs.getKeys();
  if (allKeys.isEmpty) {
    return [];
  }

  List<Map<String, dynamic>> preferencesList = [];
  for (String key in allKeys) {
    var value = prefs.get(key);
    Map<String, dynamic> preference = {key: value};
    preferencesList.add(preference);
  }

  return preferencesList;
}

Future<void> removeSharedPref(String key) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.remove(key);
}

 

조금 더 자세하게 보자면,

GetAllSharedPrefs()의 value들을 모두 가져오도록 한다. 그리고 저장된 value들은 savedMemoryList의 변수로 할당한다. 

(...)

@override
  void initState() {
    super.initState();
    getAllSharedPrefs().then((value) {
      setState(() {
        savedMemoryList = value;
      });
    });
  }
  
(...)

 

그리고 다른 함수들을 설정을 했는데 대표적인 것들이 이 2가지 이다.

  • getAllSharedPrefs: allKeys.isEmpty라면 공란을 return을 하고 리스트가 존재를 한다면 preferencesList.add를 해서 추가된 값을 보여주도록 한다. 그렇게 해서 preferencesList를 return한다.
  • removeSharedPref: 저장된 key-value를 제거하는 것이다.
  • clearSharedPreferences: 모든 저장된 key-value를 삭제한다.
(...)

Future<List<Map<String, dynamic>>> getAllSharedPrefs() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  Set<String> allKeys = prefs.getKeys();
  if (allKeys.isEmpty) {
    return [];
  }

  List<Map<String, dynamic>> preferencesList = [];
  for (String key in allKeys) {
    var value = prefs.get(key);
    Map<String, dynamic> preference = {key: value};
    preferencesList.add(preference);
  }

  return preferencesList;
}

Future<void> removeSharedPref(String key) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.remove(key);
}

(...)

이렇게 만들어진 기능들을 토대로 ListView.builder를 사용하여 저장된 내용을 반환할 수 있도록 만들었다. 만들다보니 ListView를 만들어도 되겠지만 ListView의 경우에는 적은 자식의 수가 있을때는 사용하기 좋지만 자식의 수가 많아지면 ListView.builder가 편하다. 왜냐하면 필요할때 마다 저장된 내용을 저장소나 서버로 부터 불러오기 때문이다 (일명 LazyList라고도 불리는 듯 하다).

 

자 이제, 다음으로 추가할 기능은 다음과 같다.

  • 사진 아이콘 꾸미기
  • 로그인 기능 구현
  • image_picker를 이용한 사진 저장 및 리스트 반환
  • Carousel 기능을 이용하여 찍은 사진 순환 View 생성

뭐 만들때마다 괜찮은 기능을 하나씩 추가하고 있다. 이렇게 하다보면 재미있는 앱이 나올 듯 하다.

728x90