꿈꾸는 시스템 디자이너

Flutter 강좌 - [Firebase] Firestore 사용법 #1 본문

Development/Flutter

Flutter 강좌 - [Firebase] Firestore 사용법 #1

독행소년 2019. 11. 14. 12:40

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

 

이번 시간에는 Firebase의 Firestore에 대해서 알아본다.

Firestore란 Firebase에서 제공하는 데이터베이스로 실시간 업데이트와 강력한 쿼리, 자동 확장이 가능하다고 소개하고 있다.

 

1. Firebase 설정

Firebase 콘솔에 접속하여 Database 메뉴에서 데이터베이스 만들기를 선택한다.

보안 규칙은 테스트 모드로 시작을 선택한다. 

 

위치 설정은 기본 위치인 nam5(us-central)을 그대로 선택한 후 완료 버튼을 선택한다.

 

생성이 완료된 화면이다.

 

컬렉션 시작을 선택하여 새로운 컬렉션을 생성해보자. 컬렉션 ID로 FirestDemo라고 입력한다.

 

컬렉션의 첫 번째 문서로 다음의 내용을 입력한다.

문서 ID는 자동 ID가 부여되도록 하였고, String 유형의 name, description 그리고 timestamp 유형의 datetime 필드를 추가한 후 값을 부여했다.

 

저장 버튼을 누르면 다음과 같이 새로운 컬렉션과 문서가 추가된 것을 확인할 수 있다.

기존의 데이터베이스 개념으로 살펴보면 Database안에 테이블에 해당하는 컬렉션이 존재하고, 컬렉션 안에 레코드에 해당하는 문서(document)가 쌓이는 개념이다.

 

2. 앱 구현

이 강좌에서도 이전 강좌들에서 사용한 Flutter Firebase 앱을 이용한다.

Flutter에서 Firestore를 이용하기 위해서는 cloud_firestore 플러그인을 이용한다. 플러그인의 정보는 다음의 사이트를 참고한다.

https://pub.dev/packages/cloud_firestore

 

pubspec.yaml 파일에 cloud_firestore 플러그인을 추가한다.

dependencies:
  cloud_firestore: ^0.12.10

 

소스코드는 다음과 같다.

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> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("FirestoreFirstDemo")),
      body: Column(
        children: <Widget>[
          Container(
            height: 500,
            child: StreamBuilder<QuerySnapshot>(
              stream: Firestore.instance.collection("FirstDemo").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 tt = document["datetime"];
                        DateTime dt = DateTime.fromMicrosecondsSinceEpoch(
                            tt.microsecondsSinceEpoch);

                        return Card(
                          elevation: 2,
                          child: Container(
                            padding: const EdgeInsets.all(8),
                            child: Column(
                              children: <Widget>[
                                Row(
                                  mainAxisAlignment:
                                      MainAxisAlignment.spaceBetween,
                                  children: <Widget>[
                                    Text(
                                      document["name"],
                                      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["description"],
                                    style: TextStyle(color: Colors.black54),
                                  ),
                                )
                              ],
                            ),
                          ),
                        );
                      }).toList(),
                    );
                }
              },
            ),
          )
        ],
      ),
    );
  }
}

소스를 잠깐 살펴보면 StreamBuilder를 이용해서 FirstDemo 컬렉션을 바인딩하고 다큐먼트들을 ListView로 나타내는 코드이다.

소스 코드를 실행시켜 보자. 이때 다음과 같은 에러가 발생할 수 있다.

Launching lib\main.dart on AOSP on IA Emulator in debug mode...
Initializing gradle...
Resolving dependencies...
Running Gradle task 'assembleDebug'...
D8: Cannot fit requested classes in a single dex file (# methods: 80401 > 65536)
com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 
The number of method references in a .dex file cannot exceed 64K.
Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html
	at com.android.builder.dexing.D8DexArchiveMerger.getExceptionToRethrow(D8DexArchiveMerger.java:131)
	at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:118)
	at com.android.build.gradle.internal.transforms.DexMergerTransformCallable.call(DexMergerTransformCallable.java:102)
	at com.android.build.gradle.internal.tasks.DexMergingTaskRunnable.run(DexMergingTask.kt:444)
	at com.android.build.gradle.internal.tasks.Workers$ActionFacade.run(Workers.kt:335)
	at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:39)
	at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.execute(NoIsolationWorkerFactory.java:61)
	at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
	at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:416)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:406)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158)
	at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:102)
	at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
	at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
	at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:55)
	at org.gradle.workers.internal.DefaultWorkerExecutor$1.call(DefaultWorkerExecutor.java:105)
	at org.gradle.workers.internal.DefaultWorkerExecutor$1.call(DefaultWorkerExecutor.java:99)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:215)
	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
	at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:131)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
	at java.lang.Thread.run(Thread.java:748)
Caused by: com.android.tools.r8.CompilationFailedException: Compilation failed to complete
	at com.android.tools.r8.utils.t.a(:55)
	at com.android.tools.r8.D8.run(:11)
	at com.android.builder.dexing.D8DexArchiveMerger.mergeDexArchives(D8DexArchiveMerger.java:116)
	... 30 more
Caused by: com.android.tools.r8.utils.AbortException: Error: null, Cannot fit requested classes in a single dex file (# methods: 80401 > 65536)
	at com.android.tools.r8.utils.Reporter.a(:21)
	at com.android.tools.r8.utils.Reporter.a(:7)
	at com.android.tools.r8.dex.VirtualFile.a(:33)
	at com.android.tools.r8.dex.VirtualFile$h.a(:5)
	at com.android.tools.r8.dex.ApplicationWriter.a(:13)
	at com.android.tools.r8.dex.ApplicationWriter.write(:35)
	at com.android.tools.r8.D8.d(:44)
	at com.android.tools.r8.D8.b(:1)
	at com.android.tools.r8.utils.t.a(:23)
	... 32 more

Finished with error: Gradle task assembleDebug failed with exit code 1

참조되는 메서드의 수가 65,536개가 넘으면 안드로이드 빌드 아키텍처의 한계를 넘어서 발생하는 에러인데 single dex를 이용하는 안드로이드 SDK 20 이하에서 발생하며 multi dex를 이용하는 SDK 21 이상부터는 발생하지 않는다고 한다.

android/add 폴더의 build.gradel 파일을 열어 minSdkVerstion 값을 기본 16에서 21로 변경한 후 다시 빌드한다.

 

다음과 같이 FirestDemo 컬렉션의 다큐먼트가 정상적으로 출력되면 성공이다.

 

 

 

Comments