Search code examples
flutterdependency-injection

Flutter dependency injection set-up problem with get_it


I'm trying to learn how to setup the dependency injection in flutter projects and I faced with one problem:

I/flutter (14507): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (14507): The following assertion was thrown building _InheritedProviderScope<NumberTriviaBloc>(value: null):
I/flutter (14507): No type GetConcreteNumberTrivia is registered inside GetIt.
I/flutter (14507):  Did you forget to pass an instance name?
I/flutter (14507): (Did you accidentally do  GetIt sl=GetIt.instance(); instead of GetIt sl=GetIt.instance;did you
I/flutter (14507): forget to register it?)
I/flutter (14507): 'package:get_it/get_it_impl.dart':
I/flutter (14507): Failed assertion: line 251 pos 14: 'instanceFactory != null'
I/flutter (14507): 
I/flutter (14507): The relevant error-causing widget was:
I/flutter (14507):   _InheritedProviderScope<NumberTriviaBloc>

The dependency initialization:

final sl = GetIt.instance;

void init() {
  //! Features - Number Trivia
  //* Bloc
  sl.registerFactory<NumberTriviaBloc>(() => NumberTriviaBloc(concrete: sl(), inputConverter: sl(), random: sl()));

  //* Use cases
  sl.registerLazySingleton<UseCase<NumberTrivia, Params>>(() => GetConcreteNumberTrivia(sl()));
  sl.registerLazySingleton<UseCase<NumberTrivia, NoParams>>(() => GetRandomNumberTrivia(sl()));

  //* Repository
  sl.registerLazySingleton<NumberTriviaRepository>(() => NumberTriviaRepositoryImplementation(
      localDataSource: sl(), networkInfo: sl(), remoteDataSource: sl()));

  //* Data sources
  sl.registerLazySingleton<NumberTriviaRemoteDataSource>(
      () => NumberTriviaRemoteDataSourceImpl(client: sl()));

  sl.registerLazySingleton<NumberTriviaLocalDataSource>(
      () => NumberTriviaLocalDataSourceImpl(sharedPreferences: sl()));

  //! Core
  sl.registerLazySingleton<InputConverter>(() => InputConverter());
  sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl(sl()));

  //! External
  sl.registerLazySingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
  sl.registerLazySingleton<http.Client>(() => http.Client());
  sl.registerLazySingleton<DataConnectionChecker>(() => DataConnectionChecker());
}

I can't understand what the problem, here is the code for other stuff initialization:

class NumberTriviaBloc extends Bloc<NumberTriviaEvent, NumberTriviaState> {
  final GetConcreteNumberTrivia getConcreteNumberTrivia;
  final GetRandomNumberTrivia getRandomNumberTrivia;
  final InputConverter inputConverter;

  NumberTriviaBloc(
      {@required GetConcreteNumberTrivia concrete,
      @required GetRandomNumberTrivia random,
      @required this.inputConverter})
      : assert(concrete != null),
        assert(random != null),
        assert(inputConverter != null),
        getConcreteNumberTrivia = concrete,
        getRandomNumberTrivia = random;
  ...
}

class GetConcreteNumberTrivia implements UseCase<NumberTrivia, Params>{
  final NumberTriviaRepository repository;

  GetConcreteNumberTrivia(this.repository);

  @override
  Future<Either<Failure, NumberTrivia>> call(Params params) async {
    return await repository.getConcreteNumberTrivia(params.number);
  }
}

class GetRandomNumberTrivia implements UseCase<NumberTrivia, NoParams> {
  final NumberTriviaRepository repository;

  GetRandomNumberTrivia(this.repository);

  @override
  Future<Either<Failure, NumberTrivia>> call(NoParams params) async {
    return await repository.getRandomNumberTrivia();
  }
}

class NumberTriviaRepositoryImplementation implements NumberTriviaRepository {
  final NumberTriviaRemoteDataSource remoteDataSource;
  final NumberTriviaLocalDataSource localDataSource;
  final NetworkInfo networkInfo;

  NumberTriviaRepositoryImplementation(
      {@required this.remoteDataSource,
      @required this.localDataSource,
      @required this.networkInfo});
  ...
}

class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource {
  final http.Client client;

  NumberTriviaRemoteDataSourceImpl({@required this.client});
  ...
}

class NumberTriviaLocalDataSourceImpl implements NumberTriviaLocalDataSource {
  final SharedPreferences sharedPreferences;

  NumberTriviaLocalDataSourceImpl({@required this.sharedPreferences});
  ...
}

class InputConverter {}

class NetworkInfoImpl extends NetworkInfo {
  final DataConnectionChecker connectionChecker;

  NetworkInfoImpl(this.connectionChecker);
  ...
}

The Widgets is:

void main() {
  di.init();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primaryColor: Colors.green.shade800,
        accentColor: Colors.green.shade600,
      ),
      home: NumberTriviaPage(),
    );
  }
}

class NumberTriviaPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Number Trivia'),
      ),
      body: SingleChildScrollView(child: buildBody(context)),
    );
  }

  BlocProvider<NumberTriviaBloc> buildBody(BuildContext context) {
    return BlocProvider<NumberTriviaBloc>(
      create: (_) => sl<NumberTriviaBloc>(),
      child: Center(
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: Column(
            children: <Widget>[
              SizedBox(
                height: 10,
              ),
              BlocBuilder<NumberTriviaBloc, NumberTriviaState>(
                // ignore: missing_return
                builder: (context, state) {
                  if (state is Empty) {
                    return MessageDisplay(
                      message: 'Start searching',
                    );
                  } else if (state is Loading) {
                    return LoadingWidget();
                  } else if (state is Loaded) {
                    return TriviaDisplay(
                      numberTrivia: state.trivia,
                    );
                  } else if (state is Error) {
                    return MessageDisplay(
                      message: state.message,
                    );
                  }
                },
              ),
              SizedBox(
                height: 20,
              ),
              TriviaControl()
            ],
          ),
        ),
      ),
    );
  }
}

I can't figure out why that not working for me. I following the tutorial on youtube, and also checked code from this tutorial on github repository, but that won't help me. So can you explain to me what's going on?

I got a new error with help of tnc1997 answer:

lib/injection_container.dart:27:91: Error: The argument type 'NumberTriviaRepository/*1*/' can't be assigned to the parameter type 'NumberTriviaRepository/*2*/'.
 - 'NumberTriviaRepository/*1*/' is from 'package:clean_architecture/features/number_trivia/domain/repositories/number_trivia_repository.dart' ('lib/features/number_trivia/domain/repositories/number_trivia_repository.dart').
 - 'NumberTriviaRepository/*2*/' is from 'lib/features/number_trivia/domain/repositories/number_trivia_repository.dart'.
  sl.registerLazySingleton<UseCase<NumberTrivia, NoParams>>(() => GetRandomNumberTrivia(sl<NumberTriviaRepository>()));

This is the same file, and one line below uses similar contract

//* Use cases
  sl.registerLazySingleton<UseCase<NumberTrivia, Params>>(() => GetConcreteNumberTrivia(sl<NumberTriviaRepository>()));
  sl.registerLazySingleton<UseCase<NumberTrivia, NoParams>>(() => GetRandomNumberTrivia(sl<NumberTriviaRepository>()));

  //* Repository
  sl.registerLazySingleton<NumberTriviaRepository>(() => NumberTriviaRepositoryImplementation(
      localDataSource: sl<NumberTriviaLocalDataSource>(), networkInfo: sl<NetworkInfo>(), remoteDataSource: sl<NumberTriviaRemoteDataSource>()));

And now I'm a lot more confused then before


Solution

  • I'm pretty sure that when using the GetIt package you need to depend on the interfaces and not the concrete types in your constructors. After all, one of the principles of dependency injection is to rely on the interfaces and not the implementations, thus allowing you to easily switch implementations. I have added a code sample below showing how this could be done. You might want to take a look at the documentation here for more information and code samples if you have not done so already. If you have any issues with the code sample, then don't hesitate to leave a comment!

    class NumberTriviaBloc extends Bloc<NumberTriviaEvent, NumberTriviaState> {
      final UseCase<NumberTrivia, Params> getConcreteNumberTrivia;
      final UseCase<NumberTrivia, NoParams> getRandomNumberTrivia;
      final InputConverter inputConverter;
    
      NumberTriviaBloc(
          {@required UseCase<NumberTrivia, Params> concrete,
          @required UseCase<NumberTrivia, NoParams> random,
          @required this.inputConverter})
          : assert(concrete != null),
            assert(random != null),
            assert(inputConverter != null),
            getConcreteNumberTrivia = concrete,
            getRandomNumberTrivia = random;
      ...
    }
    
    sl.registerFactory<NumberTriviaBloc>(() => NumberTriviaBloc(concrete: sl<UseCase<NumberTrivia, Params>>(), inputConverter: sl<InputConverter>(), random: sl<UseCase<NumberTrivia, NoParams>>()));