I'm setting up a project with Flutter. I want to register services with GetIn. But I'm not able to find the methods of the service that I want to use.
This is the class locator in which I register the service, for example MongoDBService.
import 'package:app/services/mongodb.dart';
import 'package:get_it/get_it.dart';
final locator = GetIt.instance;
setupLocator() {
locator.registerSingleton<MongoDBService>(MongoDBService());
}
Then when I want to use the MongoDBService I am not able to find the functions that belong to the service.
void connect() async {
await mongo.beginConnection();
}
final mongo = locator<MongoDBService>;
The error is on beginConnection which is not defined. But currently it is defined in the MongoDBService.
Also I setup the locator by calling the setupLocator function in the main.
void main() {
setupLocator();
runApp(const MyApp());
}
Put final mongo = locator<MongoDBService>; in the connect method.
void connect() async {
final mongo = locator<MongoDBService>();
await mongo.beginConnection();
}
Related
I have initilized GetStorage() in main() and calling .read() in onReady dunction of GetX Controller but always get null!
Future<void> main() async {
await GetStorage.init();
runApp(const App());
}
class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find();
/// Variables
GetStorage userStorage = GetStorage('User');
#override
void onReady() {
// Firebase User
firebaseUser = Rx<User?>(_auth.currentUser);
firebaseUser.bindStream(_auth.userChanges());
//Session
print('========= BEFORE -- ${userStorage.read('isFirstTime')} ===========');
userStorage.writeIfNull('isFirstTime', 'true');
print('========= AFTER -- ${userStorage.read('isFirstTime')} ============');
}
OUTPUT
================== BEFORE -- null ========================
================== AFTER -- true =========================
I have tried named values also like GetStorage('User');
nothing worked.
You will need to give the container name in init if you are using a custom container name.
So, you have two solutions
1 -> Update your init to this
Future<void> main() async {
await GetStorage.init('User'); // <- add your custom Container name 'User'
runApp(const App());
}
OR
2 Don't use a custom container name and GetStorage uses it's default container name. In this case, update your code while declaring GetStorage object to read and write data
class AuthenticationRepository extends GetxController {
static AuthenticationRepository get instance => Get.find();
/// Variables
GetStorage userStorage = GetStorage(); //<- remove custom container name 'User'
#override
void onReady() {
// Firebase User
firebaseUser = Rx<User?>(_auth.currentUser);
firebaseUser.bindStream(_auth.userChanges());
//Session
print('========= BEFORE -- ${userStorage.read('isFirstTime')} ===========');
userStorage.writeIfNull('isFirstTime', 'true');
print('========= AFTER -- ${userStorage.read('isFirstTime')} ============');
}
Need to add
WidgetsFlutterBinding.ensureInitialized();
Future<void> main() async {
await GetStorage.init();
runApp(const App());
}
Change this to :
void main() async {
await GetStorage.init();
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
I am trying to create a persistent interface which forks db calls to floor or another self made web db static store.
Anyway...
The interface part is looking like this:
peristent_interface.dart
import 'package:flutter/material.dart';
import 'package:mwork/database/floor/entities/map_location_entity.dart';
import 'package:mwork/database/floor/result/map_location_result.dart';
import 'persistent_stub.dart'
if(dart.library.io) 'persistent_native.dart'
if(dart.library.js) 'persistent_web.dart';
abstract class Persistent extends ChangeNotifier {
static Persistent? _instance;
static Persistent? get instance{
_instance ??= getPersistent();
return _instance;
}
Future<List<MapLocationResult?>?> getMapLocations();
Future<MapLocationResult?> getMapLocation({int id});
Future<void> insertReplaceMapLocation(MapLocation mapLocation);
Future<void> insertReplaceMapLocations(List<MapLocation> mapLocations);
}
All seems nice so far, but the trouble appears when the init() function below returns Future<AppDatabase> not AppDatabase as I want.
persistent_native.dart
import 'package:floor/floor.dart';
import 'package:mwork/database/floor/database/database.dart';
import 'package:mwork/database/floor/entities/map_location_entity.dart';
import 'package:mwork/database/floor/result/map_location_result.dart';
import 'package:mwork/services/persistent/persistent_interface.dart';
import 'package:mwork/common/m_work_config.dart' as m_work_config;
Persistent getPersistent() => PersistentNative();
class PersistentNative extends Persistent {
final AppDatabase _appDatabase = init(); //<-- Fails here !!
static Future<AppDatabase> init() async {
return await $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build();
}
#override
Future<List<MapLocationResult?>?> getMapLocations() async {
return await _appDatabase.mapLocationDao.getMapLocations();
}
#override
Future<MapLocationResult?> getMapLocation({int id=-1}) async {
return await _appDatabase.mapLocationDao.getMapLocation(id);
}
#override
Future<void> insertReplaceMapLocation(MapLocation mapLocation) async {
_appDatabase.mapLocationDao.insertMapLocation(
mapLocation
);
}
#override
Future<void> insertReplaceMapLocations(List<MapLocation> mapLocations) async {
_appDatabase.mapLocationDao.insertMapLocations(
mapLocations
);
}
}
How should I return AppDatabase from init() ?
Maybe you should change the type of the init() function to AppDatabase instead of Future<AppDatabase>? For me it seems that the code is right one and should return AppDatabase.
The init method returns a future, since you wait for it ( and it is a recommended way)
if you would like to return the AppDatabase only, rewrite it as follows::
static AppDatabase init() {
return $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build().then((AppDatabase db) => db);}
Doing this will have some implications though, this wont be awaited meaning that any call depending on this would return late..
I'd recommend using an await clause to the callee,
for example
static Future<AppDatabase> init() async {
return await $FloorAppDatabase.databaseBuilder(m_work_config.mWorkFloorDb).build();
}
and then calling it as::
final AppDatabase db = await (...........);
or:::
YourClass.init().then((AppDatabase db) { /* anything here*/});
I am using an isolate through the compute() method to fetch, parse and sort datas from an API (around 10k entries).
My method getAllCards() is defined inside a class YgoProRepositoryImpl which has an instance of my remote datasource class YgoProRemoteDataSource it is in this class that the method to call my API is defined (it is a simple GET request).
Code Sample
ygopro_repository_impl.dart
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
// ...
YgoProRepositoryImpl({
required this.remoteDataSource,
// ...
});
// ...
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
// ...
}
service_locator.dart
import 'package:get_it/get_it.dart';
import 'data/api/api.dart';
import 'data/datasources/remote/ygopro_remote_data_source.dart';
import 'data/repository/ygopro_repository_impl.dart';
import 'domain/repository/ygopro_repository.dart';
final sl = GetIt.instance;
void setupLocator() {
// ...
_configDomain();
_configData();
// ...
_configExternal();
}
void _configDomain() {
//! Domain
// ...
// Repository
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
// ...
),
);
}
void _configData() {
//! Data
// Data sources
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(sl<RemoteClient>()),
);
// ...
}
void _configExternal() {
//! External
sl.registerLazySingleton<RemoteClient>(() => DioClient());
// ...
}
The code is working properly but getAllCards() is not testable as I cannot inject a mocked class of YgoProRemoteDataSource inside my isolate because it will always get a reference from my service locator.
How can I do to not rely on my service locator to inject YgoProRemoteDataSource inside my isolate and make getAllCards() testable ?
Did a more serious attempt, please see the repo: https://github.com/maxim-saplin/compute_sl_test_sample
Essentially with the current state of affairs with Flutter/Dart you can't pass neither closures nor classes containing closures across isolates boundaries (yet that might change when newer features in Dart land Flutter https://github.com/dart-lang/sdk/issues/46623#issuecomment-916161528). That means there's no way you can pass service locator (which contains closures) or trick the isolate to instantiate a test version of locator via closure IF you don't want any test code to be part of the release build. Yet you can easily pass data source instance to isolate to be used at its entry point as a param.
Beside, I don't think asking isolate to rebuild the entire service locator makes sense. The whole idea behind compute() is to create a short leaving isolate, run the computation, return the result and terminate the isolate. Initialising the locator is an overhead which is better to be avoided. Besides it seems the whole concept of compute() is being as isolated from the rest of the app as possible.
You can clone the repo and run the tests. Few words about the sample:
Based on Flutter counter starter app
lib/classes.dart recreates the code snippet you provided
test/widget_test.dart verifies that YgoProRepositoryImpl is working fine with isolate running fake version of data source
YgoProRemoteDataSourceImpl mimics real implementation and is located at classes.dart and YgoProRemoteDataSourceFake mimics test version
Running isolates under flutter_test requires wrapping test body in tester.runAsync() in order to have real time async execution (rather than fake async used by default by tests and relying on pumping to progress test time). Running tests in this mode can be slow (there's actual 0.5 second wait), structuring the tests in a way when compute() is not used or tested not in many tests is reasonable
classes.dart
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
final sl = GetIt.instance;
class YgoCard {
YgoCard(this.name);
final String name;
}
abstract class YgoProRemoteDataSource {
Future<List<YgoCard>> getCardInfo();
}
class YgoProRemoteDataSourceImpl extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(5, (index) => YgoCard("Impl $index")));
}
}
abstract class YgoProRepository {
Future<List<YgoCard>> getAllCards();
}
class YgoProRepositoryImpl implements YgoProRepository {
final YgoProRemoteDataSource remoteDataSource;
YgoProRepositoryImpl({
required this.remoteDataSource,
});
static Future<List<YgoCard>> _fetchCards(
YgoProRemoteDataSource dataSource) async {
final cards = await dataSource.getCardInfo();
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, remoteDataSource);
return cards;
}
}
void setupLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceImpl(),
);
}
widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:test_sample/classes.dart';
import 'package:test_sample/main.dart';
void main() {
setUpAll(() async {
setupFakeLocator();
});
testWidgets('Test mocked data source', (WidgetTester tester) async {
// Wrapping with runAync() is required to have real async in place
await tester.runAsync(() async {
await tester.pumpWidget(const MyApp());
// Let the isolate spawned by compute() complete, Debug run might require longer wait
await Future.delayed(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
expect(find.text('Fake 9'), findsOneWidget);
});
});
}
class YgoProRemoteDataSourceFake extends YgoProRemoteDataSource {
#override
Future<List<YgoCard>> getCardInfo() {
return Future.delayed(Duration.zero,
() => List.generate(10, (index) => YgoCard("Fake $index")));
}
}
void setupFakeLocator() {
sl.registerLazySingleton<YgoProRepository>(
() => YgoProRepositoryImpl(
remoteDataSource: sl(),
),
);
sl.registerLazySingleton<YgoProRemoteDataSource>(
() => YgoProRemoteDataSourceFake(),
);
}
Do you really need to test the getCards() function?
What are you really testing there? That compute works, sure hope the Dart SDK team has a test for this.
That leaves _fetchCards(), and setupLocator() doesn't need to be tested either, it is precondition for your test-logic. You want to change the setup for the test anyways.
So what you actually want to test is the fetching & sorting. Restructure this into a testable static function and setup your locator beforehand. Put a #visibleForTesting annotation on it.
And on a side-note, depending on how much you bind in your service locator, this could be huge overhead for just using the one repository afterwards.
Example:
static Future<List<YgoCard>> _fetchCards(_) async {
// As I'm inside an isolate I need to re-setup my locator
setupLocator();
return reallyFetchCards();
}
#visibleForTesting
static Future<List<YgoCard>> reallyFetchCards() async {
final cards = await sl<YgoProRemoteDataSource>()
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Test:
// Setup SL and datasource
...
final cards = await YgoProRepositoryImpl.reallyFetchCrads();
// Expect stuff
As I understand you have two options, either inject the dependencies needed for static Future<List<YgoCard>> _fetchCards(_) async via parameters, or mock the object in the locator itself. I would go for the fist option, and have something like :
static Future<List<YgoCard>> _fetchCards(_,YgoProRemoteDataSource remote) async {
// No need to set up locator as you passed the needed dependencies
// setupLocator();
final cards = await remote
.getCardInfo(GetCardInfoRequest(misc: true));
cards.sort((a, b) => a.name.compareTo(b.name));
return cards;
}
#override
Future<List<YgoCard>> getAllCards() async {
final cards = await compute(_fetchCards, null);
return cards;
}
Edit
just updated the answer as its easier to edit this here than in the comments...
Hmm, the only workaround that I can think of is to pass the setupLocator() function as an argument to the class YgoProRepositoryImpl :
final Function setupLocator;
YgoProRepositoryImpl({
required this.remoteDataSource,
required this.setupLocator;
// ...
});
This way you could pass a mock that sets up your mock classes or the real setupLocator of your service_locator.dart. This might not be to elegant. But it should make it testable as now you can mock the setup and its not hardcoded in the function
Im using the library Injectable for Dependency Injection in flutter but Im getting a error where I cannot use SharedPreferences.
Error:
Exception has occurred.
FlutterError (ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization), then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
If you're running a test, you can call the TestWidgetsFlutterBinding.ensureInitialized() as the first line in your test's main() method to initialize the binding.)
I've tryed creating a class and put #lazySingleton
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
and I tryed to put WidgetsFlutterBinding.ensureInitialized()
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjection(Environment.prod);
runApp(MyApp());
}
you can pre-await the future in SharedPreference by annotating with #preResolve
#module
abstract class InjectionModule {
//injecting third party libraries
#preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}
and on the configureInjection class
final GetIt getIt = GetIt.instance;
#injectableInit
Future<void> configureInjection(String env) async {
await $initGetIt(getIt, environment: env);
}
and also on the main class
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Environment.prod);
runApp(MyApp());
}
To actually use:
final prefs = getIt<SharedPreferences>();
await prefs.setString('city', city);
NOT:
final module = getIt<InjectionModule>();
module.prefs.setString('test', test);
Note differences between SharedPreferences and InjectionModule.
Below is the way i got it to work, no guarantee it's the best method.
Await the configureInjection method in the main method.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Env.prod);
runApp(App());
}
And wrap you app in FutureBuilder that makes use of getIt.allReady().
Widget build(context) {
return FutureBuilder(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// ... your app widgets
} else {
// ... some progress indicator widget
}
}
);
}
Helpfull links:
https://pub.dev/documentation/injectable/latest/#registering-asynchronous-injectables
https://pub.dev/packages/get_it#synchronizing-asynchronous-initialisations-of-singletons
Not sure if it's a limitation or something, but below code does not load anything. I have some data driven behaviour that I'd like to test isolated.
class Loader
import 'dart:async';
import 'package:flutter/services.dart' show rootBundle;
class Loader {
Future<String> load() async{
return await rootBundle.loadString('assets/json/sketch.json');
}
}
The test
testWidgets('Should parse load sketch.json', (WidgetTester tester) async {
var loaderFuture = new Loader();
Future<String> resultFuture = loaderFuture.load();
resultFuture.then((value) => print(value))
.catchError((error) => print(error));
while(true){};
});
Future does not return neither success nor error and hangs forever. I know
the while(true) locking up the test, but for now I just wanted to see sketch.json printed
Asset location
To use rootBundle in your tests you need this at the beginning of your test programs:
import 'package:flutter_test/flutter_test.dart';
...
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
See the documentation of DefaultAssetBundle it describes using it and a AssetBundle to provide your own assets.
Create a class that wraps the rootBundle:
#injectable
class AssetsManager {
Future<String> loadString(String path) {
return rootBundle.loadString(path);
}
}
Then inject it to your class and in your test override its dependency:
getIt.unregister<AssetsManager>();
getIt.registerSingleton<AssetsManager>(AssetsManagerMock());
Based on your test scenario, Configure what will be returned when calling loadString by using Mocktail's when method.
I'm using get_it for DI. Hope it's clear enough.