modular icon indicating copy to clipboard operation
modular copied to clipboard

class that bind with singleton in core shared module should not recreate

Open fullflash opened this issue 2 years ago • 16 comments

We need as described in docs.

class AppModule extends Module { @override List<Module> get imports => [CoreModule()];

and inside core module injecting Dio instance as in examples

but when getting this httclient via Modular.get() the dio instance class construction called again.

so what is should we do in order to get a real singleton binding ?

fullflash avatar Sep 21 '22 04:09 fullflash

Show more parts of your code so we can reproduce the issue

eduardoflorence avatar Sep 21 '22 12:09 eduardoflorence

you can see in the screenshots Dio client instantiated two times on app start.

Screen Shot 2022-09-22 at 21 41 50 Screen Shot 2022-09-22 at 21 41 18

CoreModule that binds Dio client used only in AppModule imports

class AppModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

fullflash avatar Sep 22 '22 17:09 fullflash

I'm having the same issue with Modular 6.3.0.

Part of my code:

void main() async {
  runApp(
    ModularApp(
      module: AppModule(),
      child: const AppWidget(),
    ),
  );

  await Hive.initFlutter();
  await Modular.get<CacheLoginService>().openBox();
  await Modular.get<AuthController>().loadCachedUser();
  await Future.delayed(const Duration(seconds: 5));

  Modular.to.navigate('/login/');
}
class CoreModule extends Module {
  @override
  void exportedBinds(i) {
    i.addInstance(Dio());
    i.addLazySingleton<IClientHttp>(ClapsClientApi.new);
    i.addLazySingleton<AuthService>(AuthService.new);
    i.addLazySingleton<AuthController>(AuthController.new);
    i.addLazySingleton<EncryptedDBService>(EncryptedDBService.new);
    i.addLazySingleton<CacheLoginService>(CacheLoginService.new);
  }
}
class AppModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

  @override
  void routes(r) {
    ...
  }
}
class LoginModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

  @override
  void binds(i) {
    // new dependencies different from CoreModule
    ...
  }

  @override
  void routes(r) {
    ...
  }
}

image

Screenshot notes:

  • The # are the hashCode of each instance
  • The instances listed below SplashPage are Modular.get calls executed within main after runApp
  • The instances listed below of AccessLoginPage are dependencies from the Modular.get<AccessLoginController>() (last log) inside the page AccessLoginPage within the LoginModule.

guilherme-ma9 avatar Aug 31 '23 19:08 guilherme-ma9

Complete code to help test the issue. AuthServiceDefault is instantiated again on every import.

import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

void main() async {
  runApp(ModularApp(module: AppModule(), child: const AppWidget()));

  await Future.delayed(const Duration(seconds: 2));
  Modular.to.navigate('/auth/');
}

class Counter {
  int count = 0;

  Counter() {
    print('****INSTANCE: $runtimeType - $hashCode');
  }

  void add() {
    count++;
  }
}

class CoreModule extends Module {
  @override
  void exportedBinds(Injector i) {
    i.addSingleton<AuthService>(AuthServiceDefault.new);
    i.addSingleton(Counter.new);
  }
}

class AppModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

  @override
  void routes(RouteManager r) {
    r.child('/', child: (context) => const SplashScreen());
    r.module('/auth', module: AuthModule());
    r.module('/home', module: HomeModule());
  }
}

class AppWidget extends StatelessWidget {
  const AppWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: Modular.routerConfig,
    );
  }
}

class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Material(child: Center(child: CircularProgressIndicator.adaptive()));
  }
}

// Auth
class AuthModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

  @override
  void binds(Injector i) {
    i.addLazySingleton(AuthController.new);
  }

  @override
  void routes(RouteManager r) {
    r.child('/', child: (context) => LoginPage(authController: Modular.get()));
  }
}

abstract interface class AuthService {
  Future<bool> login();
}

class AuthServiceDefault implements AuthService {
  AuthServiceDefault() {
    print('***INSTANCE: $runtimeType - $hashCode');
  }

  @override
  Future<bool> login() async {
    await Future.delayed(const Duration(seconds: 1));
    return true;
  }
}

class AuthController {
  AuthController({required this.authService});
  final AuthService authService;

  Future<bool> login() async {
    return authService.login();
  }
}

class LoginPage extends StatelessWidget {
  const LoginPage({super.key, required this.authController});

  final AuthController authController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final counter = Modular.get<Counter>();
            counter.add();
            counter.add();
            counter.add();
            await authController.login();
            Modular.to.navigate('/home/');
          },
          child: const Text('Login'),
        ),
      ),
    );
  }
}

// Home
class HomeModule extends Module {
  @override
  List<Module> get imports => [CoreModule()];

  @override
  void routes(RouteManager r) {
    r.child('/', child: (context) => HomePage(counter: Modular.get()));
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key, required this.counter});

  final Counter counter;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('Home ${counter.count} ${counter.hashCode}')),
    );
  }
}

eduardoflorence avatar Sep 01 '23 20:09 eduardoflorence

Hi @guilherme-ma9,

In Modular 6.3.1 the problem with addLazySingleton in CoreModule is not happening. The problem happens if we use addSingleton, but it's just a side effect, as the first instance will be shared by all imports. Use my code above and you will see that the same Counter instance is being shared.

eduardoflorence avatar Sep 01 '23 21:09 eduardoflorence

Hi @eduardoflorence,

The problem still remains. I have a dev.log inside each constructor, which means that every time a new object of the class is instantiated, it will call this log.

log pubspec.yaml pubspec.lock

guilherme-ma9 avatar Sep 03 '23 14:09 guilherme-ma9

@guilherme-ma9, Can you provide a complete code for us to reproduce the problem? Or are you able to change the code I provided above to reproduce the problem? Did you see that although there is more than one instance, it is always the first one that is used in all imports?

eduardoflorence avatar Sep 04 '23 11:09 eduardoflorence

@guilherme-ma9 try to log your binds instead your imported modules. The imported modules are instantiated again, but there is a validation that gets the first instance created. The instance that wasn't used is removed by the Garbage Collector.

davidsdearaujo avatar Sep 06 '23 16:09 davidsdearaujo

It's working normally now

guilherme-ma9 avatar Sep 12 '23 13:09 guilherme-ma9

Hello, How do you apply this same concept for version 5? I'm trying to replicate but its telling me that "The method doesn't override an inherited method."

I tried with both: List<Bind> get exportedBinds => [ Bind.instance(AuthenticationManager.new), ];

and

@override void exportedBinds(i) { i.addLazySingleton<AuthenticationManager>(AuthenticationManager.new); }

I can't migrate to versino 6 because many dependencies my project has.

agustin-garcia avatar Feb 02 '24 12:02 agustin-garcia

@agustin-garcia, in version 5:

@override
List<Bind> get binds => [
  Bind.singleton((i) => AuthenticationManager(), export: true),
];

eduardoflorence avatar Feb 02 '24 19:02 eduardoflorence

Hello, How do you apply this same concept for version 5? I'm trying to replicate but its telling me that "The method doesn't override an inherited method."

class CoreModule extends Module { @override List<Bind> get binds => [ Bind<AuthenticationManager>((i) => AuthenticationManager(), export: true), Bind<StorageSettingsController>((i) => StorageSettingsController(), export: true), ]; }

class AppModule extends Module { @override List<Module> get imports => [CoreModule()]; }

class LoginModule extends Module { @override List<Module> get imports => [CoreModule()];

@override List<ModularRoute> get routes => [ ChildRoute('/', child: (_, __) => LoginWidget()), ]; }

Then in the LoginWidget, I call it like: final authMgr = Modular.get<AuthenticationManager>(); authMgr.X = 'X';

The setting of properties work while in the same module, but if I move to another module, the Object's properties are all null. What do I need to do for changes on a child Module are reflected in the CoreModule's object so that all Modules can update/read the properties? Thanks

agustin-garcia avatar Feb 03 '24 01:02 agustin-garcia

Use .singleton:

@override
List<Bind> get binds => [
  Bind.singleton((i) => AuthenticationManager(), export: true),
  Bind.singleton((i) => StorageSettingsController(), export: true),
];

eduardoflorence avatar Feb 06 '24 18:02 eduardoflorence

Thanks Eduardo, I tried as per your suggestion, but unfortunately I'm still missing something, because when I try the code below, within the LOGIN module I can do: Modular.get<AuthenticationManager>().isLogged = true; print("isLogged: ${Modular.get<AuthenticationManager>().isLogged}"); and everything seems all right.

But as soon as I navigate to the HOME module from the LOGIN module via the "Modular.to.pushReplacementNamed(AppPaths.home);" instruction, the AuthenticationManager properties become NULL again

Could you please look at the code snippet and try to find out what am I missing?

Thanks again

` class CoreModule extends Module { @override List<Bind> get binds => [Bind.singleton((i) => AuthenticationManager(), export: true),]; }

class AppModule extends Module { @override List<Module> get imports => [CoreModule()]; @override List<ModularRoute> get routes => [ ChildRoute('/', child: (_, __) => const AppRoot(), guards: [AuthGuard()]), ModuleRoute(AppPaths.home, module: HomeModule()), ModuleRoute(AppPaths.login, module: LoginModule()), ]; }

class LoginModule extends Module { @override List<Module> get imports => [CoreModule()]; @override List<ModularRoute> get routes => [ChildRoute('/', child: (_, __) => LoginWidget()),]; }

class HomeModule extends Module { @override List<Module> get imports => [CoreModule()]; @override List<ModularRoute> get routes => [ChildRoute('/', child: (_, __) => const HomePage()),]; }

class AuthGuard extends RouteGuard { AuthGuard() : super(redirectTo: AppPaths.login); @override Future canActivate(String url, ModularRoute router) async { return Modular.get<AuthenticationManager>().isLogged;
} } `

agustin-garcia avatar Feb 10 '24 15:02 agustin-garcia

Any guidance on this or where else to look for it?

agustin-garcia avatar Feb 14 '24 20:02 agustin-garcia

@agustin-garcia, do it:

Modular.get<AuthenticationManager>().isLogged

edugemini avatar Feb 22 '24 13:02 edugemini