Error while serializing Built<Object> using built_value combined with FlutterFire

767 views Asked by At

I would like to combine built_value with cloud_firestore in my Flutter project. I believe it makes sense and that is how it should do it in a clean world? (see https://firebase.flutter.dev/docs/firestore/usage/#typing-collectionreference-and-documentreference).

This is where I am so far.

The Place model:

// ../models/place.dart

import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:sitback/models/serializers.dart';

part 'place.g.dart';

abstract class Place implements Built<Place, PlaceBuilder> {
  // Fields

  String? get id;
  String get name;
  String get address;
  BuiltList<String> get methods;

  Place._();

  factory Place([void Function(PlaceBuilder) updates]) = _$Place;

  String toJson() {
    return json
        .encode(standardSerializers.serializeWith(Place.serializer, this));
  }

  // https://github.com/google/built_value.dart/issues/964#issuecomment-850419921
  static Place? fromJson(String jsonString) {
    return standardSerializers.deserializeWith(
        Place.serializer, json.decode(jsonString));
  }

  Map<String, Object?> toFirestore() {
    return serializers.serializeWith(Place.serializer, this);
  }

  // TODO: check if something can be improved
  // https://github.com/google/built_value.dart/issues/964#issuecomment-850419921
  static Place? fromFirestore(Map<String, dynamic> json) {
    return serializers.deserializeWith(Place.serializer, json);
  }

  static Serializer<Place> get serializer => _$placeSerializer;
}

In the above code, I have two issues:

  1. A value of type 'Object?' can't be returned from the method 'toFirestore' because it has a return type of 'Map<String, Object?>'

However, this is a solution that is proposed at multiple locartions (SO questions, github issues), so I don't understand why I get that error.

  1. In the same reference tutorials/videos, they use Place instead of Place?, as a return value for fromJson but then I have the following error:

A value of type 'Place?' can't be returned from the method 'fromJson' because it has a return type of 'Place'

Could it be related to the null-safety recent changes?

Below are the serializers (following the doc/tutorials):

// ../models/serializers.dart

library serializers;

import 'package:built_collection/built_collection.dart';
import 'package:built_value/json_object.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:sitback/models/place.dart';

part 'serializers.g.dart';

@SerializersFor([
  Place,
])
final Serializers serializers = _$serializers;

final Serializers standardSerializers =
    (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

Finally, here is the place where I want to consume a Firestore collection while using the Place "data class" defined via the built_value package:

class IndexPage extends StatelessWidget {
  IndexPage({
    Key? key,
  }) : super(key: key);

  final placesRef = FirebaseFirestore.instance
      .collection('places')
      .withConverter<Place>(
        // TODO: ! at the end due to Place? vs Place in class definition
        // would require a try-catch or something else?
        fromFirestore: (snapshot, _) => Place.fromFirestore(snapshot.data()!)!,
        toFirestore: (place, _) => place.toFirestore(),
      );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: StreamBuilder<QuerySnapshot<Place>>(
          stream: placesRef.snapshots(),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return Center(
                child: CircularProgressIndicator(),
              );
            } else {
              return CustomScrollView(
                slivers: <Widget>[
                  SliverList(
                    delegate: SliverChildBuilderDelegate(
                      (context, index) {
                        final itemData = snapshot.data!.docs[index];
                        return PlaceCard(itemData: itemData);
                      },
                      childCount: snapshot.data!.docs.length,
                    ),
                  ),
                ],
              );
            }
          },
        ),
      ),
    );
  }
}

It is not working currently because of this line: toFirestore: (place, _) => place.toFirestore(),. If I return {} from toFirestore(), then it compiles and works

BUT I suspect fromFirestore() is not even called: if I place assert(false); inside the function, nothing happen.. I'd except an exception.

1

There are 1 answers

0
matthewfx On

You can't use a serialiser for Firestore. You need to manually create a map from your built value object to upload it. Once you want to fetch from Firebase you need to create a built object from a map. Haven't done it recently but it would be something like it:

static Place _fromFirestore(Map<dynamic, dynamic> placeMap) {
    final List<dynamic> methods = placeMap['methods'] ?? [];
    
    final place = Place((b) => b
      ..id = placeMap['id']
      ..name = placeMap['name']
      ..address = placeMap['address']
      ..methods.addAll(List<String>.from(methods.cast<String>()));
      
    return place;
  }