realm / realm-dart

Realm is a mobile database: a replacement for SQLite & ORMs.
Apache License 2.0
758 stars 86 forks source link

Message: Access to invalidated Collection object #1133

Closed ffelicioautodoc closed 1 year ago

ffelicioautodoc commented 1 year ago

What happened?

When executing the deletion process of a record (via the Atlas platform or the process on the device), the message described is returned.

Here we have updated to realm version: ^0.10.0+rc, but the error occurs since version 8

As you can see below, the records exist in the Atlas console, in Realm Studio and are being presented in the interface.

To interconnect the interface with the data, we are using the flutter_bloc lib.

[ATLAS - CONSOLE] atlas-console

[REALM STUDIO] realm-studio

[UI] ScreenshotUNITO-UNDERSCORE!1675105757!

Realm Flutter Version: realm: ^0.10.0+rc

Repro steps

When deleting a record via the atlas platform or any action taken on the device, the following error is generated:

[GENERATED ERROR] error-reporting

Version

Flutter Version: Flutter 3.7.0 / Dart: Dart 2.19.0

What Atlas Services are you using?

Atlas Device Sync

What type of application is this?

Flutter Application

Client OS and version

Android 8 / Android 10 / Android 13 (emulator) / Iphone 12 / Iphone 14

Code snippets

abstract class ActivitiesDataSource {
  Future<ActivitiesModel> saveActivity({required ActivitiesModel activitiesModel});
  Stream<List<ActivitiesModel>> getActivitiesList({required String rdoId});
  Future<bool> deleteActivity({required ActivitiesModel activitiesModel});
}

// usage with injectable/getIt
@LazySingleton(as: ActivitiesDataSource)
class ActivitiesDataSourceImpl implements ActivitiesDataSource {
  final Realm _realm;

  ActivitiesDataSourceImpl(@Named(DatabaseConstants.rdo) this._realm);

  @override
  Future<ActivitiesModel> saveActivity({required ActivitiesModel activitiesModel}) async {
    try {
      Activities activitiesRealm = activitiesModel.toRealm();
      _realm.write<Activities>(() => _realm.add<Activities>(activitiesRealm, update: true));
      return Future.value(ActivitiesModel.fromRealm(activitiesRealm));
    } on RealmException catch (exception) {
      throw rdo_exception.RealmException(message: exception.message);
    }
  }

  // find results by params
  final StreamController<List<ActivitiesModel>> _streamController = StreamController<List<ActivitiesModel>>.broadcast();
  @override
  Stream<List<ActivitiesModel>> getActivitiesList({ required String rdoId }) async* {
    try {
      final Realm _realm = getIt<Realm>(instanceName: DatabaseConstants.rdo);

      final RealmResults<Activities> activitiesChange = _realm.query<Activities>(r'rdo_id = $0', [ObjectId.fromHexString(rdoId)]);
      final activitiesModelList = activitiesChange.changes.map((event) => event
          .results
          .toList()
          .map((activity) => ActivitiesModel.fromRealm(activity))
          .toList());

      activitiesModelList.listen((event) {
        _streamController.add(event);
      });
    } on RealmException catch (exception) {
      _streamController.addError(
        rdo_exception.RealmException(message: exception.message),
      );
    }
    yield* _streamController.stream;
  }

  // delete proccess
  @override
  Future<bool> deleteActivity({required ActivitiesModel activitiesModel}) {
    try {
      final Activities? realmActivities = _realm.find<Activities>(ObjectId.fromHexString(activitiesModel.idModel!));

      if (realmActivities == null) {
        throw const rdo_exception.RealmException(message: ErrorMessageConstants.objectNotFound);
      }

      _realm.write(() => _realm.delete<Activities>(realmActivities));

      return Future.value(true);
    } on RealmException catch (exception) {
      throw rdo_exception.RealmException(message: exception.message);
    }
  }
}

// BLOC
@injectable
class ActivitiesListBloc
    extends Bloc<ActivitiesListEvent, ActivitiesListState> {
  final GetActivitiesListUsecase _getActivitiesListUsecase;

  ActivitiesListBloc({
    required GetActivitiesListUsecase getActivitiesListUsecase,
  })  : _getActivitiesListUsecase = getActivitiesListUsecase,
        super(const ActivitiesListState()) {
    on<ActivitiesGetListEvent>(_getActivitiesList);
  }

  Future<void> _getActivitiesList(
      ActivitiesGetListEvent event, Emitter<ActivitiesListState> emit) async {
    emit(state.copyWith(status: StatusEnum.loading));

    await emit.forEach<List<ActivitiesEntity>>(
        _getActivitiesListUsecase(ParamsActivitiesList(rdoId: event.rdoId)),
        onData: (activities) => state.copyWith(
              status:
                  activities.isNotEmpty ? StatusEnum.success : StatusEnum.empty,
              activitiesList: activities,
            ),
        onError: (_, __) => state.copyWith(status: StatusEnum.failure));
  }
}

Stacktrace of the exception/crash you're getting

[log] onError(ActivitiesListBloc, RealmException: Error code: 7 . Message: Access to invalidated Collection object, #0      _RealmCore.throwLastError.<anonymous closure>
package:realm/…/native/realm_core.dart:119
#1      using
package:ffi/src/arena.dart:124
#2      _RealmCore.throwLastError
package:realm/…/native/realm_core.dart:113
#3      _RealmLibraryEx.invokeGetBool
package:realm/…/native/realm_core.dart:2730
#4      _RealmCore.getListSize.<anonymous closure>
package:realm/…/native/realm_core.dart:1106
#5      using
package:ffi/src/arena.dart:124
#6      _RealmCore.getListSize
package:realm/…/native/realm_core.dart:1104
#7      ManagedRealmList.length
package:realm/src/list.dart:72
#8      new ListIterator (dart:_internal/iterable.dart:329:28)
#9      ListMixin.iterator (dart:collection/list.dart:76:31)
#10     StringBuffer.writeAll (dart:core-patch/string_buffer_patch.dart:95:33)
#11     IterableBase.iterableToFullString (dart:collection/iterable.dart:269:14)
#12     ListMixin.toString (dart:collection/list.dart:588:37)
#13     mapPropsToString.<anonymous closure>
package:equatable/src/equatable_utils.dart:72
#14     MappedListIterable.elementAt (dart:_internal/iterable.dart:415:31)
#15     ListIterable.join (dart:_internal/iterable.dart:156:22)
#16     mapPropsToString
package:equatable/src/equatable_utils.dart:72
#17     EquatableMixin.toString
package:equatable/src/equatable_mixin.dart:38
#18     _StringBase._interpolateSingle (dart:core-patch/string_patch.dart:829:17)
#19     StringBuffer.write (dart:core-patch/string_buffer_patch.dart:64:24)
#20     StringBuffer.writeAll (dart:core-patch/string_buffer_patch.dart:105:9)
#21     IterableBase.iterableToFullString (dart:collection/iterable.dart:269:14)
#22     ListBase.listToString (dart:collection/list.dart:43:20)
#23     List.toString (dart:core-patch/growable_array.dart:502:33)
#24     mapPropsToString.<anonymous closure>
package:equatable/src/equatable_utils.dart:72
#25     MappedListIterable.elementAt (dart:_internal/iterable.dart:415:31)
#26     ListIterable.join (dart:_internal/iterable.dart:149:25)
#27     mapPropsToString
package:equatable/src/equatable_utils.dart:72
#28     Equatable.toString
package:equatable/src/equatable.dart:64
#29     _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19)
#30     Change.toString
package:bloc/src/change.dart:31
#31     _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19)
#32     RDOBlocObserver.onChange
package:rdo/presentation/bootstrap.dart:13
#33     BlocBase.onChange
package:bloc/src/bloc_base.dart:133
#34     BlocBase.emit
package:bloc/src/bloc_base.dart:99
#35     Bloc.emit
package:bloc/src/bloc.dart:153
#36     Bloc.on.<anonymous closure>.onEmit
package:bloc/src/bloc.dart:208
#37     _Emitter.call
package:bloc/src/emitter.dart:133
#38     _Emitter.forEach.<anonymous closure>
package:bloc/src/emitter.dart:102
#39     _rootRunUnary (dart:async/zone.dart:1406:47)
#40     _CustomZone.runUnary (dart:async/zone.dart:1307:19)
#41     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1216:7)
#42     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#43     _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7)
#44     _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:774:19)
#45     _StreamController._add (dart:async/stream_controller.dart:648:7)
#46     _rootRunUnary (dart:async/zone.dart:1406:47)
#47     _CustomZone.runUnary (dart:async/zone.dart:1307:19)
#48     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1216:7)
#49     _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#50     _DelayedData.perform (dart:async/stream_impl.dart:515:14)
#51     _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11)
#52     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:591:7)
#53     _rootRun (dart:async/zone.dart:1390:47)
#54     _CustomZone.run (dart:async/zone.dart:1300:19)
#55     _CustomZone.runGuarded (dart:async/zone.dart:1208:7)
#56     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1248:23)
#57     _rootRun (dart:async/zone.dart:1398:13)
#58     _CustomZone.run (dart:async/zone.dart:1300:19)
#59     _CustomZone.runGuarded (dart:async/zone.dart:1208:7)
#60     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1248:23)
#61     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#62     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
      )
[log] RealmException: Error code: 7 . Message: Access to invalidated Collection object

Relevant log output

[log] RealmException: Error code: 7 . Message: Access to invalidated Collection object
nielsenko commented 1 year ago

Your code is trying to get the length of a realm list on a realm object that is already dead, ie. deleted in the database.

If you need to access data on an object after it is deleted, consider freezing it before deleting it. That way you can still access the old state.

Otherwise, re-structure your code to avoid accessing deleted realm objects.

ffelicioautodoc commented 1 year ago

Good afternoon @nielsenko

Thank you very much for answering.

Sorry for my lack of intelligence, but when you say:

Your code is trying to get the length of a realm list on a realm object that is already dead, ie. deleted in the database.

If you need to access data on an object after it is deleted, consider freezing it before deleting it. That way you can still access the old state.

Otherwise, re-structure your code to avoid accessing deleted realm objects.

What would that be?

Do you have any examples to point out?

What left us confused is that, we do this same process in the other objects of the project and the problem is not generated, only in this resource that the error is sent to us.

nielsenko commented 1 year ago
import 'package:realm_dart/realm.dart'; // using plain dart instead of flutter here, but concept is the same

part 'example.g.dart'; // assuming this file is example.dart

@RealmModel()
class _Stuff {
  late int id;
}

final realm = Realm(Configuration.local([Stuff.schema]));

void main(List<String> arguments) {
  final x = realm.write(() => realm.add(Stuff(1)));
  final y = x.freeze();
  realm.write(() => realm.delete(x));

  print(y.id); // <-- safe since y was frozen (ie. point to old version of x)
  print(x.id); // <-- throws since x is deleted from realm.
}

outputs:

1
Unhandled exception:
RealmException: Error getting property Stuff.id Error: RealmException: Error code: 7 . Message: Accessing object of type Stuff which has been invalidated or deleted
#0      RealmCoreAccessor.get (package:realm_dart/src/realm_object.dart:213:7)
#1      RealmObjectBase.get (package:realm_dart/src/realm_object.dart:316:29)
...
nielsenko commented 1 year ago

In package:rdo/presentation/bootstrap.dart:13 you have hooked up some RDOBlocObserver.onChange that eventually ends up accessing the length of a realm list on a dead realm object while constructing an iterator.

You didn't show me that code, but that is where the stack trace leaves your code, as far as I can tell.

nielsenko commented 1 year ago

BTW, in:

final activitiesModelList = activitiesChange.changes.map((event) => event
          .results
          .toList() // <-- this hydrates the full list
          .map((activity) => ActivitiesModel.fromRealm(activity)) // <-- this hydrates every object
          .toList());

you are loosing a lot of the benefits of realm in my opinion.

After that you have a stream of fully instantiated List<ActivitiesModel>s. Nothing is loaded lazy after here, if I guess the implementation of ActivitiesModel.fromRealm correctly.

If your lists are small (and since realm is fast) it will work, but consider constructing bloc/view model like objects lazily, and don't copy out properties from your realm objects, but access them on demand.

nielsenko commented 1 year ago

In the short term you could consider guarding against accessing dead realm objects by sprinkling strategic if (x.isValid) (where x is a realm object) throughout your code, but it becomes messy fast.

...
  print(y.id); // <-- safe since y was frozen (ie. point to old version of x)
  if (!x.isValid) print('x is dead'); // <-- safe to call isValid on dead objects
  print(x.id); // <-- throws since x is deleted from realm.
...
ffeliciodeveloper commented 1 year ago

In package:rdo/presentation/bootstrap.dart:13 you have hooked up some RDOBlocObserver.onChange that eventually ends up accessing the length of a realm list on a dead realm object while constructing an iterator.

You didn't show me that code, but that is where the stack trace leaves your code, as far as I can tell.

Good Morning @nielsenko!

I'm sorry for the delay in responding.

So this resource is just a bloc observer that helps us map the flows. We are currently enjoying the benefits of the stream that the realm lib gives us together with the features of flutter_bloc.

class RDOBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    log('onChange(${bloc.runtimeType}, $change)'); // <- this is line 13 in the code
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    log('onError(${bloc.runtimeType}, $error, $stackTrace)');
    super.onError(bloc, error, stackTrace);
  }
}
ffeliciodeveloper commented 1 year ago

BTW, in:

final activitiesModelList = activitiesChange.changes.map((event) => event
          .results
          .toList() // <-- this hydrates the full list
          .map((activity) => ActivitiesModel.fromRealm(activity)) // <-- this hydrates every object
          .toList());

you are loosing a lot of the benefits of realm in my opinion.

After that you have a stream of fully instantiated List<ActivitiesModel>s. Nothing is loaded lazy after here, if I guess the implementation of ActivitiesModel.fromRealm correctly.

If your lists are small (and since realm is fast) it will work, but consider constructing bloc/view model like objects lazily, and don't copy out properties from your realm objects, but access them on demand.

So in this case, would it be better to use the model classes that Realm provides us?

This class you showed is a mapper that transforms the Realm class into a class to be used in our domain.

I'll leave below the implementation of this class that you indicated:

class ActivitiesModel extends ActivitiesEntity with EquatableMixin {
  final String? idModel;
  final String rdoIdModel;
  final String? statusModel;
  final List<String>? placeModel;
  final String? descriptionModel;
  final AuditModel? createdAtModel;
  final AuditModel? updatedAtModel;

  ActivitiesModel({
    this.idModel,
    required this.rdoIdModel,
    this.statusModel,
    this.placeModel,
    this.descriptionModel,
    this.createdAtModel,
    this.updatedAtModel,
  }) : super(
          id: idModel,
          rdoId: rdoIdModel,
          status: statusModel,
          place: placeModel,
          description: descriptionModel,
          createdAt: createdAtModel,
          updatedAt: updatedAtModel,
        );

  // mapper that turns the Realm object into a managed class in the project
  factory ActivitiesModel.fromRealm(Activities activities) {
    return ActivitiesModel(
      idModel: activities.id.hexString,
      rdoIdModel: activities.rdoId.hexString,
      statusModel: activities.status,
      placeModel: activities.place,
      descriptionModel: activities.description,
      createdAtModel: activities.createdAt != null
          ? AuditModel.fromRealm(activities.createdAt!)
          : null,
      updatedAtModel: activities.updatedAt != null
          ? AuditModel.fromRealm(activities.updatedAt!)
          : null,
    );
  }

  // mapper that transforms the domain class to a Realm object
  // It is used for create/update/delete operations
  Activities toRealm() {
    return Activities(
      ObjectId.fromHexString(idModel ?? ObjectId().hexString),
      ObjectId.fromHexString(rdoIdModel),
      status: statusModel,
      place: placeModel ?? [],
      description: descriptionModel,
      createdAt: createdAtModel?.toRealm(),
      updatedAt: updatedAtModel?.toRealm(),
    );
  }

  factory ActivitiesModel.fromEntity(ActivitiesEntity activitiesEntity) {
    return ActivitiesModel(
      idModel: activitiesEntity.id,
      rdoIdModel: activitiesEntity.rdoId,
      statusModel: activitiesEntity.status,
      placeModel: activitiesEntity.place,
      descriptionModel: activitiesEntity.description,
      createdAtModel: activitiesEntity.createdAt == null
          ? null
          : AuditModel.fromEntity(activitiesEntity.createdAt!),
      updatedAtModel: activitiesEntity.updatedAt == null
          ? null
          : AuditModel.fromEntity(activitiesEntity.updatedAt!),
    );
  }

  ActivitiesModel copyWith({
    String? id,
    String? rdoId,
    String? period,
    String? weather,
    String? condition,
    String? description,
    List<String>? place,
    AuditModel? createdAt,
    AuditModel? updatedAt,
  }) {
    return ActivitiesModel(
      idModel: id ?? idModel,
      rdoIdModel: rdoId ?? rdoIdModel,
      statusModel: status ?? statusModel,
      placeModel: place ?? placeModel,
      descriptionModel: description ?? descriptionModel,
      createdAtModel: createdAt ?? createdAtModel,
      updatedAtModel: updatedAt ?? updatedAtModel,
    );
  }

  @override
  List<Object?> get props => [
        idModel,
        rdoIdModel,
        statusModel,
        placeModel,
        descriptionModel,
        createdAtModel,
        updatedAtModel,
      ];
}
nielsenko commented 1 year ago

Regarding the line 13 in RDOBlocObserver and looking at the stacktrace:

...
#30     Change.toString
package:bloc/src/change.dart:31
#31     _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19)
#32     RDOBlocObserver.onChange
...

It is the $change that evokes a Change.toString() that tries to describe before and after. But here the realm list is already dead, so that fails.

Regarding ActivitiesModel.fromRealm it is as I suspected. Note how you are accessing every property of the realm object in the factory constructor, and copying the values out into a new ActivitiesModel object. This means you have defeated the lazy loading that happens when accessing the properties of a realm object.

It is not that it won't work, but it is working a bit against the grain of realm, I think.

I hope you got a few pointers to work on. Good luck with your project @ffelicioautodoc!

ffeliciodeveloper commented 1 year ago

Regarding the line 13 in RDOBlocObserver and looking at the stacktrace:

...
#30     Change.toString
package:bloc/src/change.dart:31
#31     _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19)
#32     RDOBlocObserver.onChange
...

It is the $change that evokes a Change.toString() that tries to describe before and after. But here the realm list is already dead, so that fails.

Regarding ActivitiesModel.fromRealm it is as I suspected. Note how you are accessing every property of the realm object in the factory constructor, and copying the values out into a new ActivitiesModel object. This means you have defeated the lazy loading that happens when accessing the properties of a realm object.

It is not that it won't work, but it is working a bit against the grain of realm, I think.

I hope you got a few pointers to work on. Good luck with your project @ffelicioautodoc!

Thanks a lot for your support @nielsenko.

As you yourself described, code review actions were carried out in order to find the problem.

I will review with the team about the possibility of directly using classes generated by Realm. As you explained, we are losing the benefits that the library gives us.

As we use the design following the Clean Architecture pattern, we don't think about the resources delivered by Realm objects. We ended up following the pattern as if it were using an api.

In this case, would it be better for our entire project to only use Realm classes (without using mappers/converters)? Was that really what you meant?

I apologize for the problem created and again, thank you for your support and your team in helping with this task.

nielsenko commented 1 year ago

In this case, would it be better for our entire project to only use Realm classes (without using mappers/converters)? Was that really what you meant?

Well, don't introduce mappers just because that is how you would normally do with a database. There are valid reasons to have wrappers, but use them sparsely and try to construct them lazily when you do, ie. when accessing index i of a list, instead of copying the entire list eagerly.

I don't know you project in detail, so it would be a bit presumptuous of me to say don't use mappers, but at least be careful when using them together with Realm.

ebelevics commented 1 year ago

I would like to continue this conversation with mappers, because I stumbled with same problem. I noticed especially when I have large list of data objects that are mapped, there application loses performance hard, reasons you stated above -> objects are not load lazily but every value is read and converter.

I have just no idea how to handle this problem correctly, can you show code example, what do you mean by "when accessing index i of a list, instead of copying the entire list eagerly.". I need app objects from realm for several reasons:

I wanted to find maybe there are ways how I can map all list to from realm objects to app objects lazily, so that I didn't lose lazy realm object benefits, but again I don't know if and how it's possible. Also I couldn't understand in app model how to handle multiple relationship lazy loading, so I don't load large chunks of data, if I load and map all objects that have multiple relationships.

I would be really thankful if you could provide solution of realm best practice to handle this problem.

ebelevics commented 1 year ago

I have just noticed .toList() is hydrating the full list but Iterable seems like not, meaning toList() affects performance hard, compared to leaving it just as Iterable, where performance is just slightly affected.

I have also noticed if realm object has relationship to multiple objects late List<_Person> drivers;, for some reason local.drivers.map((driver) => toPersonApp(driver)).toList(), doesn't lose any performance here, only lose performance if mapping RealmResults to List.

nielsenko commented 1 year ago

@ebelevics Yes, when calling toList on any implementor of List (including RealmList) you are copying all elements and creating a new dart List. This defeats all the nice lazy speed and space benefits of RealmList, and you also loose the ability to listen for changes.

It is a bit unclear what you are trying to say explain here:

I have also noticed if realm object has relationship to multiple objects late List<_Person> drivers;, for some reason local.drivers.map((driver) => toPersonApp(driver)).toList(), doesn't lose any performance here, only lose performance if mapping RealmResults to List.

In particular since I can only guess about toPersonApp. But perhaps you are asking why there is no difference in performance between:

for (final x in local.drivers.map((driver) => toPersonApp(driver)).toList()) {
  // do something ..
}

and

for (final d in local.drivers) {
  final x = toPersonApp(d);
  // do something ..
}

If so, then yes, there is no difference - the work is the same. You are iterating over the full list hydrating and mapping every thing in both cases.

But there is a (potentially big - depending on size of the list) difference between doing:

toPersonApp(local.drivers[424242]); // fetch handle to a single Person, then hydrates and maps it.

and

local.drivers.map((driver) => toPersonApp(driver)).toList()[424242]; // hydrates and maps every Person, then select a single.

First is O(1) second is O(n) where n is the size of local.drivers. It obviously depends on the size of lists if this is actually something your users will feel.

Now to your original question..

Regarding non-realm types such as Color you need to do the mapping, fx:

@RealmModel()
class _Pen {
  @MapTo('color')
  late int colorAsInt;

  Color get color => Color(colorAsInt);
  set color(Color c) => colorAsInt = c.value;
}

I hope we can make this more convenient later.

The non-nullable relations is impossible for us in general, since we cannot prevent some part of your system from deleting the objects pointed to. But if you can guarantee that won't happen, you can do something similar as with Color above, and stripping the nullability:

@RealmModel()
class _Line {
  @MapTo('pen')
  late _Pen? penBacking;

  _Pen get pen => penBacking!;
  set pen(_Pen p) => penBacking = p;
}

If it is worth the anxiety it to prevent !. anxiety .. maybe, maybe not.

Regarding, separation of database layer, and clean architecture.. Introducing abstractions to easily replace core components in your app is a bit overused in my opinion. You wouldn't abstract away the fact that your are using flutter, just in case a different UI framework come along for Dart at one point. More to the point, the common abstraction layer we all learned to hide the database behind does not play well with realm, as you are starting to realise. In general I suggest you model your entities directly as realm objects, and don't don't treat realm objects as just something to traffic data in and out of the database. This jives better with Realm.

I hope this helps a bit..

Anyway.. if you still prefer to map entities and results, and cannot live with the performance of all the copying you have introduced, then you need to start looking a writing lazy list wrappers. Here is a simple one:

class LazyMappedList<From, To> with ListMixin<To> {
  final List<From> _source;
  final To Function(From) _mapper;
  final From Function(To)? _reverseMapper;

  LazyMappedList(this._source, this._mapper, [this._reverseMapper]);

  @override
  int get length => _source.length;

  @override
  set length(int newLength) => _source.length = newLength; // this is likely to cause trouble..

  @override
  To operator [](int index) {
    return _mapper(_source[index]);
  }

  @override
  void operator []=(int index, To value) {
    if (_reverseMapper == null) {
      throw UnsupportedError('Cannot set value of a lazy mapped list');
    }
    _source[index] = _reverseMapper!(value);
  }
}

This is of the top of my head, haven't tested it at all..

BTW, it is better (for me/us at least) to open a new issue, instead of piggy-backing on an old one. Even if the question is closely related. You can always refer to the previous issue from the new one.