개발/Flutter

Flutter | 간단 메모 앱 만들어보고 광고까지 달아서 배포하기 - 5. 메모를 데이터 베이스에 저장하기

ffuny 2023. 2. 15. 19:08
반응형

지금까지 메모 앱의 메인 목록 화면과 작성, 수정 화면, 삭제기능까지 메모 앱의 기능과 화면을 다 완성했습니다. 하지만 메모 앱을 재 실행하면 초기화가 되고 실행 중일때만 저장 되는 문제가 남아있습니다. 이를 위해 여러 방법중 스마트폰의 내부 데이터베이스에 저장을 해서 실행 시 초기화가 되지 않고 저장된 목록을 볼 수 있도록 해보겠습니다. 우선 데이터베이스란 공유해서 사용할 목적으로 체계화해서 통합 관리하는 데이터 집합니다. 쉽게 말해서 대부분의 전산 시스템에서 사용하고 있습니다. 요즘엔 앱에서도 쉽게 볼 수 있습니다. 종류는 sql과 nosql이 있습니다. 여기서는 sql로 만들겠습니다. sql은 관계형 데이터 베이스로 쉽게 생각해서 엑셀이라 생각하시면 좋을것같습니다. 여기서는 sqflite라는 패키지를 사용해 보겠습니다. 이 패키지라는 것은 pub.dev 라는 사이트에서 쉽게 찾아 볼 수 있습니다. 패키지 내용에 대한 자세한 설명은 하나씩 봐야 알지만 어느정도는 구글링해서 찾아 볼 수 있습니다.

해당 홈페이지이고 검색에 sqflite를 검색 해보겠습니다.

검색한 화면에서 검색과 일치한 가장 위의 결과에 들어가 줍니다.

가장 최신 버전에 대한 부분이 제목에 나오고 패키지에 대한 설명, 지원 플랫폼, 변경사항, 예제등을 확인 할 수 있습니다. 그리고 설치 방법과 어떤 버전이 있나 확인 할 수 있습니다. 우리가 만들었던 프로젝트에 패키지를 추가해 보겠습니다.

우선 위의 창에서 installing로 이동하신 다음에 dependiencies 부분이 있는 곳에서 하단의 sqflite와 버전이 있는 부분을 복사해 줍니다. 다른 버전을 사용하시려면 versions에서 숫자 부분만 바꿔주시면 됩니다. 이제 우리 프로젝트의 pubspec.yaml로 이동한 뒤, dependencies: 의 하단에 flutter의 라인에 맞춰 복사한 부분을 붙여넣기 해준 뒤 우측상단에 pub get을 해서 패키지를 프로젝트에 설치 해주시면 됩니다. 이제 Note 클래스를 데이터베이스에서 사용할 수 있도록 변경해 주겠습니다. note.dart에서 변경해 주겠습니다. 우선 index 대신 사용 할 id를 추가해 주겠습니다. final int? id;로 추가해 주겠습니다. 노트마다 고유한 id를 부여하고 관리하는데 새노트 작성시 id가 없기 때문에 이를 처리하기 위해서 null을 처리 할 수 있게 ?를 넣어 주었습니다. 그리고 노트를 구성하는 데이터를 저장할 테이블 정보를 추가해주겠습니다.  우선 static const로 테이블 이름을 notes로 지정해주겠습니다. 다음으로 데이터들의 id와 title, body, color가 들어갈 column들을 static const로 지정해주겠습니다.

static const tableName = 'notes';

static const columnId = 'id';

static const columnTitle = 'title';

static const columnBody = 'body';

static const columnColor = 'color';

다음으로 Note 클래스의 정보를 데이터베이스에 저장하는 코드와 데이터베이스의 정보를 Note 클래스로 불러오는 코드를 만들어 보겠습니다. 우선 저장하는 형태로 바꿔주는 생성자를 만들어 주겠습니다. 데이터 베이스의 형태인 Map<String, dynamic> 형태로 toRow라는 생성자를 만들어 주겠습니다. return 값에 컬럼명과 값을 대응 시켜주시면 됩니다. id는 자동으로 관리하도록 만들것이기 때문에 여기에는 추가해 주지 않겠습니다. color는 RGB값을 표현하는 숫자를 저장하기 위해서 color.value로 넣어주시면 됩니다.

Map<String, dynamic> toRow() {
  return {
    columnTitle: title,
    columnBody: body,
    columnColor: color.value,
  };
}

다음은 불러오는 부분을 만들어 보겠습니다. fromRow라는 생성자를 만들어 주겠습니다. 이 생성자의 인자는 데이터 베이스의 저장 형태인 Map<String, dynamic>을 넣어주시면 됩니다. 그리고 선언된 생성자를 이용해 노트를 만들어 주겠습니다.

Note.fromRow(Map<String, dynamic> row)
    : this(
        row[columnBody],
        id: row[columnId],
        title: row[columnTitle],
        color: Color(row[columnColor]),
      );

저장된 값을 불러오는 것이기 때문에 id 값을 받아오고 color은 value 값으로만 저장되어 있기 때문에 Color로 감싸주시면 됩니다. 다음으로 노트 데이터를 데이터베이스로 관리하도록 noteManager 클래스를 바꾸고 적용해보겠습니다. note_manager.dart를 열어 주겠습니다. 빈 리스트는 사용하지 않으므로 삭제해 줍니다. 그리 static const로 데이터베이스 파일 이름을 notes.db로 해줍니다. 그리고 데이터 베이스 버전을 1로 추가해 줍니다. Database로 데이터 베이스 객체를 생성해 주겠습니다. 그리고 데이터베이스에서 받아올 함수 _getDatabase를 만들어 줍니다. 이때 이 함수를 Future<Database>로 만들어주는데 Future는 바로 값을 받아오는게 아니라 데이터베이스에서 지정된 행동을 끝까지 한 뒤에 객체를 받아 오는 것입니다. 앱이 메모리에서 가져오는 속도보다 파일로 저장된 데이터베이스는 값을 가져오는데 시간이 걸릴 수 있는데, 데이터베이스에서 값을 아직 다 받아 오지 않았는데 객체를 그냥 가져온다면 에러가 날 수 있습니다. 말하자면 비동기 방식으로 값을 받아오는 것입니다. 만약 데이터 베이스의 값을 가져오기까지 기다린다면 앱이 멈춰서 아무것도 하지 못 할것입니다. 그렇기 때문에 이런 작업들에서 Future를 사용합니다. 이제 database값을 반환해 줘야 하는데 만약 데이터베이스가 생성되어있지 않을 수 있기 때문에 _database가 null일때는 데이터베이스를 생성해 주겠습니다. 이때 openDatabase를 사용하는데 여기에 _databaseName과 _databaseVersion을 사용하고 creat 하는 것이기 때문에 onCreate로 만들어 줍니다. 함수로 sql문을 작성하고 execute로 실행해서 생성해 주겠습니다. 하지만 빨간 줄이 나오는데 openDatabase에서 Future를 반환하는데 값을 반환할때까지 기다려야 하므로 앞에 await를 추가해 줘야하는데 await를 사용하려면 이 함수에 async를 추가해 줘야 합니다. 그리고 마지막으로 return으로 _database를 넘겨줄텐데 이때 값은 null이 아니므로 !를 붙여주시면 됩니다.

Future<Database> _getDatabase() async {
  if (_database == null) {
    _database = await openDatabase(
      _databaseName,
      version: _databaseVersion,
      onCreate: (db, version) {
        final sql = '''
       CREATE TABLE ${Note.tableName} (
      ${Note.columnId} INTEGER PRIMARY KEY AUTOINCREMENT,
      ${Note.columnTitle} TEXT,
      ${Note.columnBody} TEXT NOT NULL,
      ${Note.columnColor} INTEGER NOT NULL
    )
      ''';
        return db.execute(sql);
      },
    );
  }
  return _database!;
}

현재 버전에서는 노란줄이 뜨는데 추천으로 수정을 하면 if 문도 바뀌고 final sql이 const sql로 바뀝니다.

Future<Database> _getDatabase() async {
  _database ??= await openDatabase(
      _databaseName,
      version: _databaseVersion,
      onCreate: (db, version) {
        const sql = '''
       CREATE TABLE ${Note.tableName} (
      ${Note.columnId} INTEGER PRIMARY KEY AUTOINCREMENT,
      ${Note.columnTitle} TEXT,
      ${Note.columnBody} TEXT NOT NULL,
      ${Note.columnColor} INTEGER NOT NULL
    )
      ''';
        return db.execute(sql);
      },
    );
  return _database!;
}

전 바뀐 버전으로 해보도록 하겠습니다. 이제 데이터 관리를 하는 부분들을 바꿔 보겠습니다. 우선 addNote부터 변경해 보겠습니다. 이제 데이터베이스 작업이 들어가므로 void에서 Future<void>로 바꿔주시면 됩니다. 그리고 데이터베이스를 가져와야 하기 때문에 async도 추가해주시면 됩니다. final db에 await로 _getDatabase()를 받아와 주시면 됩니다. 그리고 await db.insert로 값을 저장해 줄 수 있는데, 첫번째 인자로 tableName을 넣어주고 두번째 인자로 note를 Map형태로 바꾼 toRow()를 넣어주시면 됩니다. 다음은 delete를 수정 해주겠습니다. void를 Future<void>로 변경하고 async로 변경해줍니다. 그리고 이제 id 값을 받아오므로 index를 id로 변경해 줍니다. db를 똑같이 불러와 준 다음 db.delete를 해줍니다. 여기서 첫번째 인자는 tableName를 넣고 2번째는 삭제에서는 where로 조건을 넣어 줍니다. '${Note.columnId} = ?' 부분을 넣어서 id와 일치하는 부분을 삭제 해 줄것입니다. 3번째로  whereArgs로 where의 ? 에 들어갈 값을 넣어주는 부분인데 [id]를 넣어서 받아온 id를 넣어주시면 됩니다. 다음은 getNote를 변경해 주겠습니다. Future<Note>로 변경 해주시고 index를 id로 바꿔주신 후 async를 추가해줍니다. 위와 같이 _getDatabase 해주시면 됩니다. 이번에는 db.query로 조건에 맞는 부분을 가져오고 이를 final rows에 저장해 주겠습니다. await db.query를 해주시고 첫번째에 tableName를 넣어줍니다. 그리고 where로 delete와 같이 columnId를 찾아 주겠습니다. whereArgs에 [id]를 넣어줍니다. 이렇게 하면 id는 유니크하기때문에 1줄만 가져올 것입니다. 그래서 return에 Note.fromRow로 Note형태로 변환 시켜주고 그 안에 값을 rows.single로 보내주시면 됩니다. 다음은 전체 노트를 불러와 보겠습니다. listNotes를 Future<List<Note>>로 변경해주시고 async 해주신 뒤, _getDatabase로 db까지 불러와줍니다. 그리고 rows에 db.query에 tableName만 넣어주면 table의 전체 값을 불러 옵니다. 이를 return해 주는데 rows.map으로 각 row들을 Note.fromRow(row)로 note형태로 변경한 뒤 toList()로 리스트 형태로 바꿔서 return해주면 됩니다. 마지막으로 update를 변경해 주겠습니다. Future<void>로 변경하고 async 를 추가해준 뒤 _getDatabase로 db를 불러와 줍니다. 그 후 await db.update로 업데이트 해줍니다. tableName을 첫번째 인자에 넣어주고, 두번째 인자에는 넘겨주는 값인 note.toRow() 주면 됩니다. 세번째로 where에 columnId를 찾는 조건을 넣어주시고 whereArgs에 id를 넣어 주시면 됩니다.

static const _databaseName = 'notes.db';

static const _databaseVersion = 1;

Database? _database;

Future<void> addNote(Note note) async {
  final db = await _getDatabase();
  await db.insert(Note.tableName, note.toRow());
}

Future<void> deleteNote(int id) async {
  final db = await _getDatabase();
  await db.delete(
    Note.tableName,
    where: '${Note.columnId} = ?',
    whereArgs: [id],
  );
}

Future<Note> getNote(int id) async {
  final db = await _getDatabase();
  final rows = await db.query(Note.tableName,where: '${Note.columnId} = ?',whereArgs: [id],);
  return Note.fromRow(rows.single);
}

Future<List<Note>> listNotes() async {
  final db = await _getDatabase();
  final rows = await db.query(Note.tableName);
  return rows.map((row) => Note.fromRow(row)).toList();
}

Future<void> updateNote(int id, Note note) async{
  final db = await _getDatabase();
  await db.update(Note.tableName, note.toRow(),where: '${Note.columnId} = ?', whereArgs: [id],);
}

이제 화면 구성 페이지들에서 변경해 주겠습니다. 우선 note_edit_page.dart로 이동해 주시고 상단에 index를 id로 바꿔주겠습니다. 그냥 변경하면 아래까지 하나하나 변경 해야 하지만 index를 우클릭 후 refactor - rename을 해주시면 관련된 부분이 전부 바뀝니다. noteIndex도 noteId로 변경해 주겠습니다. 그리고 getNote를 변경해 주겠습니다. Future로 반환받은 후에 변경해야 하므로 저장 하는 앞 부분을 없애 주고 getNote 뒤에 then을 달아 줍니다. 그리고 그 안에 title과 body 값을 넣어 줍니다. color는 화면이 바뀌어야 하므로 setState안에 넣어주시면 됩니다.

@override
void initState() {
  // TODO: implement initState
  super.initState();
  final noteId = widget.id;
  if (noteId != null) {
    noteManager().getNote(noteId).then((note) {
      titleController.text = note.title;
      bodyController.text = note.body;
      setState(() {
        memoColor = note.color;
      });
    });
  }
}

다음은 note_view_page.dart를 변경해 주겠습니다. 똑같이 index를 id로 바꿔줍니다. 그리고 build 함수에 getNote를 하는데 이 부분을 바꿔주겠습니다. 이 화면은 비동기식으로 받아오기 때문에 전에 바꾼 부분과는 다르게 FutureBuild를 사용하시면 됩니다. 빈 값으로 wrap해서 사용해도 되지만 저는 비슷한 형태인 StreamBuilder로 감싼뒤 변경해 주겠습니다. Scaffold를 StreamBuild로 감싸고 StreamBuild를 FutureBuild로 바꾼뒤 <> 안에 Object를 Note로 변경해 줍니다. 그리고 하위 속성에 stream을 future로 변경해 주시면 됩니다. 그리고 future에 noteManager()로 getNote를 받아오는 부분으로 바꿔줍니다. Future로 받아오기때문에 아직 데이터가 오지 않았을때 로딩바를 만들어줄 if문을 작성합니다.. 이때 snapshot를 이용합니다. snapshot의 connectionState가 ConnectionState의 waiting 상태일때 return으로 CircularProgressIndicator로 로딩바를 띄워줍니다. 이때 가운데에 보여주기 위해서 Center로 감싸주면 더 좋습니다. 또한 error을 위한 if문을 작성해 줍니다. snapshot의 hasError로 에러를 확인할 수 있습니다. return에 Scaffold - Center - Text로 오류가 발생했습니다 라는 문자를 띄워주면 됩니다. 이제 위 2가지가 아니면 데이터를 받아온 것이므로 final note에 snapshot의 requireData로 받아오고 기존 return을 그대로 두시면 됩니다.

@override
Widget build(BuildContext context) {
  return FutureBuilder<Note>(
    future: noteManager().getNote(widget.id),
    builder: (context, snapshot) {
      if(snapshot.connectionState == ConnectionState.waiting){
        return const Center(child: CircularProgressIndicator(),);
      }
      if(snapshot.hasError){
        return Scaffold(
          appBar: AppBar(),
          body: const Center(
            child: Text('오류가 발생했습니다.'),
          ),
        );
      }
      final note = snapshot.requireData;
      return Scaffold(
        appBar: AppBar(
          title: Text(note.title.isEmpty ? '(제목없음)' : note.title),
          actions: [
            IconButton(
              onPressed: () {
                _edit(widget.id);
              },
              icon: const Icon(Icons.edit),
              tooltip: '편집',
            ),
            IconButton(
              onPressed: () {
                _confirmDelete(widget.id);
              },
              icon: const Icon(Icons.delete),
              tooltip: '삭제',
            ),
          ],
        ),
        body: SizedBox.expand(
          child: Container(
            color: note.color,
            child: SingleChildScrollView(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
              child: Text(note.body),
            ),
          ),
        ),
      );
    }
  );
}

다음으로 note_list_page.dart를 수정하겠습니다. 우선 _buildCard를 수정하겠습니다. note정보에 id가 포함 되어 있으므로 인자인 int index를 삭자해 주시고 index를 사용하던 부분은 note.id로 변경해 주시면 됩니다. 다음으로 _buildCards를 수정해 주겠습니다. 그 전에는 note를 받아와서 만들어 줬는데 이제는 비동기 방식으로 만들기 때문에 notes를 불러오던 부분인 _buildCards를 삭제해 주겠습니다. 그리고 위의 GridView를 FutureBuild로 감사주고 <List<Note>>로 노트의 리스트를 받아오겠습니다. future 속성에는 noteManager에서 listNotes를 받아옵니다. builder에는 view page에서 했던 데이터를 waiting하는 중과 에러일때 코드를 그대로 가져오면 됩니다. 여기서 전과 같이  requireData도 notes에 받아와 줍니다. 전에 쓰던 return 부분은 삭제하고 만들어 주겠습니다. 이번에는 그냥 GridView가 아닌 GridView.builder를 사용하겠습니다. padding는 기존과 동일하고 horizontal 12, vertical 16으로 해줍니다. gridDelegate도 기존과 동일하게 해줍니다. itemCount는 notes의 length만큼으로 지정해줍니다. itemBuilder에는 context, index를 사용하여 _buildCard에 notes[index]로 노트 하나씩 넣어지게 만들어서 작성해주시면 됩니다.

@override
Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sticky Memo'),
        backgroundColor: Colors.blue,
      ),
      body: FutureBuilder<List<Note>>(
        future: noteManager().listNotes(),
        builder: (context, snapshot) {
          if(snapshot.connectionState == ConnectionState.waiting){
            return const Center(child: CircularProgressIndicator(),);
          }
          if(snapshot.hasError){
            return Scaffold(
              appBar: AppBar(),
              body: const Center(
                child: Text('오류가 발생했습니다.'),
              ),
            );
          }
          final notes = snapshot.requireData;
          return GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 1,
            ),
            padding: const EdgeInsets.symmetric(
              horizontal: 12,
              vertical: 16,
            ),
            itemCount: notes.length,
            itemBuilder: (BuildContext context, int index) => _buildCard(notes[index]),
          );
        }
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: '새 노트',
        onPressed: () {
          Navigator.pushNamed(context, NoteEditPage.routeName).then((_) {
            setState(() {});
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }

마지막으로 main.dart를 수정해 주겠습니다. edit 페이지로 넘겨주는 부분에서 index를 id로 변경해주시면 됩니다. view로 넘겨주는 부분 또한 고쳐주겠습니다. 이제 앱을 실행해서 작동 시켜 보겠습니다.

위와 같이 앱을 완전히 껐다 키더라도 정상 작동하는 것을 보실 수 있습니다. 오늘은 여기까지이고 다음에는 이제 앱은 완성이 되었으니 수익을 위한 광고를 달아보도록 하겠습니다.

오늘의 전체 코드

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, this.id}) : super(key: key);

  static const routeName = '/edit';

  final int? id;

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

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

  Color memoColor = Note.colorDefault;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    final noteId = widget.id;
    if (noteId != null) {
      noteManager().getNote(noteId).then((note) {
        titleController.text = note.title;
        bodyController.text = note.body;
        setState(() {
          memoColor = note.color;
        });
      });
    }
  }

  @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: const 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: const Text('배경색 선택'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                title: const Text('없음'),
                onTap: () => _applyColor(Note.colorDefault),
              ),
              ListTile(
                leading: const CircleAvatar(
                  backgroundColor: Note.colorRed,
                ),
                title: Text('빨간색'),
                onTap: () => _applyColor(Note.colorRed),
              ),
              ListTile(
                leading: const CircleAvatar(
                  backgroundColor: Note.colorLime,
                ),
                title: Text('연두색'),
                onTap: () => _applyColor(Note.colorLime),
              ),
              ListTile(
                leading: const CircleAvatar(
                  backgroundColor: Note.colorBlue,
                ),
                title: Text('파란색'),
                onTap: () => _applyColor(Note.colorBlue),
              ),
              ListTile(
                leading: const CircleAvatar(
                  backgroundColor: Note.colorOrange,
                ),
                title: Text('주황색'),
                onTap: () => _applyColor(Note.colorOrange),
              ),
              ListTile(
                leading: const 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) {
      final note = Note(
        bodyController.text,
        title: titleController.text,
        color: memoColor,
      );
      final noteIndex = widget.id;
      if (noteIndex != null) {
        noteManager().updateNote(noteIndex, note);
      } else {
        noteManager().addNote(note);
      }
      Navigator.pop(context);
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text('내용을 입력하세요.'),
        behavior: SnackBarBehavior.floating,
      ));
    }
  }
}

note_edit_page.dart

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

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

  static const routeName = '/';

  @override
  State<NoteListPage> createState() => _NoteListPageState();
}

class _NoteListPageState extends State<NoteListPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sticky Memo'),
        backgroundColor: Colors.blue,
      ),
      body: FutureBuilder<List<Note>>(
        future: noteManager().listNotes(),
        builder: (context, snapshot) {
          if(snapshot.connectionState == ConnectionState.waiting){
            return const Center(child: CircularProgressIndicator(),);
          }
          if(snapshot.hasError){
            return Scaffold(
              appBar: AppBar(),
              body: const Center(
                child: Text('오류가 발생했습니다.'),
              ),
            );
          }
          final notes = snapshot.requireData;
          return GridView.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 1,
            ),
            padding: const EdgeInsets.symmetric(
              horizontal: 12,
              vertical: 16,
            ),
            itemCount: notes.length,
            itemBuilder: (BuildContext context, int index) => _buildCard(notes[index]),
          );
        }
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: '새 노트',
        onPressed: () {
          Navigator.pushNamed(context, NoteEditPage.routeName).then((_) {
            setState(() {});
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }



  Widget _buildCard(Note note) {
    return InkWell(
      onTap: () {
        Navigator.pushNamed(context, NoteViewPage.routeName, arguments: note.id)
            .then((_) {
          setState(() {});
        });
      },
      child: Card(
        color: note.color,
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                note.title.isEmpty ? '(제목없음)' : note.title,
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(
                height: 16,
              ),
              Expanded(
                child: Text(
                  note.body,
                  overflow: TextOverflow.fade,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

note_list_page.dart

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

class NoteViewPage extends StatefulWidget {
  const NoteViewPage({Key? key, required this.id}) : super(key: key);

  static const routeName = '/view';

  final int id;


  @override
  State<NoteViewPage> createState() => _NoteViewPageState();
}

class _NoteViewPageState extends State<NoteViewPage> {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Note>(
      future: noteManager().getNote(widget.id),
      builder: (context, snapshot) {
        if(snapshot.connectionState == ConnectionState.waiting){
          return const Center(child: CircularProgressIndicator(),);
        }
        if(snapshot.hasError){
          return Scaffold(
            appBar: AppBar(),
            body: const Center(
              child: Text('오류가 발생했습니다.'),
            ),
          );
        }
        final note = snapshot.requireData;
        return Scaffold(
          appBar: AppBar(
            title: Text(note.title.isEmpty ? '(제목없음)' : note.title),
            actions: [
              IconButton(
                onPressed: () {
                  _edit(widget.id);
                },
                icon: const Icon(Icons.edit),
                tooltip: '편집',
              ),
              IconButton(
                onPressed: () {
                  _confirmDelete(widget.id);
                },
                icon: const Icon(Icons.delete),
                tooltip: '삭제',
              ),
            ],
          ),
          body: SizedBox.expand(
            child: Container(
              color: note.color,
              child: SingleChildScrollView(
                padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
                child: Text(note.body),
              ),
            ),
          ),
        );
      }
    );
  }

  void _edit(int index) {
    Navigator.pushNamed(
      context,
      NoteEditPage.routeName,
      arguments: index,
    ).then((_) {
      setState(() {});
    });
  }

  void _confirmDelete(int index) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text('노트 삭제'),
          content: Text('노트를 삭제 할까요?'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text('아니오'),
            ),
            TextButton(
              onPressed: () {
                noteManager().deleteNote(index);
                Navigator.popUntil(context, (route) => route.isFirst);
              },
              child: Text('예'),
            ),
          ],
        );
      },
    );
  }
}

note_view_page.dart

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

class NoteManager {
  static const _databaseName = 'notes.db';

  static const _databaseVersion = 1;

  Database? _database;

  Future<void> addNote(Note note) async {
    final db = await _getDatabase();
    await db.insert(Note.tableName, note.toRow());
  }

  Future<void> deleteNote(int id) async {
    final db = await _getDatabase();
    await db.delete(
      Note.tableName,
      where: '${Note.columnId} = ?',
      whereArgs: [id],
    );
  }

  Future<Note> getNote(int id) async {
    final db = await _getDatabase();
    final rows = await db.query(
      Note.tableName,
      where: '${Note.columnId} = ?',
      whereArgs: [id],
    );
    return Note.fromRow(rows.single);
  }

  Future<List<Note>> listNotes() async {
    final db = await _getDatabase();
    final rows = await db.query(Note.tableName);
    return rows.map((row) => Note.fromRow(row)).toList();
  }

  Future<void> updateNote(int id, Note note) async {
    final db = await _getDatabase();
    await db.update(
      Note.tableName,
      note.toRow(),
      where: '${Note.columnId} = ?',
      whereArgs: [id],
    );
  }

  Future<Database> _getDatabase() async {
    _database ??= await openDatabase(
      _databaseName,
      version: _databaseVersion,
      onCreate: (db, version) {
        const sql = '''
         CREATE TABLE ${Note.tableName} (
        ${Note.columnId} INTEGER PRIMARY KEY AUTOINCREMENT,
        ${Note.columnTitle} TEXT,
        ${Note.columnBody} TEXT NOT NULL,
        ${Note.columnColor} INTEGER NOT NULL
      )
        ''';
        return db.execute(sql);
      },
    );
    return _database!;
  }
}

note_manager.dart

import 'package:flutter/material.dart';

class Note {
  static const colorDefault = Colors.white;

  static const colorRed = Color(0xFFFFCDD2);

  static const colorOrange = Color(0xFFFFE0B2);

  static const colorYellow = Color(0xFFFFF9C4);

  static const colorLime = Color(0xFFF0F4C3);

  static const colorBlue = Color(0xFFBBDEFB);

  static const tableName = 'notes';

  static const columnId = 'id';

  static const columnTitle = 'title';

  static const columnBody = 'body';

  static const columnColor = 'color';

  final int? id;

  final String title;
  final String body;
  final Color color;

  Note(
    this.body, {
    this.id,
    this.title = '',
    this.color = colorDefault,
  });

  Note.fromRow(Map<String, dynamic> row)
      : this(
          row[columnBody],
          id: row[columnId],
          title: row[columnTitle],
          color: Color(row[columnColor]),
        );

  Map<String, dynamic> toRow() {
    return {
      columnTitle: title,
      columnBody: body,
      columnColor: color.value,
    };
  }
}

note.dart

import 'package:flutter/material.dart';
import 'package:sticky_memo/page/note_edit_page.dart';
import 'package:sticky_memo/page/note_list_page.dart';
import 'package:sticky_memo/page/note_view_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      initialRoute: NoteListPage.routeName,
      routes: {
        NoteListPage.routeName: (context) => const NoteListPage(),
        NoteEditPage.routeName: (context) {
          final args = ModalRoute.of(context)!.settings.arguments;
          final id = args != null  ? args  as int : null;
          return NoteEditPage(id: id,);
        },
        NoteViewPage.routeName: (context) {
          final id = ModalRoute.of(context)!.settings.arguments as int;
          return NoteViewPage(id: id);
        },
      },
    );
  }
}

main.dart

반응형