Flamingo is a firebase firestore model framework library.
https://pub.dev/packages/flamingo
See the example directory for a complete sample app using flamingo.
Add this to your package's pubspec.yaml file:
dependencies:
flamingo:
flamingo_annotation:
dev_dependencies:
build_runner:
flamingo_generator:
Please check Setup of cloud_firestore.
https://pub.dev/packages/cloud_firestore
Adding a initializeApp code to main.dart.
import 'package:flamingo/flamingo.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Flamingo.initializeApp();
...
}
Create class that inherited Document. And add json mapping code into override functions.
Can be used flamingo_generator. Automatically create code for mapping JSON.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'user.flamingo.dart';
class User extends Document<User> {
User({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
}) : super(id: id, snapshot: snapshot, values: values);
@Field()
String? name;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
Annotation list.
- @Field()
- @StorageField()
- @ModelField()
- @SubCollection()
Execute build runner to generate data mapping JSON.
flutter pub run build_runner build
It will be generated the following code.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// FieldValueGenerator
// **************************************************************************
/// Field value key
enum UserKey {
name,
}
extension UserKeyExtension on UserKey {
String get value {
switch (this) {
case UserKey.name:
return 'name';
default:
throw Exception('Invalid data key.');
}
}
}
/// For save data
Map<String, dynamic> _$toData(User doc) {
final data = <String, dynamic>{};
Helper.writeNotNull(data, 'name', doc.name);
return data;
}
/// For load data
void _$fromData(User doc, Map<String, dynamic> data) {
doc.name = Helper.valueFromKey<String>(data, 'name');
}
If you set build.yaml in the root of the project, the automatic generation will be faster.
https://github.com/hukusuke1007/flamingo/blob/master/flamingo/example/build.yaml
targets:
$default:
builders:
flamingo_generator|field_value_generator:
generate_for:
include:
- lib/model/*.dart
- lib/model/**/*.dart
Create instance the following code.
final user = User();
print(user.id); // id: Automatically create document id;
final user = User(id: 'userId');
print(user.id); // id: 'userId'
Using DocumentAccessor or Batch or Transaction in order to CRUD.
final user = User()
..name = 'hoge';
final documentAccessor = DocumentAccessor();
// save
await documentAccessor.save(user);
// update
await documentAccessor.update(user);
// delete
await documentAccessor.delete(user);
// Batch
final batch = Batch()
..save(user)
..update(user);
..delete(user);
await batch.commit();
If save a document, please check firestore console.
And can be used field value key and save data by specific key.
final documentAccessor = DocumentAccessor();
await documentAccessor.saveRaw(
<String, dynamic>{ UserKey.name.value: 'hogehoge' },
user.reference,
);
Get a document.
final user = await documentAccessor.load<User>(User(id: 'userId'));
Get a document from cache.
final user = await documentAccessor.loadCache<User>(User(id: 'userId'));
Get a document from cache and server.
String name = 'Anonymous';
final user = await documentAccessor.load<User>(
User(id: 'userId'),
fromCache: (cache) {
setState(() {
// 1. update state from cache
if (cache != null) {
name = cache.name;
}
});
},
);
setState(() {
// 2. update state from serverAndCache
if (user != null) {
name = user.name;
}
});
Can be used get and paging features of documents by CollectionPaging.
Query of Collection.
final collectionPaging = CollectionPaging<User>(
query: User().collectionRef.orderBy('createdAt', descending: true),
limit: 20,
decode: (snap) => User(snapshot: snap),
);
// Load
List<User> items = await collectionPaging.load();
// LoadMore
final _items = await collectionPaging.loadMore();
items.addAll(_items);
Get a documents from cache and server.
List<User> items = [];
final _items = await collectionPaging.load(
fromCache: (caches) {
setState(() {
// 1. update state from cache
items = caches;
});
},
);
// 2. update state from serverAndCache
setState(() {
items = _items;
});
Query of CollectionGroup.
final collectionPaging = CollectionPaging<User>(
query: firestoreInstance
.collectionGroup('user')
.orderBy('createdAt', descending: true),
limit: 20,
decode: (snap) => User(snapshot: snap),
);
Can be used listener and paging features of documents by CollectionPagingListener.
final collectionPagingListener = CollectionPagingListener<User>(
query: User().collectionRef.orderBy('updatedAt', descending: true),
initialLimit: 20,
pagingLimit: 20,
decode: (snap) => User(snapshot: snap),
);
// Fetch to set listener.
collectionPagingListener.fetch();
final items = <User>[];
// Get documents via listener. data is ValueStream.
collectionPagingListener.data.listen((event) {
setState(() {
items = event;
});
});
// Get document changes status and cache status.
collectionPagingListener.docChanges.listen((event) {
for (var item in event) {
final change = item.docChange;
print('id: ${item.doc.id}, changeType: ${change.type}, oldIndex: ${change.oldIndex}, newIndex: ${change.newIndex} cache: ${change.doc.metadata.isFromCache}');
}
});
// LoadMore. To load next page data.
collectionPagingListener.loadMore();
// Dispose.
await collectionPagingListener.dispose();
Can be get documents in collection.
final path = Document.path<User>();
final snapshot = await firestoreInstance.collection(path).get();
// from snapshot
final listA = snapshot.docs.map((item) => User(snapshot: item)).toList()
..forEach((user) {
print(user.id); // user model.
});
// from values.
final listB = snapshot.docs.map((item) => User(id: item.documentID, values: item.data)).toList()
..forEach((user) {
print(user.id); // user model.
});
Listen snapshot of document.
// Listen
final user = User(id: '0')
..name = 'hoge';
final dispose = user.reference.snapshots().listen((snap) {
final user = User(snapshot: snap);
print('${user.id}, ${user.name}');
});
// Save, update, delete
DocumentAccessor documentAccessor = DocumentAccessor();
await documentAccessor.save(user);
user.name = 'fuga';
await documentAccessor.update(user);
await documentAccessor.delete(user);
await dispose.cancel();
Listen snapshot of collection documents. And can be used also CollectionPagingListener.
// Listen
final path = Document.path<User>();
final query = firestoreInstance.collection(path).limit(20);
final dispose = query.snapshots().listen((querySnapshot) {
for (var change in querySnapshot.documentChanges) {
if (change.type == DocumentChangeType.added ) {
print('added ${change.document.documentID}');
}
if (change.type == DocumentChangeType.modified) {
print('modified ${change.document.documentID}');
}
if (change.type == DocumentChangeType.removed) {
print('removed ${change.document.documentID}');
}
}
final _ = querySnapshot.docs.map((item) => User(snapshot: item)).toList()
..forEach((item) => print('${item.id}, ${item.name}'));
});
// Save, update, delete
final user = User(id: '0')
..name = 'hoge';
DocumentAccessor documentAccessor = DocumentAccessor();
await documentAccessor.save(user);
user.name = 'fuga';
await documentAccessor.update(user);
await documentAccessor.delete(user);
await dispose.cancel();
Example, Owner's document object is the following json.
{
"name": "owner",
"address": {
"postCode": "0000",
"country": "japan"
},
"medals": [
{"name": "gold"},
{"name": "silver"},
{"name": "bronze"}
]
}
Owner that inherited Document has model of map object.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
import 'address.dart';
import 'medal.dart';
part 'owner.flamingo.dart';
class Owner extends Document<Owner> {
Owner({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
}) : super(id: id, snapshot: snapshot, values: values);
@Field()
String? name;
@ModelField()
Address? address;
@ModelField()
List<Medal>? medals;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
Create class that inherited Model.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'address.flamingo.dart';
class Address extends Model {
Address({
this.postCode,
this.country,
Map<String, dynamic>? values,
}) : super(values: values);
@Field()
String? postCode;
@Field()
String? country;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'medal.flamingo.dart';
class Medal extends Model {
Medal({
this.name,
Map<String, dynamic>? values,
}) : super(values: values);
@Field()
String? name;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
Example of usage.
// save
final owner = Owner()
..name = 'owner'
..address = Address(
postCode: '0000',
country: 'japan',
)
..medals = [
Medal(name: 'gold',),
Medal(name: 'silver',),
Medal(name: 'bronze',),
];
await documentAccessor.save(owner);
// load
final _owner = await documentAccessor.load<Owner>(Owner(id: owner.id));
print('id: ${_owner.id}, name: ${_owner.name}');
print('address: ${_owner.id} ${_owner.address.postCode} ${_owner.address.country}');
print('medals: ${_owner.medals.map((d) => d.name)}');
For example, ranking document has count collection.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
import 'count.dart';
part 'ranking.flamingo.dart';
class Ranking extends Document<Ranking> {
Ranking(
{String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
CollectionReference<Map<String, dynamic>>? collectionRef})
: super(
id: id,
snapshot: snapshot,
values: values,
collectionRef: collectionRef) {
count = Collection(this, RankingKey.count.value);
}
@Field()
String? title;
@SubCollection()
late Collection<Count> count;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'count.flamingo.dart';
class Count extends Document<Count> {
Count({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
CollectionReference<Map<String, dynamic>>? collectionRef,
}) : super(
id: id,
snapshot: snapshot,
values: values,
collectionRef: collectionRef);
@Field()
String? userId;
@Field()
int count = 0;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
final ranking = Ranking(id: '20201007')
..title = 'userRanking';
// Save sub collection of ranking document
final countA = Count(collectionRef: ranking.count.ref)
..userId = '0'
..count = 10;
final countB = Count(collectionRef: ranking.count.ref)
..userId = '1'
..count = 100;
final batch = Batch()
..save(ranking)
..save(countA)
..save(countB);
await batch.commit();
// Get sub collection
final path = ranking.count.ref.path;
final snapshot = await firestoreInstance.collection(path).get();
final list = snapshot.docs.map((item) => Count(snapshot: item)).toList()
..forEach((count) {
print(count);
});
Can operation into Firebase Storage and upload and delete storage file. Using StorageFile and Storage class.
For example, create post model that have StorageFile.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'post.flamingo.dart';
class Post extends Document<Post> {
Post({String? id}) : super(id: id);
@StorageField()
StorageFile? file;
@StorageField()
List<StorageFile>? files;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
Upload file to Firebase Storage.
final post = Post();
final storage = Storage();
final file = ... // load image.
// Fetch uploader stream
storage.fetch();
// Checking status
storage.uploader.listen((data){
print('total: ${data.totalBytes} transferred: ${data.bytesTransferred}');
});
// Upload file into firebase storage and save file metadata into firestore
final path = '${post.documentPath}/${PostKey.file.value}';
post.file = await storage.save(path, file, mimeType: mimeTypePng, metadata: {'newPost': 'true'}); // 'mimeType' is defined in master/master.dart
await documentAccessor.save(post);
// Dispose uploader stream
storage.dispose();
Delete storage file.
// delete file in firebase storage and delete file metadata in firestore
await storage.delete(post.file);
await documentAccessor.update(post);
Alternatively, flamingo provide function to operate Cloud Storage and Firestore.
// Save storage and document of storage data.
final storageFile = await storage.saveWithDoc(
post.reference,
PostKey.file.value,
file,
mimeType: mimeTypePng,
metadata: {
'newPost': 'true'
},
additionalData: <String, dynamic>{
'key0': 'key',
'key1': 10,
'key2': 0.123,
'key3': true,
},
);
// Delete storage and document of storage data.
await storage.deleteWithDoc(post.reference, PostKey.file.value, post.file, isNotNull: true);
Example, CreditCard's document has point and score field. Their fields is Increment type.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'credit_card.flamingo.dart';
class CreditCard extends Document<CreditCard> {
CreditCard({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
}) : super(id: id, snapshot: snapshot, values: values);
@Field()
Increment<int> point = Increment<int>();
@Field()
Increment<double> score = Increment<double>();
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
/// Call after create, update, delete.
@override
void onCompleted(ExecuteType executeType) {
point = point.onRefresh();
score = score.onRefresh();
}
}
Increment and decrement of data.
// Increment
final card = CreditCard()
..point.incrementValue = 1
..score.incrementValue = 1.25;
await documentAccessor.save(card);
print('point ${card.point.value}, score: ${card.score.value}'); // point 1, score 1.25
final _card = await documentAccessor.load<CreditCard>(card);
print('point ${_card.point.value}, score: ${_card.score.value}'); // point 1, score 1.25
// Decrement
card
..point.incrementValue = -1
..score.incrementValue = -1.00;
await documentAccessor.update(card);
print('point ${card.point.value}, score: ${card.score.value}'); // point 0, score 0.25
final _card = await documentAccessor.load<CreditCard>(card);
print('point ${_card.point.value}, score: ${_card.score.value}'); // point 0, score 0.25
// Clear
card
..point.isClearValue = true
..score.isClearValue = true;
await documentAccessor.update(card);
print('point ${card.point.value}, score: ${card.score.value}'); // point 0, score 0.0
final _card = await documentAccessor.load<CreditCard>(card);
print('point ${_card.point.value}, score: ${_card.score.value}'); // point 0, score 0.0
Or can be use with increment method of DocumentAccessor.
final card = CreditCard();
final batch = Batch()
..save(card);
await batch.commit();
// Increment
card
..point = await documentAccessor.increment<int>(card.point, card.reference, fieldName: CreditCardKey.point.value, value: 10)
..score = await documentAccessor.increment<double>(card.score, card.reference, fieldName: CreditCardKey.score.value, value: 3.5);
// Decrement
card
..point = await documentAccessor.increment<int>(card.point, card.reference, fieldName: CreditCardKey.point.value, value: -5)
..score = await documentAccessor.increment<double>(card.score, card.reference, fieldName: CreditCardKey.score.value, value: -2.5);
// Clear
card
..point = await documentAccessor.increment<int>(card.point, card.reference, fieldName: CreditCardKey.point.value, isClear: true)
..score = await documentAccessor.increment<double>(card.score, card.reference, fieldName: CreditCardKey.score.value, isClear: true);
Attension:
Clear process only set 0 to document and update. It not try transaction process. Do not use except to first set doument
Using DistributedCounter and Counter.
For example, create score model that have Counter.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'score.flamingo.dart';
class Score extends Document<Score> {
Score({
String? id,
}) : super(id: id) {
counter = Counter(this, ScoreKey.counter.value, numShards);
}
@Field()
String? userId;
/// DistributedCounter
@SubCollection()
late Counter counter;
int numShards = 10;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
Create and increment and load.
/// Create
final score = Score()
..userId = '0001';
await documentAccessor.save(score);
final distributedCounter = DistributedCounter();
await distributedCounter.create(score.counter);
/// Increment
for (var i = 0; i < 10; i++) {
await distributedCounter.increment(score.counter, count: 1);
}
/// Load
final count = await distributedCounter.load(score.counter);
print('count $count ${score.counter.count}');
This api is simply wrap transaction function of Firestore.
RunTransaction.scope((transaction) async {
final hoge = User()
..name = 'hoge';
// save
await transaction.set(hoge.reference, hoge.toData());
// update
final fuge = User(id: '0')
..name = 'fuge';
await transaction.update(fuge.reference, fuge.toData());
// delete
await transaction.delete(User(id: '1').reference);
});
Create the following model class.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'map_sample.flamingo.dart';
class MapSample extends Document<MapSample> {
MapSample({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
}) : super(id: id, snapshot: snapshot, values: values);
@Field()
Map<String, String>? strMap;
@Field()
Map<String, int>? intMap;
@Field()
Map<String, double>? doubleMap;
@Field()
Map<String, bool>? boolMap;
@Field()
List<Map<String, String>>? listStrMap;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
And save and load documents.
final sample1 = MapSample()
..strMap = {'userId1': 'tanaka', 'userId2': 'hanako', 'userId3': 'shohei',}
..intMap = {'userId1': 0, 'userId2': 1, 'userId3': 2,}
..doubleMap = {'userId1': 1.02, 'userId2': 0.14, 'userId3': 0.89,}
..boolMap = {'userId1': true, 'userId2': true, 'userId3': true,}
..listStrMap = [
{'userId1': 'tanaka', 'userId2': 'hanako',},
{'adminId1': 'shohei', 'adminId2': 'tanigawa',},
{'managerId1': 'ueno', 'managerId2': 'yoshikawa',},
];
await documentAccessor.save(sample1);
final _sample1 = await documentAccessor.load<MapSample>(MapSample(id: sample1.id));
Create the following model class.
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';
part 'list_sample.flamingo.dart';
class ListSample extends Document<ListSample> {
ListSample({
String? id,
DocumentSnapshot<Map<String, dynamic>>? snapshot,
Map<String, dynamic>? values,
}) : super(id: id, snapshot: snapshot, values: values);
@Field()
List<String>? strList;
@Field()
List<int>? intList;
@Field()
List<double>? doubleList;
@Field()
List<bool>? boolList;
@StorageField(isWriteNotNull: false)
List<StorageFile>? filesA;
@StorageField()
List<StorageFile>? filesB;
@override
Map<String, dynamic> toData() => _$toData(this);
@override
void fromData(Map<String, dynamic> data) => _$fromData(this, data);
}
And save and load documents.
/// Save
final sample1 = ListSample()
..strList = ['userId1', 'userId2', 'userId3',]
..intList = [0, 1, 2,]
..doubleList = [0.0, 0.1, 0.2,]
..boolList = [true, false, true,]
..filesA = [
StorageFile(
name: 'name1', url: 'https://sample1.jpg', mimeType: mimeTypePng),
StorageFile(
name: 'name2', url: 'https://sample2.jpg', mimeType: mimeTypePng),
StorageFile(
name: 'name3', url: 'https://sample3.jpg', mimeType: mimeTypePng),
]
..filesB = [
StorageFile(
name: 'name1', url: 'https://sample1.jpg', mimeType: mimeTypePng),
StorageFile(
name: 'name2', url: 'https://sample2.jpg', mimeType: mimeTypePng),
StorageFile(
name: 'name3', url: 'https://sample3.jpg', mimeType: mimeTypePng),
];
await documentAccessor.save(sample1);
/// Load
final _sample1 = await documentAccessor.load<ListSample>(ListSample(id: sample1.id));
※Under construction
Install packages for unit test.
dev_dependencies:
...
test: ^1.14.4
cloud_firestore_mocks:
firebase_storage_mocks:
Set Firestore and Cloud Storage mock instance.
import 'package:cloud_firestore_mocks/cloud_firestore_mocks.dart';
import 'package:firebase_storage_mocks/firebase_storage_mocks.dart';
import 'package:flamingo/flamingo.dart';
import 'package:test/test.dart';
void main() async {
final firestore = MockFirestoreInstance();
final storage = MockFirebaseStorage();
await Flamingo.initializeApp(
firestore: firestore,
storage: storage,
root: firestore.document('test/v1'));
...
}