Soojeong Lee

Aug 9, 2025

Soojeong Lee

Aug 9, 2025

Soojeong Lee

Aug 9, 2025

Mid Refactoring: Firestore

Mid Refactoring: Firestore

Mid Refactoring: Firestore

Untangling Relationships

First, I tried to redefine the model.

class BaseModel {
  String? id;

  BaseModel({this.id});
}
import 'package:intl/intl.dart';

import 'base.model.dart';
import 'custom_user.model.dart';
import 'media_memory.model.dart';
import 'memory.model.dart';
import 'written_memory.model.dart';

class LostPerson extends BaseModel {
  final String name;
  final String? nickname;
  final DateTime? dateOfDeath;
  final String? memorialLocation;
  final String? profileImageUrl;
  List<MediaMemory> medias;
  final List<WrittenMemory> notes;
  final CustomUser? user;
  String? _userId;

  LostPerson({
    super.id,
    required this.name,
    this.nickname,
    this.dateOfDeath,
    this.memorialLocation,
    this.profileImageUrl,
    this.user,
    List<MediaMemory>? medias,
    List<WrittenMemory>? notes,
    String? userId,
  }) : medias = medias ?? [],
       notes = notes ?? [],
       _userId = userId;

  set userId(String? value) => _userId = value;

  String? get userId => user?.id ?? _userId;

  String get displayName => nickname ?? name;

  String? get displayDateOfDeath =>
      dateOfDeath != null ? DateFormat.yMMMMd().format(dateOfDeath!) : null;

  List<Memory> get memories => [...medias, ...notes];

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'nickname': nickname,
      'dateOfDeath': dateOfDeath?.toIso8601String(),
      'memorialLocation': memorialLocation,
      'profileImageUrl': profileImageUrl,
      'userId': userId,
    };
  }

  static LostPerson fromJson({
    required String id,
    required Map<String, dynamic> json,
  }) {


When creating an object, I improved it to only send the necessary information. In other words, only the key (id) that establishes the relationship is sent.

{
      'name': name,
      'nickname': nickname,
      'dateOfDeath': dateOfDeath?.toIso8601String(),
      'memorialLocation': memorialLocation,
      'profileImageUrl': profileImageUrl,
      'userId': userId,
    };


LostPerson and MediaMemory have a 1:N relationship. However, our Firebase does not provide a join. Therefore, we have no choice but to query them in parallel.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_starter/repositories/media_memory.repository.dart';
import 'package:flutter_starter/services/providers/media_memory.provider.dart';

import '../../models/lost_person.model.dart';
import '../../repositories/lost_person.repository.dart';
import 'firebase.provider.dart';

final lostPersonRepositoryProvider = Provider(
  (ref) => LostPersonRepository(ref.watch(firebaseFirestoreProvider)),
);

final lostPersonProvider =
    StateNotifierProvider<LostPersonController, AsyncValue<List<LostPerson>>>(
      (ref) => LostPersonController(
        auth: ref.watch(firebaseAuthProvider),
        repo: ref.watch(lostPersonRepositoryProvider),
        mediaRepo: ref.watch(mediaMemoryRepositoryProvider),
      ),
    );

class LostPersonController extends StateNotifier<AsyncValue<List<LostPerson>>> {
  LostPersonController({
    required FirebaseAuth auth,
    required LostPersonRepository repo,
    required MediaMemoryRepository mediaRepo,
  }) : _auth = auth,
       _repo = repo,
       _mediaRepo = mediaRepo,
       super(const AsyncValue.loading());

  final FirebaseAuth _auth;
  final LostPersonRepository _repo;
  final MediaMemoryRepository _mediaRepo;

  LostPerson? getById(String id) {
    return state.maybeWhen(
      data: (list) => list.firstWhere((p) => p.id == id),
      orElse: () => null,
    );
  }

  Future<void> fetchAll() async {
    final uid = _auth.currentUser?.uid;

    try {
      final lostPersons = await _repo.fetchByUserId(uid!);
      final mediaLists = await Future.wait(
        lostPersons.map((p) => _mediaRepo.fetchByLostPersonId(p.id!)),
      );

      for (var i = 0; i < lostPersons.length; ++i) {
        lostPersons[i].medias = mediaLists[i];
      }

      state = AsyncValue.data(lostPersons);
    } catch (e, st) {
      state = AsyncValue.error(e, st);
    }
  }

  Future<void> createOne(LostPerson person) async {
    try {
      final uid = _auth.currentUser?.uid;
      person.userId = uid;

      await _repo.createOne(person);
      await fetchAll();
    } catch (e, st) {
      state = AsyncValue.error(e, st);
    }
  }
}

Here, we inject the necessary repositories. LostPersonRepository and MediaMemoryRepository each query data, and we combine them to process it for use in the object.

The important thing here is that each repository queries from its respective table, and there is a logic in the provider that combines them. Each repository wraps up with its own responsibility to avoid mixing.

This way, a clean and maintainable structure is completed.

Comments

Comments

Comments

Create a free website with Framer, the website builder loved by startups, designers and agencies.