Search code examples
flutterdartflutter-isar

flutter_isar saving models with nested Links not working


I am trying to save a model that has IsarLinks which also each have an IsarLink. Saving IsarLinks (sets as WorkoutSets) works but the IsarLink(exercise as Exercise) inside is not persisted. Following is the method to save the models as well as the models themselves.

///contained in a separate class
 static saveWorkout(Workout workout) async {
    await isar!.writeTxn(() async {
      List<Exercise> exercises = workout.workoutExercises.toList();
      await isar!.workouts.put(workout);
      for (Exercise exercise in exercises) {
        await isar!.exercises.put(exercise);
        workout.exercises.add(exercise);
      }

      for (Exercise exercise in exercises) {
        List<WorkoutSet> sets = exercise.sets.toList();
        for (WorkoutSet set in sets) {
          await isar!.workoutSets.put(set); // Save set with link
          set.exercise.value = exercise; // Link set to exercise
          await set.exercise.save();
          workout.sets.add(set); // Prepare to link set to workout
        }
      }

      await workout.sets.save();
      await workout.exercises.save();
      
    });
  }


@Collection(inheritance: false)
class Workout extends Equatable {
  final String id;
  Id get isarId => Crypto.fastHash(id);
  IsarLink<WorkoutTemplate> template = IsarLink<WorkoutTemplate>();

  @override
  @ignore
  List<Object?> get props => [
        id,
        performedOn,
        durationSeconds,
        sets,
        workoutExercises,
        template,
      ];

  @Index()
  DateTime performedOn;
  int durationSeconds = 0;

  Workout({
    required this.id,
    required this.performedOn,
    List<Exercise>? workoutExercises, // Allow passing exercises to constructor
  }) : workoutExercises =
            workoutExercises ?? List<Exercise>.empty(growable: true);

  final sets = IsarLinks<WorkoutSet>();

  ///used only for finding workout by exercises used in the workout e.g. for workout history
  final exercises = IsarLinks<Exercise>();

  @ignore
  final List<Exercise> workoutExercises;

  @ignore
  get totalWeightLifted {
    return sets.fold<double>(
        0, (previousValue, element) => previousValue + (element.weight ?? 0));
  }

  void prepareForSaving() {
    template.value?.exercises = workoutExercises;
  }

  Workout clone() {
    var templateClone = IsarLink<WorkoutTemplate>();
    if (template.value != null) {
      templateClone.value = template.value; // Adjust according to your needs
    }
    List<Exercise> exercisesClone =
        workoutExercises.map((e) => e.clone()).toList();
    return Workout(
      id: id,
      performedOn: DateTime.fromMillisecondsSinceEpoch(
          performedOn.millisecondsSinceEpoch),
      workoutExercises: exercisesClone,
    )
      ..durationSeconds = durationSeconds
      ..template = templateClone;
  }

  addExercises(List<Exercise> exercises) async {
    for (var exercise in exercises) {
      var newExercise = exercise.clone();
      newExercise.sets.add(WorkoutSet(performedAs: 0, exercise: exercise));
      workoutExercises.add(newExercise);
    }
  }

  addSet(int exerciseIndex) async {
    workoutExercises[exerciseIndex].sets.add(WorkoutSet(
        performedAs: workoutExercises[exerciseIndex].sets.length,
        exercise: workoutExercises[exerciseIndex]));
  }

  removeSet(int exerciseIndex, int setIndex) {
    workoutExercises[exerciseIndex].sets.removeAt(setIndex);
    for (final (index, set) in workoutExercises[exerciseIndex].sets.indexed) {
      set.performedAs = index;
    }
  }

  Exercise? getExercise(int index) {
    var id = template.value?.exerciseIds.elementAtOrNull(index);
    if (id == null) {
      return null;
    }
    return Database.isar!.exercises
        .where()
        .idEqualTo(int.parse(id))
        .findFirstSync();
  }

  factory Workout.empty() {
    return Workout(
      id: Uuid().v4(),
      performedOn: DateTime.now(),
    );
  }
}


@Collection(inheritance: false)
class WorkoutSet extends Equatable {
  WorkoutSet({
    this.performedOn,
    this.weight,
    this.reps,
    this.type = SetType.work,
    required this.performedAs,
    this.completed = false,
    this.note,
    this.isChecked = false,
    this.rpe,
    this.restTimer,
    this.autoRestTimer,
    Exercise? exercise,
  }) : uuid = Uuid().v4() {
    this.exercise.value = exercise;
  }

  @override
  @ignore
  List<Object?> get props => [
        performedOn,
        weight,
        reps,
        type,
        performedAs,
        completed,
        note,
        isChecked,
      ];

  WorkoutSet clone() {
    return WorkoutSet(
      performedOn:
          performedOn, // DateTime is immutable in Dart, so direct assignment is okay
      weight: weight,
      reps: reps,
      type: type, // Enums are immutable
      performedAs: performedAs,
      // Add the rest of the properties following the same pattern
      rpe: rpe,
      restTimer: restTimer,
      autoRestTimer: autoRestTimer,
      completed: completed,
      note: note,
      isChecked: isChecked,
      exercise: exercise.value,
    )
      ..id = id
      // ..exercise = exercise
      ..uuid = uuid;
  }

  // LINK PR
  Id id = Isar.autoIncrement;
  String uuid;
  IsarLink<Exercise> exercise = IsarLink<Exercise>();
  @enumerated
  SetType type;
  int? weight;
  int? reps;
  int? rpe; // e.g. 8.5 = 85
  int? restTimer; // in seconds e.g. 300 = 5 Minutes
  int? autoRestTimer;
  bool completed = false;
  String? note;
  bool isChecked = false;
  DateTime? performedOn;
  int performedAs; // represents,  when - during a number of sets of the same exercise - this set was performed
}

enum SetType {
  work,
  warmup,
  dropSet,
  failure,
}

extension Utils on SetType {
  Color color(BuildContext context) {
    switch (this) {
      case SetType.work:
        return Theme.of(context).colorScheme.secondaryContainer;
      case SetType.warmup:
        return Colors.yellow.shade200;
      case SetType.dropSet:
        return Colors.purple.shade200;
      case SetType.failure:
        return Colors.redAccent.shade100;
      default:
        return Theme.of(context).colorScheme.secondaryContainer;
    }
  }

  Color textColor(BuildContext context) {
    switch (this) {
      case SetType.work:
        return Theme.of(context).colorScheme.onSurface;
      case SetType.warmup:
        return Colors.yellow.shade800;
      case SetType.dropSet:
        return Colors.purple.shade800;
      case SetType.failure:
        return Colors.redAccent.shade700;
      default:
        return Theme.of(context).colorScheme.secondaryContainer;
    }
  }
}

extension Formatting on SetType {
  String format() {
    return name.toUpperCase();
  }
}

enum BarType {
  olympic,
  short,
  ez,
  hex,
  none,
}

enum UnitType {
  metric,
  imperial,
}



I have tried putting the workout again after saving, but this doesn't change anything.


Solution

  • Tried a lot of variations, none of which worked. Ended up simply storing the ID of the desired Exercise and implementing a getter for it instead of using a Link.