How to return a firestore collection and add to a flutter streamController repository patter

Viewed 41

Update: In vsCode, I see the following error,

_CastError (type 'Future<QuerySnapshot<Map<String, dynamic>>>' is not a subtype of type 'List' in type cast)

I'm new to Flutter and Firestore and am trying to adapt the Todos example from Bloc Library to use Firestore storage instead of local storage. Adding and deleting Todos works, however I am struggling with loading Todos as a Stream on initial load. It seems like data is returned from Firestore as I can see todosin the console by using a print(todos) command, but I get an error (Expected a value of type 'List', but got one of type 'List>') and nothing displays on the page.

The following is my API class, and the issues seems to be in the _init method

import 'dart:async';
import 'dart:convert';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:rxdart/subjects.dart';
import 'package:wherervapp/packages/apis/todos_api.dart';
import 'package:wherervapp/packages/models/todo.dart';
import 'package:wherervapp/packages/utils/json_map.dart';

/// {@template cloud_firestore_todos_api}
/// A Flutter implementation of the [TodosApi] that
/// uses Firebase Cloud Firestgore storage.
/// {@endtemplate}
class FirestoreTodosApi extends TodosApi {
  /// {@macro cloud_firestore_todos_api}
  FirestoreTodosApi({
    required FirebaseFirestore cloudFirestore,
  }) : db = cloudFirestore {
    _init();
  }

  final FirebaseFirestore db;

  final _todoStreamController = BehaviorSubject<List<Todo>>.seeded(const []);

  void _init() async {
    final QuerySnapshot<Map<String, dynamic>>? todosJson =
        await db.collection('todos').get();

    if (todosJson != null) {
      final todos = List<Map<String, dynamic>>.from(todosJson as List)
          .map((jsonMap) => Todo.fromJson(Map<String, dynamic>.from(jsonMap)))
          .toList();
      print(todos);

      _todoStreamController.add(todos as dynamic);
    } else {
      _todoStreamController.add(const []);
    }
  }

  @override
  Stream<List<Todo>> getTodos() => _todoStreamController.asBroadcastStream();

  @override
  Future<void> saveTodo(Todo todo) async {
    final todos = [..._todoStreamController.value];
    final todoIndex = todos.indexWhere((t) => t.id == todo.id);

    if (todoIndex >= 0) {
      todos[todoIndex] = todo;
    } else {
      todos.add(todo);
    }
    final ref = db.collection('todos').doc(todo.id);
    _todoStreamController.add(todos);
    return ref.set(todo.toJson()).onError((error, stackTrace) => null);
  }

  @override
  Future<void> deleteTodo(String id) async {
    final todos = [..._todoStreamController.value];
    final todoIndex = todos.indexWhere((t) => t.id == id);

    if (todoIndex == -1) {
      throw TodoNotFoundException();
    } else {
      final ref = db.collection('todos').doc(id);
      todos.removeAt(todoIndex);
      _todoStreamController.add(todos);
      return ref.delete();
    }
  }

The following is my Todo model class, which uses Json_Serializable to serialize / de-serialize JSON,

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:wherervapp/packages/utils/json_map.dart';
import 'package:uuid/uuid.dart';

part 'todo.g.dart';

/// {@template todo}
/// A single todo item.
///
/// Contains a [title], [description] and [id], in addition to a [isCompleted]
/// flag.
///
/// If an [id] is provided, it cannot be empty. If no [id] is provided, one
/// will be generated.
///
/// [Todo]s are immutable and can be copied using [copyWith], in addition to
/// being serialized and deserialized using [toJson] and [fromJson]
/// respectively.
/// {@endtemplate}
@immutable
@JsonSerializable()
class Todo extends Equatable {
  /// {@macro todo}
  Todo({
    String? id,
    required this.title,
    this.description = '',
    this.isCompleted = false,
  })  : assert(
          id == null || id.isNotEmpty,
          'id can not be null and should be empty',
        ),
        id = id ?? const Uuid().v4();

  /// The unique identifier of the todo.
  ///
  /// Cannot be empty.
  final String id;

  /// The title of the todo.
  ///
  /// Note that the title may be empty.
  final String title;

  /// The description of the todo.
  ///
  /// Defaults to an empty string.
  final String description;

  /// Whether the todo is completed.
  ///
  /// Defaults to `false`.
  final bool isCompleted;

  /// Returns a copy of this todo with the given values updated.
  ///
  /// {@macro todo}
  Todo copyWith({
    String? id,
    String? title,
    String? description,
    bool? isCompleted,
  }) {
    return Todo(
      id: id ?? this.id,
      title: title ?? this.title,
      description: description ?? this.description,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }

  Todo.fromDocumentSnapshot(DocumentSnapshot<Map<String, dynamic>> doc)
      : id = doc.id,
        title = doc.data()!['title'],
        description = doc.data()!['description'],
        isCompleted = doc.data()!['isCompleted'];

  /// Deserializes the given [JsonMap] into a [Todo].
  static Todo fromJson(JsonMap json) => _$TodoFromJson(json);

  /// Converts this [Todo] into a [JsonMap].
  JsonMap toJson() => _$TodoToJson(this);

  @override
  List<Object> get props => [id, title, description, isCompleted];
}

I'm not sure what else to provide but hopefully someone sees a glaring mistake on my part.

0 Answers
Related