flutterdartdependency-injectioninjectableget-it

Proper way to dispose repositories with get_it and injectable in Flutter/Dart?


I'm implementing a dependency cleanup system in Flutter using get_it and injectable where certain repositories need to be disposed when the user logs out. I've created a Disposable pattern but want to ensure I'm doing it correctly.

I've tried something like this, but does not work


abstract class Disposable {
  void dispose();
}

@lazySingleton
class RepoA extends Disposable {
  void printHello() {
    print('hello');
  }

  @override
  void dispose() {
    print('dispose');
  }
}

@lazySingleton
class RepoB extends Disposable {
  void printHelloFromRepoB() {
    print('hello');
  }

  @override
  void dispose() {
    print('dispose');
  }
}

@injectable
class UseCase {
  UseCase(
    this.repoB,
    this.repoA,
  );

  final RepoB repoB;
  final RepoA repoA;
}

void disposeAllDisposables() {
  final disposables =
      getIt.getAll<Disposable>(); // Disposable is not registered

  for (var disposable in disposables) {
    disposable.dispose();
  }
}

Solution

  • First of all, Disposable is a member of the get_it package and the package already knows how to handle classes that extend it, so you shouldn't be reinventing it. (Maybe you only included it here for demonstration purposes, but it still needs to be said.)

    Having said that, now on to the frame challenge:

    Doing it the way you are describing is certainly possible, but it's cumbersome, overrides a lot of type-safety, and requires manual management of the disposable singletons. Instead, I would recommend instead using get_it's scope system:

    // Login logic
    void login(String username, String password) {
      ... // Whatever other login logic you are doing
    
      GetIt.pushNewScope(
        scopeName: username,
        init: (scopedGI) {
          // Note: The dispose parameters are optional if RepoA or RepoB extend Disposable
          scopedGi.registerLazySingleton<RepoA>(() => RepoA(), dispose: (repoA) => repoA.dispose());
          scopedGi.registerLazySingleton<RepoB>(() => RepoB(), dispose: (repoB) => repoB.dispose());
        },
      );
    }
    
    //Logout logic
    void logout(String username) {
      ... // Whatever other logout logic you are doing
    
      GetIt.popScope();
      // or
      GetIt.dropScope(username);
    }
    

    Note: you can use this approach with injectable, but injectable's support for scopes seems somewhat limited and doesn't support scopes with dynamic names. You could use a static scope name instead (though personally I would prefer the dynamic approach just for the added security of each user having their own scope so there's no unintended crosstalk).

    @lazySingleton
    @Scope('user')
    class RepoA {
      void printHello() {
        print('hello');
      }
    
      @disposeMethod
      void dispose() {
        print('dispose');
      }
    }
    
    @lazySingleton
    @Scope('user')
    class RepoB {
      void printHelloFromRepoB() {
        print('hello');
      }
    
      @disposeMethod
      void dispose() {
        print('dispose');
      }
    }
    
    ...
    
    // Login logic
    void login(String username, String password) {
      ... // Whatever other login logic you are doing
    
      GetIt.initUserScope();
    }
    
    //Logout logic
    void logout(String username) {
      ... // Whatever other logout logic you are doing
    
      GetIt.popScope();
    }
    

    (As a personal note, the scope functionality of injectable is poorly documented and has no first-party test or example code that I can find, so I would personally prefer using the vanilla get_it approach here.)