typed_firestore를 소개합니다
이 글은 google/built_value.dart를 모르면 이해하기 힘들 수도 있습니다.
typed_firestore 이란?
flutter
프레임워크와 firestore
를 같이 사용할 때 유용한 라이브러리입니다.
기능
typed
dart
는 타입이 있는 언어입니다. 2.0이 나오면서 타입 시스템 관련된 기능이 많이 개선되었죠.
공식 firestore
라이브러리는 Map<String, dynamic>
을 사용하는데,dynamic
은 타입 시스템의 이점을 전부 날려버립니다.
그래서 나온 라이브러리가 typed_firestore
입니다. 이 라이브러리는 dart 2.0의 타입 시스템을 충분히 활용합니다.
final _fb = TypedFirestore(Firestore.instance, serializers);
// CollRef<Car> extends TypedQuery<Car>
final CollRef<Car> cars = _fb.collection<Car>("cars");
// 쿼리
final TypedQuery<Car> query = cars.limit(5);
final TypedQuerySnapshot<Car> qs = await query.getDocs();
final List<DocSnapshot<Car>> cars = qs.docs;
// 문서 접근
final DocRef<Car> carRef = cars.doc('foo');
final DocSnapshot<Car> car = await carRef.get();
(물론 firestore
의 특성상, 가끔은 dynamic
이 필요합니다)
상속
@BuiltValue(instantiable: false)
abstract class Schedule {
DateTime get start;
DateTime get end;
}
/// 일회성 이벤트를 나타냅니다.
@BuiltValue(wireName: 'once')
abstract class Once implements Built<Once, OnceBuilder>, Schedule {
static Serializer<Once> get serializer => _$onceSerializer;
Once._();
factory Once([updates(OnceBuilder b)]) = _$Once;
}
abstract class Repeated implements Built<Repeated, RepeatedBuilder>, Schedule {
TimeOfDay get startTime;
TimeOfDay get endTime;
RepeatMode get mode;
static Serializer<Repeated> get serializer => _$repeatedSerializer;
Repeated._();
factory Repeated([updates(RepeatedBuilder b)]) = _$Repeated;
}
.raw
가끔은 dynamic
이 필요합니다. 필드 값을 서버 시간에 맞출 때와 필드를 삭제할 때는 dynamic
(혹은 Object
) 타입이 필요합니다.
firestore
의 FieldValue
가 특수한 값을 사용하는 대신 Object
타입의 객체와 비교하기 때문인데요, 이럴 때를 위해 DocRef
클래스에 .raw
라는 getter
가 정의되어 있습니다.
Dogfooding
이 라이브러리를 이용해서 4개의 앱을 제작했으며, 그 중 하나는 다트 코드만 3만 5천 줄이 넘는 대형 플랫폼 앱입니다. 앱별로 요구사항이 상이했는데, 이 라이브러리는 그 요구사항을 모두 만족합니다.
사용법
1. 타입 정의
이 라이브러리는 built_value
를 이용해서 json을 변환합니다.
import 'package:typed_firestore/typed_firestore.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
abstract class Activity extends DocData
implements Built<Activity, ActivityBuilder> {
// built_value 에선 이런 식으로 필드를 지정합니다.
String get content;
// 모든 필드가 항상 존재할 필요는 없죠. 그럴 땐 @nullable 을 활용하시면 됩니다.
@nullable
String get location;
// 중요한 부분입니다. typed_firestore은 built_value를 이용해
// serialization, deserialization을 처리하기 때문에 아래와 같은 코드가 필요합니다.
static Serializer<Activity> get serializer => _$activitySerializer;
// 아래 두 줄은 built_value에서 사용하는 생성자입니다.
Activity._();
factory Activity([updates(ActivityBuilder b)]) = _$Activity;
}
2. 코드 생성
@SerializersFor([
Activity,
])
final Serializers mySerializers = _$mySerializers;
final Serializers serializers = (mySerializers.toBuilder()
..addPlugin(StandardJsonPlugin())
.build();
# 코드 변경되면 자동으로 업데이트
flutter packages pub run build_runner watch
# CI에서 사용하는 명령어
flutter packages pub run build_runner build
3. 사용
final _fb = TypedFirestore(Firestore.instance, serializers);
// CollRef<Car> extends TypedQuery<Car>
final CollRef<Activity> activities = _fb.collection<Activity>("activities");
// 쿼리
final TypedQuery<Activity> query = activities.limit(5);
final TypedQuerySnapshot<Activity> qs = await query.getDocs();
final List<DocSnapshot<Activity>> activities = qs.docs;
// 문서 접근
final DocRef<Car> activityRef = activities.doc('foo');
final DocSnapshot<Car> activity = await activityRef.get();
팁
bt 스니펫
이 파트의 내용은 built_value.dart/README.md의 관련 섹션과 같은 내용입니다.
IntelliJ live template에 아래 내용을 bt로 저장해놓으시면 매우 편리합니다.
abstract class $CLASS_NAME$ implements Built<$CLASS_NAME$, $CLASS_NAME$Builder> {
$CLASS_NAME$._();
factory $CLASS_NAME$([void Function($CLASS_NAME$Builder) updates]) = _$$$CLASS_NAME$;
}
DateTime
final Serializers serializers = (mySerializers.toBuilder()
..addPlugin(_TimestampSerializerPlugin()))
.build();
class _TimestampSerializerPlugin implements SerializerPlugin {
@override
Object beforeSerialize(Object object, FullType specifiedType) {
if (object is DateTime && specifiedType.root == DateTime) {
return object.toUtc();
}
return object;
}
@override
Object afterSerialize(Object object, FullType specifiedType) {
if (specifiedType.root == DateTime) {
return Timestamp.fromMicrosecondsSinceEpoch(object);
}
return object;
}
@override
Object beforeDeserialize(Object object, FullType specifiedType) {
if (object is Timestamp && specifiedType.root == DateTime) {
return object.microsecondsSinceEpoch;
// return object.toDate();
} else {
return object;
}
}
@override
Object afterDeserialize(Object object, FullType specifiedType) {
return object;
}
}
TimeOfDay
flutter에서 제공하는 TimeOfDay를 직렬화하는 예시입니다.
final Serializers serializers = (mySerializers.toBuilder()
..add(_TimeOfDaySerializer())
.build();
/// TimeOfDay <-> minutes (int)
///
/// - 1시 10분 <-> 70
/// - 3시 5분 <-> 185
class _TimeOfDaySerializer implements PrimitiveSerializer<TimeOfDay> {
@override
Iterable<Type> get types => BuiltList<Type>([TimeOfDay]);
@override
String get wireName => 'TimeOfDay';
@override
TimeOfDay deserialize(Serializers serializers, Object serialized,
{FullType specifiedType = FullType.unspecified}) {
final v = serialized as int;
return TimeOfDay(hour: v ~/ 60, minute: v - (v ~/ 60) * 60);
}
@override
Object serialize(Serializers serializers, TimeOfDay object,
{FullType specifiedType = FullType.unspecified}) {
return object.hour * 60 + object.minute;
}
}
계획
partial update
partial 업데이트를 더 잘 지원하기 위해 코드 생성기를 만들 계획입니다.
가능한지는 확실하지 않지만, 현재 생각하고 있는 API는 이렇습니다.
final DocRef<Car> ref = _fb.collection('cars').doc('foo');
await ref.set(
CarUpdate()..brand = "newBrand",
merge: true
);
merge
클라이언트에서 여러 개의 쿼리를 merge 해주는 함수를 추가할지 말지 고민중입니다.