개발하는 두더지

Flutter - Firestore 라이브러리 사용하기 (3) 본문

Flutter

Flutter - Firestore 라이브러리 사용하기 (3)

덜지 2019. 4. 26. 15:20

지금까지 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>";
}
Comments