Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/hukusuke1007/flamingo

[Flutter Library] Flamingo is a firebase firestore model framework library. 🐀
https://github.com/hukusuke1007/flamingo

cloud-firestore firebase-storage firestore flamingo flutter flutter-package flutter-plugin

Last synced: 3 months ago
JSON representation

[Flutter Library] Flamingo is a firebase firestore model framework library. 🐀

Awesome Lists containing this project

README

        

# Flamingo

Flamingo is a firebase firestore model framework library.

[https://pub.dev/packages/flamingo](https://pub.dev/packages/flamingo)

[ζ—₯本θͺžγƒ‰γ‚­γƒ₯γƒ‘γƒ³γƒˆ](https://github.com/hukusuke1007/flamingo/blob/master/README_j.md)

## Example code
See the example directory for a complete sample app using flamingo.

[example](https://github.com/hukusuke1007/flamingo/tree/master/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](https://pub.dev/packages/cloud_firestore#setup)

## Usage

Adding a initializeApp code to main.dart.

### Initialize

```dart
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.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'user.flamingo.dart';

class User extends Document {
User({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
}) : super(id: id, snapshot: snapshot, values: values);

@Field()
String? name;

@override
Map toData() => _$toData(this);

@override
void fromData(Map 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.

```dart
// 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 _$toData(User doc) {
final data = {};
Helper.writeNotNull(data, 'name', doc.name);

return data;
}

/// For load data
void _$fromData(User doc, Map data) {
doc.name = Helper.valueFromKey(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

```yaml
targets:
$default:
builders:
flamingo_generator|field_value_generator:
generate_for:
include:
- lib/model/*.dart
- lib/model/**/*.dart
```

### CRUD

Create instance the following code.

```dart
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.

```dart
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.

```dart
final documentAccessor = DocumentAccessor();
await documentAccessor.saveRaw(
{ UserKey.name.value: 'hogehoge' },
user.reference,
);
```

Get a document.

```dart
final user = await documentAccessor.load(User(id: 'userId'));
```

Get a document from cache.

```dart
final user = await documentAccessor.loadCache(User(id: 'userId'));
```

Get a document from cache and server.

```dart
String name = 'Anonymous';

final user = await documentAccessor.load(
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;
}
});
```

### Get Collection Documents

#### CollectionPaging
Can be used get and paging features of documents by CollectionPaging.

Query of Collection.

```dart
final collectionPaging = CollectionPaging(
query: User().collectionRef.orderBy('createdAt', descending: true),
limit: 20,
decode: (snap) => User(snapshot: snap),
);

// Load
List items = await collectionPaging.load();

// LoadMore
final _items = await collectionPaging.loadMore();
items.addAll(_items);
```

Get a documents from cache and server.

```dart
List 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.

```dart
final collectionPaging = CollectionPaging(
query: firestoreInstance
.collectionGroup('user')
.orderBy('createdAt', descending: true),
limit: 20,
decode: (snap) => User(snapshot: snap),
);
```

[sample code](https://github.com/hukusuke1007/flamingo/blob/master/flamingo/example/lib/collection_paging_page.dart)

#### CollectionPagingListener
Can be used listener and paging features of documents by CollectionPagingListener.

```dart
final collectionPagingListener = CollectionPagingListener(
query: User().collectionRef.orderBy('updatedAt', descending: true),
initialLimit: 20,
pagingLimit: 20,
decode: (snap) => User(snapshot: snap),
);

// Fetch to set listener.
collectionPagingListener.fetch();

final items = [];

// 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();
```

[sample code](https://github.com/hukusuke1007/flamingo/blob/master/flamingo/example/lib/collection_paging_listener_page.dart)

#### firestoreInstance
Can be get documents in collection.

```dart
final path = Document.path();
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.
});
```

### Snapshot Listener

Listen snapshot of document.

```dart
// 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.

```dart
// Listen
final path = Document.path();
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();
```

### Model of map object

Example, Owner's document object is the following json.

```json
{
"name": "owner",
"address": {
"postCode": "0000",
"country": "japan"
},
"medals": [
{"name": "gold"},
{"name": "silver"},
{"name": "bronze"}
]
}
```

Owner that inherited **Document** has model of map object.

```dart
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({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
}) : super(id: id, snapshot: snapshot, values: values);

@Field()
String? name;

@ModelField()
Address? address;

@ModelField()
List? medals;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

Create class that inherited **Model**.

```dart
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? values,
}) : super(values: values);

@Field()
String? postCode;

@Field()
String? country;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'medal.flamingo.dart';

class Medal extends Model {
Medal({
this.name,
Map? values,
}) : super(values: values);

@Field()
String? name;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

Example of usage.

```dart
// 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(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

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

import 'count.dart';

part 'ranking.flamingo.dart';

class Ranking extends Document {
Ranking(
{String? id,
DocumentSnapshot>? snapshot,
Map? values,
CollectionReference>? collectionRef})
: super(
id: id,
snapshot: snapshot,
values: values,
collectionRef: collectionRef) {
count = Collection(this, RankingKey.count.value);
}

@Field()
String? title;

@SubCollection()
late Collection count;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

#### Count model

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'count.flamingo.dart';

class Count extends Document {
Count({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
CollectionReference>? collectionRef,
}) : super(
id: id,
snapshot: snapshot,
values: values,
collectionRef: collectionRef);

@Field()
String? userId;

@Field()
int count = 0;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

#### Save and Get Sub Collection.

```dart
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);
});
```

### 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.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'post.flamingo.dart';

class Post extends Document {
Post({String? id}) : super(id: id);

@StorageField()
StorageFile? file;

@StorageField()
List? files;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

Upload file to Firebase Storage.

```dart
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.

```dart
// 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.

```dart
// Save storage and document of storage data.
final storageFile = await storage.saveWithDoc(
post.reference,
PostKey.file.value,
file,
mimeType: mimeTypePng,
metadata: {
'newPost': 'true'
},
additionalData: {
'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);
```

### Increment

Example, CreditCard's document has point and score field. Their fields is Increment type.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'credit_card.flamingo.dart';

class CreditCard extends Document {
CreditCard({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
}) : super(id: id, snapshot: snapshot, values: values);

@Field()
Increment point = Increment();

@Field()
Increment score = Increment();

@override
Map toData() => _$toData(this);

@override
void fromData(Map 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.

```dart
// 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(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(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(card);
print('point ${_card.point.value}, score: ${_card.score.value}'); // point 0, score 0.0
```

Or can be use with increment method of DocumentAccessor.

```dart
final card = CreditCard();
final batch = Batch()
..save(card);
await batch.commit();

// Increment
card
..point = await documentAccessor.increment(card.point, card.reference, fieldName: CreditCardKey.point.value, value: 10)
..score = await documentAccessor.increment(card.score, card.reference, fieldName: CreditCardKey.score.value, value: 3.5);

// Decrement
card
..point = await documentAccessor.increment(card.point, card.reference, fieldName: CreditCardKey.point.value, value: -5)
..score = await documentAccessor.increment(card.score, card.reference, fieldName: CreditCardKey.score.value, value: -2.5);

// Clear
card
..point = await documentAccessor.increment(card.point, card.reference, fieldName: CreditCardKey.point.value, isClear: true)
..score = await documentAccessor.increment(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.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'score.flamingo.dart';

class Score extends Document {
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 toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

Create and increment and load.

```dart
/// 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}');
```

### Transaction

This api is simply wrap transaction function of Firestore.

```dart
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);
});
```

### Objects for model

#### Map objects

Create the following model class.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'map_sample.flamingo.dart';

class MapSample extends Document {
MapSample({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
}) : super(id: id, snapshot: snapshot, values: values);

@Field()
Map? strMap;

@Field()
Map? intMap;

@Field()
Map? doubleMap;

@Field()
Map? boolMap;

@Field()
List>? listStrMap;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

And save and load documents.

```dart
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(id: sample1.id));
```

#### List

Create the following model class.

```dart
import 'package:flamingo/flamingo.dart';
import 'package:flamingo_annotation/flamingo_annotation.dart';

part 'list_sample.flamingo.dart';

class ListSample extends Document {
ListSample({
String? id,
DocumentSnapshot>? snapshot,
Map? values,
}) : super(id: id, snapshot: snapshot, values: values);

@Field()
List? strList;

@Field()
List? intList;

@Field()
List? doubleList;

@Field()
List? boolList;

@StorageField(isWriteNotNull: false)
List? filesA;

@StorageField()
List? filesB;

@override
Map toData() => _$toData(this);

@override
void fromData(Map data) => _$fromData(this, data);
}
```

And save and load documents.

```dart
/// 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(id: sample1.id));
```

## [WIP] Unit Test

β€»Under construction

Install packages for unit test.

- [test](https://pub.dev/packages/test)
- [cloud_firestore_mocks](https://pub.dev/packages/cloud_firestore_mocks)
- [firebase_storage_mocks](https://pub.dev/packages/firebase_storage_mocks)

```
dev_dependencies:
...

test: ^1.14.4
cloud_firestore_mocks:
firebase_storage_mocks:
```

Set Firestore and Cloud Storage mock instance.

```dart
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](https://github.com/hukusuke1007/flamingo/blob/master/flamingo/example/test/example_test.dart)

## Reference
- [Firebase for Flutter](https://firebase.google.com/docs/flutter/setup)
- [Ballcap for iOS](https://github.com/1amageek/Ballcap-iOS)
- [Ballcap for TypeScript](https://github.com/1amageek/ballcap.ts)