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.