꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] Firestore 사용법 #2 | CRUD 동작의 이해 본문

Development/Flutter

Flutter 강좌 - [Firebase] Firestore 사용법 #2 | CRUD 동작의 이해

독행소년 2019. 11. 14. 15:55

Flutter Code Examples 강좌를 추천합니다.

  • 제 블로그에서 Flutter Code Examples 프로젝트를 시작합니다.
  • Flutter의 다양한 예제를 소스코드와 실행화면으로 제공합니다.
  • 또한 모든 예제는 Flutter Code Examples 앱을 통해 테스트 가능합니다.

Flutter Code Examples 강좌로 메뉴로 이동

Flutter Code Examples 강좌 목록 페이지로 이동

Flutter Code Examples 앱 설치 | Google Play Store로 이동

 

Flutter Code Examples - Google Play 앱

Are you a beginner at Flutter? Check out the various features of Flutter through the demo. Source code for all demos is also provided.

play.google.com


 

Flutter 강좌 시즌2 목록 : https://here4you.tistory.com/149

 

이번 강좌에서는 Flutter 앱을 통해 Firestore에 CRUD를 하는 방법에 대해서 알아본다.

CRUD는 데이터를 다루는 일반적인 방식으로 Create/Read/Update/Delete를 의미한다.

지난 강좌에서 구현한 코드를 다음과 같이 수정한다.

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

FirestoreFirstDemoState pageState;

class FirestoreFirstDemo extends StatefulWidget {
  @override
  FirestoreFirstDemoState createState() {
    pageState = FirestoreFirstDemoState();
    return pageState;
  }
}

class FirestoreFirstDemoState extends State<FirestoreFirstDemo> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  // 컬렉션명
  final String colName = "FirstDemo";

  // 필드명
  final String fnName = "name";
  final String fnDescription = "description";
  final String fnDatetime = "datetime";

  TextEditingController _newNameCon = TextEditingController();
  TextEditingController _newDescCon = TextEditingController();
  TextEditingController _undNameCon = TextEditingController();
  TextEditingController _undDescCon = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text("FirestoreFirstDemo")),
      body: ListView(
        children: <Widget>[
          Container(
            height: 500,
            child: StreamBuilder<QuerySnapshot>(
              stream: Firestore.instance
                  .collection(colName)
                  .orderBy(fnDatetime, descending: true)
                  .snapshots(),
              builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> snapshot) {
                if (snapshot.hasError) return Text("Error: ${snapshot.error}");
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return Text("Loading...");
                  default:
                    return ListView(
                      children: snapshot.data.documents
                          .map((DocumentSnapshot document) {
                        Timestamp ts = document[fnDatetime];
                        String dt = timestampToStrDateTime(ts);
                        return Card(
                          elevation: 2,
                          child: InkWell(
                            // Read Document
                            onTap: () {
                              showDocument(document.documentID);
                            },
                            // Update or Delete Document
                            onLongPress: () {
                              showUpdateOrDeleteDocDialog(document);
                            },
                            child: Container(
                              padding: const EdgeInsets.all(8),
                              child: Column(
                                children: <Widget>[                                  Row(                                    mainAxisAlignment:                                        MainAxisAlignment.spaceBetween,                                    children: <Widget>[                                      Text(                                        document[fnName],
                                        style: TextStyle(
                                          color: Colors.blueGrey,
                                          fontSize: 17,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                      Text(
                                        dt.toString(),
                                        style:
                                            TextStyle(color: Colors.grey[600]),
                                      ),
                                    ],
                                  ),
                                  Container(
                                    alignment: Alignment.centerLeft,
                                    child: Text(
                                      document[fnDescription],
                                      style: TextStyle(color: Colors.black54),
                                    ),
                                  )
                                ],
                              ),
                            ),
                          ),
                        );
                      }).toList(),
                    );
                }
              },
            ),
          )
        ],
      ),
      // Create Document
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add), onPressed: showCreateDocDialog),
    );
  }

  /// Firestore CRUD Logic

  // 문서 생성 (Create)
  void createDoc(String name, String description) {
    Firestore.instance.collection(colName).add({
      fnName: name,
      fnDescription: description,
      fnDatetime: Timestamp.now(),
    });
  }

  // 문서 조회 (Read)
  void showDocument(String documentID) {
    Firestore.instance
        .collection(colName)
        .document(documentID)
        .get()
        .then((doc) {
      showReadDocSnackBar(doc);
    });
  }

  // 문서 갱신 (Update)
  void updateDoc(String docID, String name, String description) {
    Firestore.instance.collection(colName).document(docID).updateData({
      fnName: name,
      fnDescription: description,
    });
  }

  // 문서 삭제 (Delete)
  void deleteDoc(String docID) {
    Firestore.instance.collection(colName).document(docID).delete();
  }

  void showCreateDocDialog() {
    showDialog(
      barrierDismissible: false,
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("Create New Document"),
          content: Container(
            height: 200,
            child: Column(
              children: <Widget>[
                TextField(
                  autofocus: true,
                  decoration: InputDecoration(labelText: "Name"),
                  controller: _newNameCon,
                ),
                TextField(
                  decoration: InputDecoration(labelText: "Description"),
                  controller: _newDescCon,
                )
              ],
            ),
          ),
          actions: <Widget>[
            FlatButton(
              child: Text("Cancel"),
              onPressed: () {
                _newNameCon.clear();
                _newDescCon.clear();
                Navigator.pop(context);
              },
            ),
            FlatButton(
              child: Text("Create"),
              onPressed: () {
                if (_newDescCon.text.isNotEmpty &&
                    _newNameCon.text.isNotEmpty) {
                  createDoc(_newNameCon.text, _newDescCon.text);
                }
                _newNameCon.clear();
                _newDescCon.clear();
                Navigator.pop(context);
              },
            )
          ],
        );
      },
    );
  }

  void showReadDocSnackBar(DocumentSnapshot doc) {
    _scaffoldKey.currentState
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          backgroundColor: Colors.deepOrangeAccent,
          duration: Duration(seconds: 5),
          content: Text(
              "$fnName: ${doc[fnName]}\n$fnDescription: ${doc[fnDescription]}"
              "\n$fnDatetime: ${timestampToStrDateTime(doc[fnDatetime])}"),
          action: SnackBarAction(
            label: "Done",
            textColor: Colors.white,
            onPressed: () {},
          ),
        ),
      );
  }

  void showUpdateOrDeleteDocDialog(DocumentSnapshot doc) {
    _undNameCon.text = doc[fnName];
    _undDescCon.text = doc[fnDescription];
    showDialog(
      barrierDismissible: false,
      context: context,
      builder: (context) {
        return AlertDialog(
          title: Text("Update/Delete Document"),
          content: Container(
            height: 200,
            child: Column(
              children: <Widget>[
                TextField(
                  decoration: InputDecoration(labelText: "Name"),
                  controller: _undNameCon,
                ),
                TextField(
                  decoration: InputDecoration(labelText: "Description"),
                  controller: _undDescCon,
                )
              ],
            ),
          ),
          actions: <Widget>[
            FlatButton(
              child: Text("Cancel"),
              onPressed: () {
                _undNameCon.clear();
                _undDescCon.clear();
                Navigator.pop(context);
              },
            ),
            FlatButton(
              child: Text("Update"),
              onPressed: () {
                if (_undNameCon.text.isNotEmpty &&
                    _undDescCon.text.isNotEmpty) {
                  updateDoc(doc.documentID, _undNameCon.text, _undDescCon.text);
                }
                Navigator.pop(context);
              },
            ),
            FlatButton(
              child: Text("Delete"),
              onPressed: () {
                deleteDoc(doc.documentID);
                Navigator.pop(context);
              },
            )
          ],
        );
      },
    );
  }

  String timestampToStrDateTime(Timestamp ts) {
    return DateTime.fromMicrosecondsSinceEpoch(ts.microsecondsSinceEpoch)
        .toString();
  }
}

 

다큐먼트를 CRUD하는 메서드들을 살펴보자. 별다른 설명이 필요없을 정도로 간단한다.

  • 다큐먼트 생성 (Create)
  void createDoc(String name, String description) {
    Firestore.instance.collection(colName).add({
      fnName: name,
      fnDescription: description,
      fnDatetime: Timestamp.now(),
    });
  }

 

  • 다큐먼트 조회 (Read)
  void showDocument(String documentID) {
    Firestore.instance
        .collection(colName)
        .document(documentID)
        .get()
        .then((doc) {
      showReadDocSnackBar(doc);
    });
  }

 

  • 다큐먼트 갱신 (Update)
  void updateDoc(String docID, String name, String description) {
    Firestore.instance.collection(colName).document(docID).updateData({
      fnName: name,
      fnDescription: description,
    });
  }

 

  • 다큐먼트 삭제 (Delete)
  void deleteDoc(String docID) {
    Firestore.instance.collection(colName).document(docID).delete();
  }

 

앱을 통해서 CRUD를 해보자. FloatingActionButton을 클릭하여 새로운 다큐먼트를 추가해보자.

 

Firebase 콘솔에서도 추가된 다큐먼트를 확인해보자.

코드 몇줄로 새로운 다큐먼트가 앱의 ListView에도 추가되고 Firestore에도 저장된 것을 확인할 수 있다.

 

다큐먼트를 몇개 더 추가해보자.

name 필드에는 here4you를 description 필드에는 test라는 텍스트값에 숫자를 증가시켜가면서 다큐먼트를 추가했다.

다큐먼트 생성과 함께 ListView와 Firestore에 실시간으로 다큐먼트들이 추가되는 것을 확인할 수 있다. 그런데 다큐먼트의 순서가 이상하다. 이는 NoSQL의 특징으로 데이터를 입력한 순서를 보장하지 않는다. 그러므로 데이터를 바인딩할 때 정렬처리를 해줘야한다.

방법은 다음과 같이 build 메서드안의 StreamBuilder 구현에서 정렬방식을 추가해 준다. datetime값에 따라 정렬처리한 것이다.

            child: StreamBuilder<QuerySnapshot>(
              stream: Firestore.instance
                  .collection(colName)
                  .orderBy(fnDatetime, descending: true) // 정렬 처리
                  .snapshots(),
              builder: ...

 

앱을 다시 실행하면 다음과 같이 시간값에 따라 다큐먼트들이 출력되는 것을 확인할 수 있다.

 

다큐먼트의 조회를 시도해보자.

ListView의 아이템을 선택하면 해당 아이템의 documentID를 통해 Firestore의 컬렉션에서 해당 도큐먼트를 다시 조회하여 스낵바로 출력한다.

 

다큐먼트의 업테이트를 시도해보자

ListView의 아이템을 길게 누르면 다이어로그가 나타난다.

 

같은 방식으로 다큐먼트의 삭제를 시도해보자.

데이터가 정상적으로 삭제되는 것을 확인할 수 있다.

 

이번 강좌에서는 Flutter앱을 이용해서 Firestore에 CRUD를 하는 방법에 대해서 알아봤다.

기존의 서버 기반의 앱 서비스를 개발할 때에는 서버를 구축하고 데이터베이스를 설치한 후 CRUD를 수행하는 로직을 클라이언트(앱)측과 서버측으로 나누어서 개발해야했다. 데이터베이스의 레코드들이 필요하면 클라이언트에서 서버로 요청을 하고, 서버는 요청에 맞는 데이터를 CRUD한 다음에 그 결과를 반환했고 클라이언트는 그 결과를 보고 앱의 화면을 다시 구성해야 했다.

Firestore를 이용하면 이 모든 작업을 로컬 앱에서 CRUD 함수를 호출하는것 만으로 구현할 수 있다. 서버 데이터베이스는 앱과 바인딩되어 있어 실시간으로 변화를 감지하고 자동으로 반영된다. 더욱이 오프라인 상태에서도 캐쉬된 로컬 데이터베이스를 이용하므로 앱을 지속적으로 서비스할수있고 온라인 상태가 되면 로컬의 변화가 서버에 자동으로 반영된다. 굉장히 매력적이고 강력한 기능임을 확인할 수 있었다.

 

Comments