개발/Flutter

Flutter | 간단 메모 앱 만들어보고 광고까지 달아서 배포하기 - 3. 메모 작성 화면 만들기

ffuny 2023. 2. 13. 23:18
반응형

이번에는 메모를 작성하는 화면을 만들어 보겠습니다. 새로운 화면이기때문에 새로운 dart 파일을 만들어 줍니다. note_edit_page.dart 파일을 만들어 주고 statefulWidget로 만들어 준뒤 클래스 명을 NoteEditPage로 지정해줍니다. material로 import해주시면 됩니다. Scaffold를 만들어 주신 후 appBar에 title을 Text로 노트 편집으로 작성해 줍니다.

body는 Column으로 만드시고 mainAxisSize는 min으로 설정해 줍니다. 제목을 입력받기 위해서는 TextField라는 위젯을 사용합니다. TextField를 만들어 주신 후 테두리 선을 만들기 위해 decoration 속성에 InputDecoration을 사용해 줍니다. 그리고 그 안에 border 속성에 OutlineInputBorder 위젯을 넣어주시고, 현재 작성하는 텍스트 필드가 제목을 입력하는 부분이기 때문에 그 안의 label 필드에 Text('제목 입력')으로 해주시면 됩니다. 그리고 제목이기 때문에 1줄만 보여주기위해서 TextField의 maxLines는 1로 넣어주시고 글씨를 크게 보여주기 위해서 style 속성에 TextStyle 위젯에 fontSize를 20으로 해주시면 됩니다. 그리고 해당 TextField의 값을 읽거나 편집등 관리하기 위해서 TextEditingController를 만들어 주겠습니다. _NoteEditPageState 클래스 아래 @override의 위에 final TextEditingController titleController = TextEditingController();를 만들어 준뒤 TextField의 controller 속성에 titleController을 넣어주면 됩니다. 제목입력부분은 끝이고 다음으로 본문입력부분을 만들어줘야하는데 해당 부분의 사이가 떨어져 있어야 보기 좋기 때문에 SizedBox를 만들고 height를 8을 넣어주겠습니다. 내용을 입력하는 부분도 TextFiled로 만들어 줍니다. 내용 입력 부분은 테두리를 없애주도록 하겠습니다. decoration에 InputDecoration은 똑같이 만들어준 뒤, border에 InputBorder.none를 넣어줘서 테두리 선을 없애주겠습니다. 그리고 hintText 속성에 '내용 입력'을 넣어줘서 내용을 입력하는 부분인지 알도록 해줍니다. TextField가 입력내용의 양에 따라서 자동으로 길이가 길어지도록 하기 위해서 maxLines를 null로 해주겠습니다. 또한 엔터로 다음줄로 넘어가게 하기 위해서 keyboardType를 TextInputType.multiline를 넣어줍니다. 그리고 이 TextField 또한 관리를 위해서 TextEditingController를 bodyController로 위에 똑같이 만들어 준 뒤, controller 속성에 넣어주겠습니다. 현재까지 만들어진 화면을 보기 위해서는 메모의 목록에 버튼을 만들어서 페이지를 보이게 할 수도 있지만, 현재까지만 만들어진걸 보기위해서 main.dart의 home을 지금 만들어진 페이지가 보이게 바꿔주겠습니다. home을 NoteEditPage()로 바꿔주시면 되겠습니다.

실행시 위와같이 화면이 보이면 정상작동하는 것입니다. material3라서 appbar에 색은 따로 넣어주시면 되겠습니다. 현재 실행화면을 보시면 뭔가 답답하실겁니다. 여백이 없어서 꽉막힌 느낌이 듭니다. 또한 본문 내용이 너무 길어지면 overflow에러가 나는데 내용이 길어지면 scroll로 볼수있도록 해주면 해당 에러를 해결할 수 있습니다. column을 SingleChildScrollView로 감싸주고 padding 속성에 가로 horizontal을 12을 세로 vertical을 16을 넣어주시면 됩니다.

스크롤도 잘 작동하고 padding도 잘 넣어진것을 확인할 수 있습니다.

이제 작성기능은 끝났으니 메모의 색을 선택하는 부분을 만들어 보겠습니다.

클래스 상단에 Color의 memoColor를 만들어 줍니다. 기본값은 Note.colorDefault를 받아오겠습니다. 그리고 SingleChildScrollView를 Container로 감싸주겠습니다. Container에 color 속성에 memoColor를 넣어주시면 색상의 값에 따라 변하는 환경은 만들었습니다. 이제 이 컬러값을 선택하는 부분을 만들어 주겠습니다. 이 부분은 AppBar에 만들어주겠습니다. AppBar에는 actions이라는 속성이 존재하는데 해당 영역을 누르면 action을 취하는 부분입니다. AppBar의 우측에 만들어 집니다. actions에 IconButton을 만들어 주고 icon에는 Icon(Icons.color_lens)로 팔레트 모양을 추가 시켜줍니다. 그리고 tooltip에 길게 눌렀을때 나올 설명으로 '배경색 선택'을 추가해 줍니다. 그리고 onPressed는 눌렀을때 작동인데 이 때 우리는 색을 선택하는 창을 띄워줄 것입니다. 그래서 하단에 build 위젯 밖에 void _displayColorSelectionDialog(){ }을 만들어 줍니다. 그리고 만약 글 작성중에 키보드가 있는 상태에서 하면 색 선택을 가릴수 있어서 우선 키보드를 내리는 코드를 먼저 작성해 줍니다. FocusManager을 이용해서 instance의 primaryFocus를 unfocus 해주시면 됩니다. FocusManager.instance.primaryFocus!.unfocus();라고 작성해 주시면 됩니다. 그리고 이제 Dialog를 띄워주겠습니다. showDialog를 사용해주시면 됩니다. 그러면 context 속성에는 현재의 context를 그대로 사용해 주시면 됩니다. builder에는 (context){}로 변경해 주시고 가장 기본적으로 사용되는 AlertDialog를 return 해주시면 됩니다.

showDialog(context: context, builder: (context){
  return AlertDialog();
},);

title 속성에는 Text로 제목인 배경색 선택을 넣어주시면 됩니다. content에는 색을 세로로 나열할 것이기 때문에 Column을 넣어주겠습니다. mainAxisSize는 최소 크기만 사용하도록 min으로 해줍니다. children에는 ListTile을 한번 사용하겠습니다. title에는 색을 나타내는 부분을 작성해 주시면 됩니다. 가장 처음은 색이 없는 없음을 넣어주겠습니다. onTap은 눌렀을때 작동하는 부분입니다. 색 선택에 공통으로 작동하는 부분이므로 하단에 _applyColor로 만들어 주겠습니다. 하단에 void_applyColor(Color newColor){}로 만들어 줍니다. 그 안에 색을 선택할때 화면이 바뀌는 형태이므로 setState((){});를 만들어 주시면 됩니다. 그리고 그 안에 색을 선택하면 Dialog가 꺼져야 하므로 Navigator.pop(context);를 해주시면 됩니다. 다음으로 선택한 색이 반영 되도록 memoColor = newColor;를 해주시면 됩니다. 그리고 onTap에 () => _applyColor(Note.colorDefault), 를 해줘서 색 없음이므로 기본색으로 선택되게 만들어 줍니다. 이제부터는 색이 있는 부분을 만들어 주겠습니다. 똑같이 ListTile을 만들어 주는데 leading 속성에 CircleAvatar로 원형으로 색이 보이게 해줍니다. backgroundColor 속성에 Note.colorRed를 넣어주고 title에는 Text('빨간색'), onTap에는 _applyColor에 Note.colorRed를 넘겨주면 됩니다. 다른색들도 다 만들어 주시면 됩니다.

ListTile(
  title: Text('없음'),
  onTap: () => _applyColor(Note.colorDefault),
),
ListTile(
  leading: CircleAvatar(backgroundColor: Note.colorRed,),
  title: Text('빨간색'),
  onTap: ()=>_applyColor(Note.colorRed),
),
ListTile(
  leading: CircleAvatar(backgroundColor: Note.colorLime,),
  title: Text('연두색'),
  onTap: ()=>_applyColor(Note.colorLime),
),
ListTile(
  leading: CircleAvatar(backgroundColor: Note.colorBlue,),
  title: Text('파란색'),
  onTap: ()=>_applyColor(Note.colorBlue),
),
ListTile(
  leading: CircleAvatar(backgroundColor: Note.colorOrange,),
  title: Text('주황색'),
  onTap: ()=>_applyColor(Note.colorOrange),
),
ListTile(
  leading: CircleAvatar(backgroundColor: Note.colorYellow,),
  title: Text('노란색'),
  onTap: ()=>_applyColor(Note.colorYellow),
),

그리고 AppBar의 onPressed에 _displayColorSelectionDialog를 넣어줘서 아이콘을 누르면 Dialog가 뜨게 만들어주시면 됩니다.

길게 눌러보시면 툴팁이 나오는걸 확인할 수 있고 배경을 선택하면 색이 변하는 것도 확인할 수 있습니다. 하지만 전체가 아닌 Container로 감싼부분만 색이 변하는것을 볼 수 있습니다. 간단하게 Conainer를 SizedBox.expand로 감싸주시면 전체로 바뀐것을 확인 할 수 있습니다.

이제 색도 변경할 수 있으니 관리하는 기능을 만들어 보겠습니다. data 폴더에 note_manager.dart 파일을 만들어 준 뒤 NoteManager 클래스를 만들어 주시면 됩니다. 그리고 Note를 여러개 저장해야 하므로 List<Note>로 만들어 주고 외부에서 직접 접근하지 못하도록 _를 붙여서 _notes = [];로 빈 리스트로 만들어 줍니다. 그리고 notes에 추가하는 addNote 함수를 만들어 주겠습니다. Note 인자값을 받아오고 _notes에 add 해주시면 됩니다. _notes.add(note); 작성해주시면 됩니다.

삭제인  deleteNote도 만들어 주겠습니다. 인자값으로 int index를 받아오면 됩니다. 리스트는 해당값과 일치하면 삭제할수도 있지만 메모앱같이 간단한 형태는 해당 list의 index값으로 지우는게 더 간편합니다. _notes에 removeAt(index)로 해당 index의 Note를 삭제 해주시면 됩니다. 그리고 Note를 받아오는 getNote도 만들어 보겠습니다. delete와 같이 int index를 인자로 받아옵니다. 하지만 이 함수는 값을 넘겨줘야 하므로 void가 아닌 Note로 해주셔야 합니다. 그리고 return에 _notes[index];를 넘겨주시면 해당 index의 Note를 확인 할 수 있습니다. 그리고 전체 Note를 불러오는 listNotes 함수도 만들어 주겠습니다. 전체이므로 List<Note>로 해주시면 됩니다. return으로 그냥 _notes를 넘겨주시면 됩니다. 마지막으로 수정하는 updateNote를 만들어 주겠습니다. int index와 Note note를 인자로 받아 옵니다. 그리고 _notes[index]의 값을 note로 저장해주시면 됩니다.

void addNote(Note note){
  _notes.add(note);
}

void deleteNote(int index){
  _notes.removeAt(index);
}

Note getNote(int index){
  return _notes[index];
}

List<Note> listNotes(){
  return _notes;
}

void updateNote(int index,Note note){
  _notes[index] = note;
}

이 메모앱에서는 목록화면과 작성화면에서 동일한 목록이 공유되어야 하고 이를 위해 메모를 관리하는 NoteManager가 단 한개만 있어야 합니다. 이를 위해 NoteManager의 생성과 접근을 관리하고 한개의 인스턴스만 존재하게 하는 솔루션이 있어야 합니다. 이를 위해 lib에 providers.dart 파일을 만들어 줍니다. 그리고 NoteManager를 저장하는 공간을 선언합니다. NoteManager? _noteManager; 그리고 noteManager를 필요할때 사용할 noteManager 함수를 만들어 주겠습니다. NoteManager noteManager(){}을 만들어 줍니다. 우선 이 함수가 호출 될때 NoteManager 클래스의 인스턴스가 생성되어있는지 확인해 주어야 합니다. if문에 _noteManager이 null이면 _noteManager에 NoteManager()를 생성해 주시면 됩니다. 만약 있다면 if문은 지나가므로 바로 return에 _noteManager!를 반환해주시면 됩니다. 안드로이드 스튜디오에서는 if문을 작성하면 _noteManager ??= NoteManager();로 바꿀 수 있는것 같습니다. 이제 샘플로 해뒀던 _buildCards의 list를 바꿔보겠습니다. list를 삭제해주시고 return의 notes를  noteManager().listNotes()로 바꿔서 매니저에서 노트를 불러오면 됩니다.

List<Widget> _buildCards() {
  return noteManager().listNotes().map((note) => _buildCard(note)).toList();
}

이제 저장도 손봐주겠습니다. note_edit_page로 이동해 주겠습니다. 그리고 actions에 IconButton을 만들어 줍니다. icon은 save로 만들어 주고 tooltip은 저장으로 해줍니다. onPressed에 기능을 바로 만들어도 좋지만 하단에 따로 만들어 주겠습니다. _applyColor밑에 void로 _saveNote를 만들어 줍니다. 저장을 하기 위해서는 내용이 비어있지 않아야 합니다. if문에 bodyController.text가 inNotEmpty일때 noteManager()에 addNote를 해줍니다. 보내줄 인자는 Note에 bodyController.text와 title로 titleController.text, color로 memoColor를 보내줍니다. 그리고 비어있을때 else에는 내용이 없다는 스낵바를 띄워주겠습니다. ScaffoldMessenger을 사용하면 SnackBar를 사용할 수 있습니다. ScaffoldMessenger.of(context).showSnackBar()로 보여줄 수 있습니다. 이때 SnackBar로 보내는데 context에는 Text로 내용을 입력하세요. 를 작성해 주고 띄우는 모양은 behavior로 바꿔줄수 있습니다.SnackBarBehavior의 floating으로 띄워주겠습니다.

위와같이 스낵바도 잘나오고 본문을 작성했다면 저장되었습니다. 하지만 현재는 작성페이지와 연결을 시켜주지 않았고 작성 후 목록으로 이동하고 작성페이지가 초기화 되는 등 여러가지 해줘야 할것이 남았습니다. 다음에 한번 해보도록 하겠습니다. 오늘의 전체 코드 입니다.

import 'package:flutter/material.dart';
import 'package:sticky_memo/data/note.dart';
import 'package:sticky_memo/providers.dart';

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

  @override
  State<NoteEditPage> createState() => _NoteEditPageState();
}

class _NoteEditPageState extends State<NoteEditPage> {
  final TextEditingController titleController = TextEditingController();
  final TextEditingController bodyController = TextEditingController();

  Color memoColor = Note.colorDefault;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('노트 편집'),
        backgroundColor: Colors.blue,
        actions: [
          IconButton(
            onPressed: _displayColorSelectionDialog,
            icon: const Icon(Icons.color_lens),
            tooltip: '배경색 선택',
          ),
          IconButton(
            onPressed: _saveNote,
            icon: const Icon(Icons.save),
            tooltip: '저장',
          ),
        ],
      ),
      body: SizedBox.expand(
        child: Container(
          color: memoColor,
          child: SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                TextField(
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                    label: Text('제목 입력'),
                  ),
                  maxLines: 1,
                  style: const TextStyle(
                    fontSize: 20,
                  ),
                  controller: titleController,
                ),
                const SizedBox(
                  height: 8,
                ),
                TextField(
                  decoration: InputDecoration(
                    border: InputBorder.none,
                    hintText: '내용 입력',
                  ),
                  maxLines: null,
                  keyboardType: TextInputType.multiline,
                  controller: bodyController,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  void _displayColorSelectionDialog() {
    FocusManager.instance.primaryFocus!.unfocus();

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text('배경색 선택'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                title: Text('없음'),
                onTap: () => _applyColor(Note.colorDefault),
              ),
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Note.colorRed,
                ),
                title: Text('빨간색'),
                onTap: () => _applyColor(Note.colorRed),
              ),
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Note.colorLime,
                ),
                title: Text('연두색'),
                onTap: () => _applyColor(Note.colorLime),
              ),
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Note.colorBlue,
                ),
                title: Text('파란색'),
                onTap: () => _applyColor(Note.colorBlue),
              ),
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Note.colorOrange,
                ),
                title: Text('주황색'),
                onTap: () => _applyColor(Note.colorOrange),
              ),
              ListTile(
                leading: CircleAvatar(
                  backgroundColor: Note.colorYellow,
                ),
                title: Text('노란색'),
                onTap: () => _applyColor(Note.colorYellow),
              ),
            ],
          ),
        );
      },
    );
  }

  void _applyColor(Color newColor) {
    setState(() {
      Navigator.pop(context);
      memoColor = newColor;
    });
  }

  void _saveNote() {
    if (bodyController.text.isNotEmpty) {
      noteManager().addNote(Note(
        bodyController.text,
        title: titleController.text,
        color: memoColor,
      ));
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('내용을 입력하세요.'),
        behavior: SnackBarBehavior.floating,
      ));
    }
  }
}

note_edit_page.dart

import 'package:sticky_memo/data/note.dart';

class NoteManager{
  final List<Note> _notes = [];

  void addNote(Note note){
    _notes.add(note);
  }

  void deleteNote(int index){
    _notes.removeAt(index);
  }

  Note getNote(int index){
    return _notes[index];
  }

  List<Note> listNotes(){
    return _notes;
  }

  void updateNote(int index,Note note){
    _notes[index] = note;
  }
}

note_manager.dart

import 'package:sticky_memo/data/note_manager.dart';

NoteManager? _noteManager;

NoteManager noteManager(){
  _noteManager ??= NoteManager();
  return _noteManager!;
}

providers.dart

반응형