[TIL] Flutter + Firebase CRUD

jeongjwon·2023년 11월 29일
0

이론

목록 보기
13/19

dependencies

Firebase 세팅을 위해 cloud_firestorefirebase_core 를 설치해야 한다.

$flutter pub add cloud_firestore
$flutter pub add firebase_core

터미널에 입력한다면 패키지가 설치되고 pubspec.yaml 파일의 dependencies 부분에 설치된 것을 볼 수 있다.

dependencies:
	cloud_firestore: ^4.13.1
  	firebase_core: ^2.22.0




CREATE

Firebase DB 에 데이터를 저장하기 위해서는 Firebase 홈페이지에서 했던 것처럼 collection ID와 document ID 를 생성 후 필요한 필드 데이터를 추가해야한다.
document ID 는 자동 생성과 수동 생성으로 나눌 수 있고 이에 따라 코드가 달라질 수 있다.


Document ID 자동 생성 vs 수동 생성

자동 생성

FirebaseFirestore firestore = FirebaseFirestore.instace; //인스턴스 생성
firestore.collection("collectionId").add({ ... }, { ... }, );
firestore.collection("collectionId").doc().set({ ... }, { ... }, );

collection ID 값은 필수로 collection("collectionId").add() 를 통해 데이터를 추가할 수 있거나, collection("collectionId").doc().set() 을 통해 doc 부분에 아무것도 추가하지 않고 자동으로 document ID 를 생성하도록 할 수 있다.


수동 생성

firestore.collection("collectionId").doc("documentId").set({ ... }, { ... }, );

수동으로 생성하려면 doc 부분에 document ID 값을 추가시켜주면 된다.

결론적으로, collection ID값과 document ID 값은 모두 String 형태이어야 하며, document Id 는 유일성을 가지고 있어야 한다. 만약 동일하다면 나중에 추가된 데이터로 덮어씌우게 되니 주의해야할 점이다.







READ

firebase 에 추가한 데이터들을 읽어서 가져오려면
한번만 불러오는 것과 지속적으로 DB 의 변경이 있을 때마다 불러오는 방식으로 크게 두가지 방식이 있다.

one Time vs Read Time

one Time

Future<List<Running>> fetchData() async {
	FirebaseFirestore firestore = FirebaseFirestore.instace;
	QuerySnapshot<Map<String, dynamic>> snapshot = await firestore.collection("runnings").get();
    List<Running> result = snapshot.docs.map((e) => Running( 속성1 : doc['속성1'], 속성2: doc['속성2'], )).toList();
 }

컬렉션의 도큐먼트들을 한 번만 호출해서 Future 비동기로 구현하는 방식이다.
QuerySnapshot 을 사용해서 데이터를 가져오는데, 리스트 안에 데이터는 객체 형태가 아닌 json형태의 데이터이기 때문에 객체로 변경해주어야 한다.


Read Time

StreamBuilder<QuerySnapshot>(
	stream: FirebaseFirestore.instance.collection("runnings").snapshots(),
    builder: ((context, snapshot) {
    	return ListView.builder();
    })
)

실시간으로 DB 컬렉션의 변경이 발생할 때마다 StreamBuilder 를 이용해 데이터를 불러오는 방식이다.





특정 조건으로 데이터 불러오기

collection 안의 document들을 한번에 불러오기보다는 특정 조건에 맞는 데이터들만 선별하여 가져오기 위해 다음 메서드? 키워드? 를 사용할 수 있다.

Limit

limit 를 이용해 원하는 개수만큼 데이터를 가져올 수 있다.
limit() 안 원하는 갯수를 설정해주면 된다.

firesotre.collection("runnings").limit($limit).get();

orderBy

DB에 저장되어있는 document 들은 순서대로 저장이 되어있지 않다.
사용자가 특정한 필드값에 정렬기준을 orderBy 를 이용해 데이터를 읽어올 수 있다.

firesotre.collection("runnings").orderBy("date").get();  //오름차순
firesotre.collection("runnings").orderBy("date", descending: true).get(); //내림차순

descending 옵션으로 지정하지 않는다면 default 값으로 첫번째 매개변수값인 date 속성을 기준으로 오름차순으로 정렬된 데이터를 가져온다. true 로 지정하였다면 내림차순으로 데이터가 정렬되어 읽어온다.

이때 date 라는 속성값의 유형은 Firebase DB 에서는 DateTime 이 아니라 Timestamp 값으로 사용하기 때문에 이 점도 유의해야할 것이다.


query

특정 조건의 필드 값을 쿼리문을 이용해 선별해 데이터를 불러올 수 있다.

firestore.collection("runnings").
	where("id", isEqualTo / isNotEqualTo / isGreaterThan / isGreaterThanOrEqualTo / isLessThan / isLessThanOrEqualTo / arrayCountains  : __).get();
   

where 쿼리문의 두번째 매개변수에는 다양한 연산자를 사용할 수 있다.
주로 사용되는 연산자는 다음과 같다.

isEqualTo (같은 경우) / isNotEqualTo (같지 않은 경우) / isGreaterThan (큰 경우) / isGreaterThanOrEqualTo (크거나 같은 경우) / isLessThan (작은 경우) / isLessThanOrEqualTo (작거나 같은 경우) / arrayCountains (배열에 특정값이 포함된 경우)

두 개의 쿼리문을 사용하고 싶을 경우에는 두번째, 세번째 를 이어서 작성할 수 있다.

firestore.collection("runnings").
	where("id", isEqualTo : _ , isLessThanOrEqualTo : _ ).get();






Update

저장된 Firebase DB 에서 데이터를 수정하려면 데이터 필드의 key-value 만 수정 및 추가하는 방법이 있다. 이때 컬렉션 내 많은 document 들 중 식별할 수 있는 유일성을 가진 document ID 값이 꼭 필요하다.

FirebaseFirestore firestore = FirebaseFirestore.instace;
bool documentExists =
          await firestore.collection("collectionId").doc("documentId").get().then((doc) => doc.exists);

      if (documentExists) {
        await runnings.doc("documentId").update(updatedData);
        await runnings.doc("documentId").update(updatedData);
        }

먼저 컬렉션 내 documents ID 값을 이용해 문서가 존재하는지 확인한다.
존재한다면,
update 메서드를 이용해 매개변수는 Map<String, dynamic> 형태의 데이터를 추가해준다.
set 메서드를 이용해서도 데이터를 추가해서 저장할 수도 있다.







Delete

삭제하는 방법도 Update 와 비슷하게 필드 안 특정 key-value 만 삭제할 수도 있고, document 하나를 삭제할 수도 있다.

FirebaseFirestore firestore = FirebaseFirestore.instace;
await firestore.collection("collectionsId").doc("documentId").update({ "필드" : FieldValue.delete(), });
await firestore.collection("collectionsId").doc("documentId").delete();

먼저, 삭제하고자 하는 key 값에 value 에는 FieldValue.delete()를 하면 하나의 필드를 삭제할 수 있다.
그리고 하나의 document 를 삭제하려면 delete()를 이용해 삭제할 수 있다.







코드

Running 객체


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

class Running extends StatefulWidget {
  String name, unit, workoutTime, avgPace, paceUnit;
  double distance;
  Timestamp date;
  TimeOfDay time;
  bool isIndoor;
  int? strength;
  String? place;
  String? memo;

  Running({
    super.key,
    required this.name,
    required this.date,
    required this.time,
    required this.distance,
    required this.unit,
    required this.avgPace,
    required this.paceUnit,
    required this.workoutTime,
    required this.isIndoor,
    this.strength,
    this.place,
    this.memo,
  });
  void setStrength(int value) {
    strength = value;
  }

  void setPlace(String value) {
    place = value;
  }

  void setMemo(String value) {
    memo = value;
  }

  factory Running.fromMap(Map<String, dynamic> map) {
    TimeOfDay timeOfDayFromJson(String json) {
      List<String> parts = json.split(':');
      return TimeOfDay(
        hour: int.parse(parts[0]),
        minute: int.parse(parts[1]),
      );
    }

    return Running(
      name: map['name'],
      date: map['date'],
      time: map['time'] != null
          ? timeOfDayFromJson(map['time'])
          : TimeOfDay.now(), // 기본값 설정

      distance: map['distance'],
      unit: map['unit'],
      avgPace: map['avgPace'],
      paceUnit: map['paceUnit'],
      workoutTime: map['workoutTime'],
      isIndoor: map['isIndoor'],
      strength: map['strength'],
      place: map['place'],
      memo: map['memo'],
    );
  }
  
  State<Running> createState() => _RunningState();
}

class _RunningState extends State<Running> {
  
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

CRUD

import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:my_run_club/widgets/running.dart';

class TaskProvider extends ChangeNotifier {
  List<BarChartGroupData> _chartData = [];
  List<BarChartGroupData> get chartData => _chartData;

  set chartData(List<BarChartGroupData> newData) {
    _chartData = newData;
    notifyListeners();
  }

  final CollectionReference runnings =
      FirebaseFirestore.instance.collection('runnings');

  Stream<QuerySnapshot> getRunningStream() {
    return runnings.orderBy('date', descending: true).snapshots();
  }

  Stream<QuerySnapshot> getRunningStandard(DateTime start, DateTime end) {
    return runnings
        .where('date',
            isGreaterThanOrEqualTo: Timestamp.fromDate(start),
            isLessThanOrEqualTo: Timestamp.fromDate(end))
        .snapshots();
  }

  final List<Running> _runnings = [];

  List<Running> get runningsList => _runnings;

  Future<void> fetchRunnings() async {
    try {
      QuerySnapshot querySnapshot = await runnings.get();
      _runnings.clear();

      for (QueryDocumentSnapshot doc in querySnapshot.docs) {
        Running running = Running(
          name: doc['name'],
          date: doc['date'],
          time: doc['time'],
          distance: doc['distance'],
          unit: doc['unit'],
          avgPace: doc['avgPace'],
          paceUnit: doc['paceUnit'],
          workoutTime: doc['workoutTime'],
          isIndoor: doc['isIndoor'],
        );

        _runnings.add(running);
      }

      notifyListeners();
    } catch (e) {
      print('Error fetching runnings: $e');
    }
  }

  Future<void> addTask(Running task) async {
    await runnings.add({
      'name': task.name,
      'date': task.date,
      'distance': task.distance,
      'unit': task.unit,
      'avgPace': task.avgPace,
      'paceUnit': task.paceUnit,
      'workoutTime': task.workoutTime,
      'isIndoor': task.isIndoor,
    });
    _runnings.add(task);
    notifyListeners();
  }

  Future<void> updateTask(String id, Map<String, dynamic> updatedData) async {
    try {
      bool documentExists =
          await runnings.doc(id).get().then((doc) => doc.exists);

      if (documentExists) {
        await runnings.doc(id).update(updatedData);

        notifyListeners();
      } else {
        print('ID가 $id인 문서가 존재하지 않습니다.');
      }
    } catch (e) {
      print('작업 업데이트 중 오류 발생: $e');
    }
  }

  Future<void> deleteTask(String id) async {
    try {
      bool documentExists =
          await runnings.doc(id).get().then((doc) => doc.exists);

      if (documentExists) {
        await runnings.doc(id).delete();

        // _runnings List에서도 삭제
        // _runnings.removeWhere((task) => task.id == id);

        notifyListeners();
      } else {
        print('ID가 $id인 문서가 존재하지 않습니다.');
      }
    } catch (e) {
      print('작업 삭제 중 오류 발생: $e');
    }
  }
}

Provider 를 이용해서 복잡해보일 수 있으나, 사용하고자 하는 파일에서 Provider 로 메서드에 접근하여 간단한 매개변수를 사용하여 CRUD 를 구현할 수 있다.

그럼 runnings 라는 컬렉션 안에 자동적으로 생선된 documentId 내 다양한 필드가 추가되어 있는 모습을 볼 수 있다.








마무리

로컬 스토리지를 이용해 데이터를 관리하는 것이 꽤나 번거롭기도 해서 Firebase Firestore 를 사용하기로 결심했는데, 설정부터 쉽지 않았다.
컬렉션과 문서와 필드 등의 No-SQL 데이터 베이스 구조의 원리를 이해하면서 CRUD 기능을 간단하게 동작시킬 수 있어서 굉장히 뿌듯했다.
아직 배워야 할 게 많지만 flutter + firestore 가 오히려 더 가볍게 느껴지는 것 같기도 하다.

다음은 Flutter 의 전역 상태 관리를 하는 Provider 에 대해 살펴보도록 하겠다.





0개의 댓글