Closed stargazing-dino closed 3 years ago
So I tried a lot of ways but didn't make much progress.
Here were the things I really wanted:
Doc
class (e.g. UserDoc)My proof of concept interestingly works but has some problems I think.
Given a file user.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fireproof_annotation/fireproof_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.fire.dart';
part 'user.freezed.dart';
part 'user.g.dart';
// fireproof is the lib name in place of flamingo for personal testing
@Fireproof()
abstract class BaseUser {
const BaseUser({
required this.name,
required this.email,
});
final String name;
final String email;
}
It'll first generate a file called user.fire.dart
:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// FireproofGenerator
// **************************************************************************
@freezed
class User with _$User {
factory User({
required String name,
required String email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
@freezed
class UserDoc with _$UserDoc {
factory UserDoc({
required String name,
required String email,
required DocumentReference reference,
}) = _UserDoc;
factory UserDoc.fromSnapshot(DocumentSnapshot snapshot) {
final data = snapshot.data();
final dataUser = _$UserFromJson(data!);
return UserDoc(
name: dataUser.name,
email: dataUser.email,
reference: snapshot.reference,
);
}
}
Because this runner is set to run before json_serializable and freezed, those builders will consider this part file in the rest of the original file and build their own user.freezed.dart
and user.g.dart
.
This solution solved 2 out of 3 of my personal requirements but I couldn't think of way you could add easily customize the generated classes for example to add custom getters on UserDoc
.
I'd love to know what you think.
@mono0926 I know you are doing similar work with flutter_firestore_ref and I'm wondering if you have any thoughts about this?
Sorry to spam you! I just don't know where to write what i found as I haven't seen anyone else asking for this. I'm also grateful that you had this awesome framework :)
I finally found a method for me that covered my use cases (although it's not as pretty I'd like). As it turns out, what I really wanted was to be able to fully type wrap the entire firebase database at compile time (that is, considering it a graph, have all the nodes mapped and typed out beforehand by the user) and still have everything also be non-nullable by default.
Honestly, this problem would've been very easy if dart had inherited factories or static methods !!! Also, meta programming would've been very helpful.
So what I ended up doing instead is making a way to create fully type safe wrappers around most firebase types. For example, consider I have a user in a user.dart
file:
@freezed
class User with _$User {
const factory User({
required String name,
required String email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
I decided to make a code generator that will work on top of a user defined mixin _$UserDoc
. Why not a class? Well, because it'd force them to write out all the fields and factory as well.
So, in the same user.dart
file, if they define this:
@Fireproof()
mixin _$UserDoc on FireDocumentReference<User, UserDoc> {
@override
FireCollectionReference get parent => super.parent;
@Collection(name: 'badges')
late final FireCollectionReference<BadgeDoc> badgesCollection;
}
After running build runner I'll generate a class called UserDoc
inside user.fire.dart
. It mirrors DocumentReference
from firestore except for that all of its methods are type safe.
class UserDoc with _$UserDoc implements FireDocumentReference<User, UserDoc> {
final User data;
final DocumentReference reference;
final String id;
final SnapshotMetadata metadata;
// TODO: How do I get the parent?
FireCollectionReference get parent => reference.parent;
String get path => reference.path;
FirebaseFirestore get firestore => reference.firestore;
UserDoc({
required this.data,
required this.reference,
required this.id,
required this.metadata,
});
factory UserDoc.fromSnapshot(DocumentSnapshot snapshot) {
final data = snapshot.data();
if (!snapshot.exists) {
throw StateError('Snapshot $snapshot does not exist');
}
if (data == null) {
throw StateError('No data for snapshot $snapshot');
}
return UserDoc(
data: User.fromJson(data),
id: snapshot.id,
metadata: snapshot.metadata,
reference: snapshot.reference,
);
}
@override
Future<void> delete() async {
await reference.delete();
}
@override
Future<UserDoc> get([GetOptions? options]) async {
final documentSnapshot = await reference.get();
return UserDoc.fromSnapshot(documentSnapshot);
}
@override
Future<void> set(User data, [SetOptions? options]) async {
await reference.set(data.toJson(), options);
}
@override
Stream<UserDoc> snapshots({bool includeMetadataChanges = false}) async* {
yield* reference
.snapshots(includeMetadataChanges: includeMetadataChanges)
.map(
(documentSnapshot) {
return UserDoc.fromSnapshot(documentSnapshot);
},
);
}
@override
Future<void> update(User data) async {
await reference.update(data.toJson());
}
}
Why not a generic class that has a type T
for User
? Well that's because I do a lot of toJson
and fromJson
and abstract classes cannot define static methods or factories so there'd be no way to define that that's even possible on a sub type.... sigh i know dumb.
I'm probably going to publish this as a package once I figure out the mapping the entire database but I wanted to share this in case it helps you in any way. I'll close this as I've mostly been rambling.
Feel free to reopen though
@Nolence Thank you for your suggestion. I think that the flamingo is a problem because it can be use as mutable. I'm thinking of updating it so that it can be used immutable in the future, but I haven't had time to do it yet.
I will consider immutable design in the near future, so I will think about it at that time.
Thank you
No rush :) I also really enjoyed using your package and just wanted to help out in some way !
Feel free to mention me if you'd like me to help out with any issues. I'd be happy to help if i can
Hi ! I've been struggling to find a good approach to modeling firebase data. I started with
functional_data
and then looked at this library but was discouraged by mutability and then I settled onfreezed
. However with the new null safety, I've been trying to solve one problem. Having everything be non-null. This includes thereferences
,id
s and so on of a document. One thing I've come to realize is that this would need two separate classes. Take for example auser
. One class is the base implementation. Nothing to do with firebase. The second (generated) class would be the one after we've associated it with a firebase document. Its reference would never null.I came up with a close approximation of what it'd look like here:
One thing I really like about this is that nothing is nullable. If something is we'd have a @nullable annotation or something otherwise we'll throw a parse error.
To update a value for example we could:
If this is something you'd be interested in, I could try for a PR or be glad to keep discussing