Flutter - Firestore 라이브러리 사용하기 (3)
지금까지 Flutter에서 Firestore dependency를 추가하고 android에서 사용할 수 있도록 환경을 구성했습니다.
이번에는 실제 flutter 코드에서 firestore를 어떻게 사용하는지 알아보겠습니다.
1편에서 본 코드와 크게 다르지 않습니다.
dummySnapshot을 이용하지 않고 StreamBuilder
를 이용하여 Firebase 쿼리 결과값을 가져오게됩니다.StreamBuilder
위젯은 데이터베이스에 대한 업데이트를 수신하고 데이터가 변경될 때마다 목록을 새로 고칩니다.Firebase Console
에서 document의 값 한개를 변경하면 컬렉션에 포함된 모든 document의 값이 호출되어 실시간으로 변경된 값을 감지하고 UI변경 작업을 진행할 수 있습니다.
Widget _buildBody(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("baby").snapshots(),
builder: (context, snapshot) {
if(!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
//return _buildList(context, dummySnapshot); <- old
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
print("record ${record.name} ${record.votes}");
...
}
자 여기까지 App에서 firestore의 데이터를 불러오는 과정을 마쳤습니다.
이제 항목을 클릭했을 때 투표값이 계속 반영되는 작업이 필요합니다.
onTap: () => record.reference.updateData({'votes': record.votes + 1})
사용자가 하나의 타일을 클릭하면 Firestore 데이터를 업데이트하라는 메서드를 실행합니다.
Firestore에서 값이 변경되면 다시 StreamBuilder
를 통해 변경된 내용을 받아 UI를 업데이트 합니다.
Hot Reload
를 통해 변경된 내용을 확인해봅니다.
타일을 누르면 투표스가 잘 증가됨을 확인할 수 있습니다.
지금은 단말기 한대로 테스트를 해서 큰 문제가 없습니다.
만약 여러 디바이스에서 동시에 투표를 한다면 어떻게 될까요? 투표 결과는 한개만 반영이 될껍니다.
그러면 이 문제를 어떻게 해결해야 할까요?
바로 transaction
을 사용해서 경쟁 상태(race condition)을 회피하도록 해야 합니다.
onTap: () => Firestore.instance.runTransaction((transaction) async {
final freshSnapshot = await transaction.get(record.reference);
final fresh = Record.fromSnapshot(freshSnapshot);
await transaction
.update(record.reference, {'votes': fresh.votes + 1});
}),
이렇게하면 투표 적용 시간은 조금 늘어나지만 경쟁 상태에 빠지지 않아 원하는 결과가 나옵니다.
최종적인 소스 코드입니다.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
// 메인 진입점
void main() => runApp(MyApp());
// 껍데기는 상태가 변하지 않는 위젯
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Baby Names',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Baby Name Votes')),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
// StreamBuilder 위젯은 데이터베이스에 대한 업데이트를 수신하고 데이터가 변경될 때마다 목록을 새로 고칩니다.
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("baby").snapshots(),
builder: (context, snapshot) {
if(!snapshot.hasData) return LinearProgressIndicator();
return _buildList(context, snapshot.data.documents);
},
);
//return _buildList(context, dummySnapshot); <- old
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
padding: const EdgeInsets.only(top: 20.0),
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
print("record ${record.name} ${record.votes}");
return Padding(
key: ValueKey(record.name),
padding: const 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(record.name),
trailing: Text(record.votes.toString()),
onTap: () => Firestore.instance.runTransaction((transaction) async {
final freshSnapshot = await transaction.get(record.reference);
final fresh = Record.fromSnapshot(freshSnapshot);
await transaction
.update(record.reference, {'votes': fresh.votes + 1});
}),
),
),
);
}
}
class Record {
final String name;
final int votes;
final DocumentReference reference;
Record.fromMap(Map<String, dynamic> map, {this.reference})
: assert(map['name'] != null),
assert(map['votes'] != null),
name = map['name'],
votes = map['votes'];
Record.fromSnapshot(DocumentSnapshot snapshot)
: this.fromMap(snapshot.data, reference: snapshot.reference);
@override
String toString() => "Record<$name:$votes>";
}