flutter에서 firestore 사용하기(3)

Namkyu Park
Flutter Seoul
Published in
18 min readNov 19, 2019

firestore, StreamBuilder 를 사용한 예제

들어가며

이전 포스트에서는 firestore에서 데이터를 관리하는 방법에 대해 알아보았습니다. 이번 포스트에서는 지금까지 살펴보았던 내용들과 더불어, firestore의 query 그리고 StreamBuilder를 활용하여 간단한 예제를 같이 만들어보겠습니다.

목차

결과화면

글을 다 읽을 시점엔, 여러분들도 이런 애플리케이션을 만드실 수 있습니다.

firestore에는 다음과 같이 데이터를 저장하였습니다.

눈치채신 분들도 계시겠지만, 앱을 처음 실행할 때 나오는 4개의 항목들은 모두 firebase의 document 들입니다. 그리고 오른편 상단에 메뉴 버튼을 클릭하면 세개의 항목이 나오는데요, 그중 두번째 ‘already purchased’ 버튼을 누르면 4개의 항목중 ‘purchased?’가 true인 document들만 나오게 됩니다. 또한 세번째 ‘price<20000’ 버튼을 누르게 되면 마찬가지로 4개의 항목 중 ‘price’가 20000 미만인 document들만 나오게 됩니다. 이 기능들은 flutter의 StreamBuilder 클래스와 Firestore의 query를 이용하면 쉽게 구현 가능합니다. 마지막으로 오른쪽 하단에 있는 ADD버튼을 누르고 형식대로 작성하여 Finish 버튼을 누르게 되면 새로운 document가 생기면서 화면에 새로운 항목이 추가 됩니다. 자, 앱이 어떻게 운영되는지 이해하셨나요? 지금부터 차근차근 같이 해봅시다.

환경 세팅하기

이전 포스트에서 사용하였던 Firestore의 프로젝트를 그대로 사용하겠습니다. (새로 만들어도 무방합니다.) 데이터베이스를 다음과 같이 수정해주세요

구현하기

안드로이드 스튜디오로 돌아와서, 새로운 flutter 프로젝트를 만들어 줍니다. 생성 후 lib 폴더에 main.dart 이외에 3개의 dart file을 추가해줍니다.

main.dart에 있는 모든 내용을 지우신 후 다음과 같이 수정합니다.

import 'package:flutter/material.dart';
import 'package:flutter_firestore/result_screen.dart';
import 'package:flutter_firestore/add_screen.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes:{
'/':(context) => ResultScreen("all"),
'a':(context) => ResultScreen("purchase"),
'b':(context) => ResultScreen("price"),
'c':(context) => AddScreen(),
},
);
}
}

다음과 같이 입력하면 에러가 나타날 것입니다. 아직 완성이 안된것이니 그대로 두시면 됩니다. 예제의 프로젝트 이름이 flutter_firestore이기 때문에 2,3 번째 라인에 다음과 같은 경로로 파일을 import합니다. 그 자리를 자신의 프로젝트 이름으로 대체하시면 됩니다. main 함수의 MaterialApp 안을 보시면 route가 눈에 띌 것입니다. 이는 후에 Navigator클래스를 활용하여 화면을 전환하는데 이용됩니다.

document_view.dart파일을 여신 후 다음과 같이 코드를 수정해주세요

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

class DocumentView extends StatelessWidget {
final DocumentSnapshot documentData;
DocumentView(this.documentData);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5.0),
),
child: ListTile(
title: Text(documentData.data["title"]),
subtitle: Row(
children: <Widget>[
Text(documentData.data["price"].toString()),
SizedBox(width: 10, height: 10),
Text(documentData.data["purchase?"].toString()),
],
),
),
),
);
}
}

이 DocumentView 클래스는 앞서 동영상에서 보았던 document항목 하나를 만들어 주는 클래스입니다. 매개변수로 DocumentSnapshot 클래스를 정보로 받아 이 정보들을 화면에 표시해줍니다. DocumentSnapshot 클래스를 통해 데이터를 읽는 방법은 이전 포스트에서 다루었으므로 생략하겠습니다.

result_screen.dart 파일을 여시고 다음과 같이 코드를 수정해주세요.

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

class ResultScreen extends StatefulWidget {
final String index;
ResultScreen(this.index);
@override
_ResultScreenState createState() => _ResultScreenState();
}

class _ResultScreenState extends State<ResultScreen> {
Firestore firestore = Firestore.instance;
Stream<QuerySnapshot> currentStream;
List<String> menuIndex = ["see all","already purchased", "price<20000"];
@override void initState() {
super.initState();
switch(widget.index){
case "all" :{
currentStream = firestore.collection("books").snapshots();
break;
}
case "purchase" :{
currentStream = firestore.collection("books").where("purchase?", isEqualTo: true).snapshots();
break;
}
case "price" :{
currentStream = firestore.collection("books").where("price", isLessThan: 20000).snapshots();
break;
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("BOOK SHELF"),
actions: <Widget>[
PopupMenuButton<String>(
onSelected: (String choice){
if(choice == "see all") Navigator.pushNamed(context, '/');
else if(choice == "already purchased")
Navigator.pushNamed(context, 'a');
else Navigator.pushNamed(context, 'b');
},
itemBuilder: (BuildContext context) {
return menuIndex.map((choice) => PopupMenuItem(value: choice, child: Text(choice))).toList();
},
),
],
),
body: StreamBuilder(
stream: currentStream,
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
List<DocumentSnapshot> documents = snapshot.data.documents;
return ListView(
padding:EdgeInsets.only(top: 20.0),
children: documents.map((eachDocument) => DocumentView(eachDocument)).toList(),
);
},
),
floatingActionButton: FloatingActionButton.extended( label : Text("ADD"),
icon : Icon(Icons.add),
onPressed: (){
Navigator.pushNamed(context, 'c');
},
),
);
}
}

차근차근 코드를 설명하겠습니다. 이 ResultScreen 클래스는 앞서 동영상에서 보았던 항목들을 보여주는 화면을 출력해줍니다. main.dart의 중간즈음에 이런 코드가 있습니다.

‘/’:(context) => ResultScreen(“all”),

처음 ResultScreen이 출력될 때, 매개변수로 “all”이라는 문자열을 받습니다. 이는 생성자를 통해 index에 저장됩니다. 오버라이드된 initState 함수를 보겠습니다. 아시다시피 initState함수는 위젯의 생성자 코드 실행 이후 처음으로 실행되는 메서드입니다.

@override  
void initState() {
super.initState();
switch(widget.index){
case "all" :{
currentStream = firestore.collection("books").snapshots();
break;
}
case "purchase" :{
currentStream = firestore.collection("books").where("purchase?", isEqualTo: true).snapshots();
break;
}
case "price" :{
currentStream = firestore.collection("books").where("price", isLessThan: 20000).snapshots();
break;
}
}
}

동영상에서 보았듯, 처음 화면에서 우측 상단의 아이콘을 누르면 세개의 메뉴가 나옵니다. 메뉴가 선택되었을 때 선택된 메뉴에 따라 다르게 화면이 전환됩니다. ‘see all’의 경우에는 ResultScreen의 매개변수가 “all”, “already purchased”의 경우에는 매개변수로 “purchase”, “price<20000”의 경우에는 매개변수로 “price”가 전달됩니다. 이 전달된 매개변수에 따라서 currentStream에 각기 다른 값을 저장하게 됩니다.

if(choice == “see all”) Navigator.pushNamed(context, ‘/’);
else if(choice == “already purchased”) Navigator.pushNamed(context, ‘a’);
else Navigator.pushNamed(context, ‘b’);

query 이용하기

잠깐 Firestore에 대한 이야기로 넘어와서, 만약 document들 중에서 특정 조건의 데이터를 포함한 document들을 가져오고 싶다면, CollectionReference 클래스의 where 메서드를 사용해야 합니다. 이 메서드의 첫번째 메서드는 조건을 만들고 싶은 field의 key값이고, value 값이 조건 데이터와 같은지( isEqualTo), 조건 데이터 보다 큰지( isGreaterThan), 조건 데이터 보다 작은지( isLessThan)등을 지정할 수 있습니다. 자세한건 링크로 표시된 문서를 참고해주세요. where메서드는 Query클래스를 반환하고 이 클래스의 snapshots메서드를 통해 우리가 원하는 document들이 저장되어 있는 Stream 데이터를 얻습니다.

구현하기(이어서)

다음은 ResultScreen 중간에 구현되어있는 StreamBuilder 클래스입니다.

StreamBuilder(
stream: currentStream,
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
List<DocumentSnapshot> documents = snapshot.data.documents;
return ListView(
padding:EdgeInsets.only(top: 20.0),
children: documents.map((eachDocument) => DocumentView(eachDocument)).toList(),
);
},
),

StreamBuilder 클래스에 대한 설명은 링크에 표시된 영상을 보면 쉽게 이해가 갈 것입니다. 아래의 코드 하나만 짚고 넘어가겠습니다.

documents.map((eachDocument) => DocumentView(eachDocument)).toList()

자료형이 list인 documents의 요소를 하나씩 가져와서 DocumentView를 만들고 생성된 DocumentView들을 하나의 list로 저장하는 코드입니다. 일종의 반복문이라고 생각하면 편할 것 같습니다.

이제 내용이 거의 끝나 갑니다. 마지막으로 오른쪽하단에 있는 ADD버튼을 누를 때 생성되는 화면을 구현해보겠습니다. add_screen.dart를 여시고 다음과 같이 수정해주세요

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

class AddScreen extends StatefulWidget {
@override
_AddScreenState createState() => _AddScreenState();
}
class _AddScreenState extends State<AddScreen> {
String title;
String price;
String purchase;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ADD BOOK")
),
body: Column(
children: <Widget>[
TextField(
decoration: kTextFieldDecoration.copyWith(hintText:"title"),
onChanged: (value){title = value;},
),
TextField(
decoration: kTextFieldDecoration.copyWith(hintText:"price(only number)"),
keyboardType: TextInputType.number,
onChanged: (value){price = value;},
),
TextField(
decoration: kTextFieldDecoration.copyWith(hintText:"purchased?(true / false)"),
onChanged: (value){purchase = value;},
),
FlatButton(
color: Colors.blue,
textColor: Colors.white,
onPressed: () {
bool check = (purchase == 'true');
print(price);
print(title);
print(check);
Firestore.instance.collection("books").document(title) .setData({"price":int.parse(price),"title":title,
"purchase?":check});
Navigator.pop(context);
},
child: Text("Finish"),
),
],
),
);
}
}

const kTextFieldDecoration = InputDecoration(
filled: true,
fillColor: Colors.white,
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
borderSide: BorderSide.none,
),
);

중간즈음에 다음과 같은 코드가 나옵니다.

Firestore.instance.collection("books").document(title)
.setData({"price":int.parse(price),"title":title, "purchase?":check});

이 코드를 보시고 무슨 의미인지 이해하셨다면, 여러분들은 어디가서 Firestore를 사용할 줄 안다고 하셔도 무방합니다!

축하드립니다! 이번 포스트를 통해서 여러분들은 Firestore를 활용하여 애플리케이션을 만드는 방법에 대해서 알게 되었습니다.

지금까지는 필자가 만든 예제코드를 따라 치셨을 것입니다. 처음 Firestore를 사용하시는 분들이라면, 예제 코드를 바탕으로 내용을 하나씩 덧붙여나가는 것을 권장합니다.(document delete 버튼 구현 등) 이후에는 여러분만의 프로젝트를 만드시면서 Firestore를 사용해보세요. Firebase에는 Firestore 뿐 아니라 다양하고 강력한 제품들이 있습니다. 공식 홈페이지에서 확인해보세요!

처음 firebase와 firestore를 사용하는 분들을 대상으로 쓴 글이니 만큼,궁금하신 내용이 많을 거라 생각합니다. 제 프로필에 있는 이메일로 어떤 이야기든 하시면 친절히 답해드리겠습니다. 긴 글 읽어주신 분들께 진심으로 감사의 말씀을 전하면서 마무리하겠습니다.

--

--