Snapshot do README original do repositório-fonte vinculado. Consulte o repositório para o conteúdo mais recente.
Flamingo
Flamingo is a firebase firestore model framework library.
https://pub.dev/packages/flamingo
日本語ドキュメント
Example code
See the example directory for a complete sample app using flamingo.
example
Installation
Add this to your package's pubspec.yaml file:
dependencies:
flamingo:
flamingo_annotation:
dev_dependencies:
build_runner:
flamingo_generator:
Setup
Please check Setup of cloud_firestore.
https://pub.dev/packages/cloud_firestore
Usage
Adding a initializeApp code to main.dart.
Initialize
import 'package:flamingo/flamingo.dart' ;
void main () async {
WidgetsFlutterBinding .ensureInitialized ();
await Flamingo .initializeApp ();
...
}
Create Model
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.
part of 'user.dart' ;
enum UserKey {
name,
}
extension UserKeyExtension on UserKey {
String get value {
switch (this ) {
case UserKey .name:
return 'name' ;
default :
throw Exception ('Invalid data key.' );
}
}
}
Map <String , dynamic > _$toData (User doc) {
final data = <String , dynamic >{};
Helper .writeNotNull (data, 'name' , doc.name);
return data;
}
void _$fromData (User doc, Map <String , dynamic > data) {
doc.name = Helper .valueFromKey<String >(data, 'name' );
}
[Option] build.yaml
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
CRUD
Create instance the following code.
final user = User ();
print (user.id);
final user = User (id: 'userId' );
print (user.id);
Using DocumentAccessor or Batch or Transaction in order to CRUD.
final user = User ()
..name = 'hoge' ;
final documentAccessor = DocumentAccessor ();
await documentAccessor.save (user);
await documentAccessor.update (user);
await documentAccessor.delete (user);
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 (() {
if (cache != null ) {
name = cache.name;
}
});
},
);
setState (() {
if (user != null ) {
name = user.name;
}
});
Get Collection Documents
CollectionPaging
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),
);
List <User > items = await collectionPaging.load ();
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 (() {
items = caches;
});
},
);
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),
);
sample code
CollectionPagingListener
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),
);
collectionPagingListener.fetch ();
final items = <User >[];
collectionPagingListener.data.listen ((event) {
setState (() {
items = event;
});
});
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} ' );
}
});
collectionPagingListener.loadMore ();
await collectionPagingListener.dispose ();
sample code
firestoreInstance
Can be get documents in collection.
final path = Document .path<User >();
final snapshot = await firestoreInstance.collection (path).get ();
final listA = snapshot.docs.map ((item) => User (snapshot: item)).toList ()
..forEach ((user) {
print (user.id);
});
final listB = snapshot.docs.map ((item) => User (id: item.documentID, values: item.data)).toList ()
..forEach ((user) {
print (user.id);
});
Snapshot Listener
Listen snapshot of document.
final user = User (id: '0' )
..name = 'hoge' ;
final dispose = user.reference.snapshots ().listen ((snap) {
final user = User (snapshot: snap);
print ('${user.id} , ${user.name} ' );
});
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.
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} ' ));
});
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 ();
Model of map object
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.
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);
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)} ' );
Sub Collection
For example, ranking document has count collection.
Ranking model
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);
}
Count model
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);
}
Save and Get Sub Collection.
final ranking = Ranking (id: '20201007' )
..title = 'userRanking' ;
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 ();
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);
});
File
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 = ...
storage.fetch ();
storage.uploader.listen ((data){
print ('total: ${data.totalBytes} transferred: ${data.bytesTransferred} ' );
});
final path = '${post.documentPath} /${PostKey.file.value} ' ;
post.file = await storage.save (path, file, mimeType: mimeTypePng, metadata: {'newPost' : 'true' });
await documentAccessor.save (post);
storage.dispose ();
Delete storage file.
await storage.delete (post.file);
await documentAccessor.update (post);
Alternatively, flamingo provide function to operate Cloud Storage and Firestore.
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 ,
},
);
await storage.deleteWithDoc (post.reference, PostKey .file.value, post.file, isNotNull: true );
Increment
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);
@override
void onCompleted (ExecuteType executeType) {
point = point.onRefresh ();
score = score.onRefresh ();
}
}
Increment and decrement of data.
final card = CreditCard ()
..point.incrementValue = 1
..score.incrementValue = 1.25 ;
await documentAccessor.save (card);
print ('point ${card.point.value} , score: ${card.score.value} ' );
final _card = await documentAccessor.load<CreditCard >(card);
print ('point ${_card.point.value} , score: ${_card.score.value} ' );
card
..point.incrementValue = -1
..score.incrementValue = -1.00 ;
await documentAccessor.update (card);
print ('point ${card.point.value} , score: ${card.score.value} ' );
final _card = await documentAccessor.load<CreditCard >(card);
print ('point ${_card.point.value} , score: ${_card.score.value} ' );
card
..point.isClearValue = true
..score.isClearValue = true ;
await documentAccessor.update (card);
print ('point ${card.point.value} , score: ${card.score.value} ' );
final _card = await documentAccessor.load<CreditCard >(card);
print ('point ${_card.point.value} , score: ${_card.score.value} ' );
Or can be use with increment method of DocumentAccessor.
final card = CreditCard ();
final batch = Batch ()
..save (card);
await batch.commit ();
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 );
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 );
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
Distributed counter
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;
@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.
final score = Score ()
..userId = '0001' ;
await documentAccessor.save (score);
final distributedCounter = DistributedCounter ();
await distributedCounter.create (score.counter);
for (var i = 0 ; i < 10 ; i++) {
await distributedCounter.increment (score.counter, count: 1 );
}
final count = await distributedCounter.load (score.counter);
print ('count $count ${score.counter.count} ' );
Transaction
This api is simply wrap transaction function of Firestore.
RunTransaction .scope ((transaction) async {
final hoge = User ()
..name = 'hoge' ;
await transaction.set (hoge.reference, hoge.toData ());
final fuge = User (id: '0' )
..name = 'fuge' ;
await transaction.update (fuge.reference, fuge.toData ());
await transaction.delete (User (id: '1' ).reference);
});
Objects for model
Map objects
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));
List
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.
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);
final _sample1 = await documentAccessor.load<ListSample >(ListSample (id: sample1.id));
[WIP] Unit Test
※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' ));
...
}
sample code
Reference