I'm working on my first MvvmCross project and I'm using a Splash Screen to get some user data from sqlite database and start Login or Main View Model depending on there is or no data.
Then I would like to send those data to the MainViewModel. I would like to know if that if possible to make and how to do it.
This is my MvxSplashScreenActivity code:
[Activity(Theme = "#style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashScreen : MvxSplashScreenActivity
{
protected override void OnResume()
{
base.OnResume();
Task startupWork = new Task(() => { SimulateStartup(); });
startupWork.Start();
}
async void SimulateStartup()
{
Task<Core.Models.TrackrUserData> result = Mvx.Resolve<RepositoryService>().GetUserDataAsync();
await Task.Delay(2000);
Core.Models.TrackrUserData userData = result.Result;
if(userData != null){
await GetProjects(userData);
await Task.Delay(1000);
}else{
await Task.Delay(1000);
}
}
async Task<bool> GetProjects(Core.Models.TrackrUserData login)
{
var apiService = new ApiService();
var respuesta = await apiService.GetProjects(login.IdUser.ToString(), login.ActiveTeamId.ToString(), login.Jwt);
await Mvx.Resolve<RepositoryService>().DeleteProjects();
var projectsSaved = SaveProjects(respuesta);
return true;
}
async Task<bool> SaveProjects(List<ProjectGetResponse> respuesta)
{
foreach (var item in respuesta)
{
await Mvx.Resolve<RepositoryService>().CreateProject(new Project
{
Id = item.Id,
Name = item.Name
});
}
return true;
}
}
Thanks in advance!
I wouldn't recommend you to perform all the async work at a View level. Business logic and heavy work is not a concern of the View. Think about this: If you were to create an iOS version of the App, you will need to replicate all the code on that platform too...
You can take advantage of the MVVM implementation that MvvmCross offers and do all the operations at a Core level.
In order to do this, you can follow these steps:
1) Create a class that derives from MvxNavigationServiceAppStart.
2) Override Start method and make sure you navigate to a ViewModel (MainViewModel in your case) before the method ends.
3) Before navigating, you can perform all the operations that you want. But if you were to do async/await you will end up with an async void Start method... which isn't really good. So my recommendation here is to store a simple value to make the initial decisions using something like this plugin and load fast.
4) After that you can do all the heavy work on your initial ViewModel (maybe MainViewModel?).
Final hint: You should consider loading all dependencies using Dependency Injection in constructors instead of resolving them through the IoC Container.
Related
I'm trying to create some coroutines (async) in a loop . I want to start everything in parallel then wait for them all to finish before proceeding. The documentation provides the following example:
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
but this requires that all the class instantiations be known in advance. However with a varying number of instantiations this is not practical. As a work around I found that the following works:
given a mutable List of class objects from class MyObject and MyObject has a method called myDo()
private val mObjects = mutableListOf<MyObject>()
and ignoring error checking and assuming the list has 2 or more objects then the following works but it's kind of clunky and not very elegant
coroutineScope {
val pd = async { myObjects[0].myDo() }
val dds = mutableListOf(pd)
for (i in 1..numObjects - 1) {
dds.add(async {mObjects[i].myDo() })
}
val nds = dds.toList()
nds.awaitAll()
}// end coroutineScope
What I'd hope to do was something like
val dds = mutableListOf<Job>()
for (i in 0..numObjects - 1) {
dds.add(async {mObjects[i].myDo() })
}
val nds = dds.toList()
nds.awaitAll()
but this doesn't work as the async result is a
Deferred<out T> : Job
interface not a Job interface. The problem with this is in the line
val dds = mutableListOf<Job>()
I don't know what to use in place of Job. That is, for async what is T?
Any help or suggestions would be appreciated
T in this case is whatever type myDo() returns.
I think you are overcomplicating it by creating the extra MutableLists. You can do it like this:
val results = coroutineScope {
mObjects.map { obj ->
async { obj.myDo() }
}.awaitAll()
}
results will be a List<MyDoReturnType>.
Edit: I just realized, since it wasn't obvious to you that the type of a Deferred is whatever the async lambda returns, maybe it's because myDo() doesn't return anything (implicitly returns Unit). If that's the case, you should use launch instead of async. The only difference between them is that async's lambda returns something and launch's doesn't. Deferred inherits from Job because a Deferred is a Job with a result. If myDo() doesn't return anything, your code should look like the following, with no result.
coroutineScope {
for (obj in mObjects) launch { obj.myDo() }
}
The answer from TenFour04 provided the key to my answer The following code works for me
coroutineScope {
val dds = mutableListOf<Deferred<Unit>>()
for (item in mObjects) { dds.add(async {item.myDo() }) }
val nds = dds.toList()
nds.awaitAll()
}
Am I stupid!!! or what. After I figured it out, the answer is almost trivial. The best solution I found is
private val mObjects = mutableListOf<MyObject>()
coroutineScope {val deferreds = listOf(mObjects.size){async{mObjects[it].myDo()}}
deferreds.awaitAll()
}// end coroutineScope
I like this better than the map solution as it doesn't create an intermediate Pair set
I have still a little bit of trouble putting all information together about the thread-safety of using coroutines to launch network requests.
Let's say we have following use-case, there is a list of users we get and for each of those users, I will do some specific check which has to run over a network request to the API, giving me some information back about this user.
The userCheck happens inside a library, which doesn't expose suspend functions but rather still uses a callback.
Inside of this library, I have seen code like this to launch each of the network requests:
internal suspend fun <T> doNetworkRequest(request: suspend () -> Response<T>): NetworkResult<T> {
return withContext(Dispatchers.IO) {
try {
val response = request.invoke()
...
According to the documentation, Dispatchers.IO can use multiple threads for the execution of the code, also the request function is simply a function from a Retrofit API.
So what I did is to launch the request for each user, and use a single resultHandler object, which will add the results to a list and check if the length of the result list equals the length of the user list, if so, then all userChecks are done and I know that I can do something with the results, which need to be returned all together.
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
userList?.forEach {
checkUser(it, handler)
}
My question is: Is this implementation thread-safe? As far as I know, Kotlin objects should be thread safe, but I have gotten feedback that this is possibly not the best implementation :D
But in theory, even if the requests get launched asynchronous and multiple at the same time, only one at a time can access the lock of the thread the result handler is running on and there will be no race condition or problems with adding items to the list and comparing the sizes.
Am I wrong about this?
Is there any way to handle this scenario in a better way?
If you are executing multiple request in parallel - it's not. List is not thread safe. But it's simple fix for that. Create a Mutex object and then just wrap your operation on list in lock, like that:
val lock = Mutex()
val userList: List<String>? = getUsers()
val userCheckResultList = mutableListOf<UserCheckResult>()
val handler = object : UserCheckResultHandler {
override fun onResult(
userCheckResult: UserCheckResult?
) {
lock.withLock {
userCheckResult?.let {
userCheckResultList.add(
it
)
}
if (userCheckResultList.size == userList?.size) {
doSomethingWithResultList()
print("SUCCESS")
}
}
}
}
userList?.forEach {
checkUser(it, handler)
}
I have to add that this whole solution seems very hacky. I would go completely other route. Run all of your requests wrapping those in async { // network request } which will return Deferred object. Add this object to some list. After that wait for all of those deferred objects using awaitAll(). Like that:
val jobs = mutableListOf<Job>()
userList?.forEach {
// i assume checkUser is suspendable here
jobs += async { checkUser(it, handler) }
}
// wait for all requests
jobs.awaitAll()
// After that you can access all results like this:
val resultOfJob0 = jobs[0].getCompleted()
I have an issue with my android app. I'm fairly new with it and have some issues with finding the correct documentation for a asynchronous. I'm using the kohttp library to help me a bit.
The thing is, you can't run this on the main UI thread so I want to make this request Async. I can't find a clear reference in the documentation and I don't really know how to do this in plain Kotlin.
This is what I come up with; in a separate class named LoginCall. I tried other answers, this however didn't result in success. How can I run this on a new thread and still use the response?
class LoginCall {
fun callLoginRequest(a:String, b:String): Any {
val response: Response = httpPost {
host = "XXX"
path = "XXX"
param { }
header { }
body {
form {
"email" to a
"password" to b
}
}
}
return response
}
}
There are many ways to achieve this, if you're using android as the underlying platform, you can use the native component called AsyncTask a good SO post on how to use it.
If you wish to leverage kotlin as a language and the features provided by it, you can try using coroutines ref.
Personally, i would recommend coroutines, it simplifies exception and error handling, also prevents callback hell.
here's a sample of the same code in a coroutine,
// global
private val mainScope = CoroutineScope(Dispatchers.MAIN + SupervisorJob())
// inside a method
mainScope.launch{
withContext(Dispatchers.IO){
// do your async task here, as you can see, you're doing this in an IO thread scope.
}
}
Also, you can create an issue to implement asyncHttpPost.
Since kohttp 0.10.0 you can use async methods for such cases. You can try them.
Code example:
suspend fun callLoginRequest(a:String, b:String): Any {
val response: Differed<Response> = httpPostAsync {
host = "XXX"
path = "XXX"
param { }
header { }
body {
form {
"email" to a
"password" to b
}
}
}
// for further usage in coroutines
return response.await()
}
And call this function from coroutine
I am quite frustrated at the amount documentation that says explicitly you should use asynchronous code for networking.
I even read an article which outright states 'Networking is an inherently asynchronous operation.'
https://react-native.org/doc/network.html
So first there is a difference between background processing and asynchronous code.
For example, running code asynchronously doesn't necessarily mean it is on the background. For this we can actually use a background thread.
When you write an iOS app, and you have several view controllers, each accessing the same data which is downloaded by the Model, when you download the data for code asynchronously, you have a frustrating tangle of callbacks and asynchronous messages being passed throughout the application.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which is opened accessing the data before it is downloaded? You might not be able to tell which controller was opened first so this poses an issue, how do you make sure they don't access the data until it is finished downloading?
I suppose you solve this using a completion handler and a model which then fires off a Key Value Observing notification calling the controller when it is finished downloading (a push model).
But what happens if the said controller is not loaded when the notification is posted, does this mean it never gets the data? Wouldn't it make more sense to use a pull model, so when the controller is loaded it can check if the data is available, if so how do you handle this with an async paradigm?
But any notification callback cannot access the outlying scope of the rest of the controller.
However I have written some entirely synchronous code which uses locks and semaphores. The model downloads the data synchronously in a background thread. The controller classes (if they are loaded) check the Model class to see if the data is available. Locks mean that the code cannot access the data if the data is not downloaded. The App signals when the data has finished downloading and while all controllers and models use the same shared synchronous DispatchQueue this prevents the controllers accessing the data arrays when they are empty or the data is being downloaded.
Async code commonly produces weakly coupled code that can't access the scope of the rest of the class and you have methods firing at different times and in different places in the app which i think is difficult to keep track of. So why is networking 'an inherently asynchronous operation'?
Can anyone provide sound scientific reasons why asynchronous code is better, or reasons why I should not do what I have done with synchronous code, and also methods of how you can make asynchronous code safer, less spaghetti like, easier to work with and easier to read?
CODE:
Table View Controller
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !fromSelectionCtrllr {
let downloader = Downloader.sharedInstance
let group = downloader.group
group.notify(queue: .main, execute: {
let defaults : UserDefaults = UserDefaults.standard
let firstLaunch = defaults.bool(forKey: "firstLaunch")
if firstLaunch {
self.arrayOfData = Model.sharedInstance.provideData()
} else {
self.arrayOfData = Model.sharedInstance.provideNewData()
}
for object in self.arrayOfData {
if let deviceName = object.chargeDeviceName {
let theSubscript = deviceName.prefix(1)
let theString = String(theSubscript)
if !self.sectionTitles.contains(theString) {
self.sectionTitles.append(theString)
}
} else {
self.sectionTitles.append("")
}
if let deviceName = object.chargeDeviceName {
let string = String(describing: deviceName.prefix(1))
var arry = self.chargingPointDict[string]
if arry == nil {
arry = []
}
arry?.append(object)
self.chargingPointDict.updateValue(arry!, forKey: string)
} else {
self.chargingPointDict[" "]?.append(object)
}
}
self.sectionTitles = self.removeDuplicates(array: self.sectionTitles)
self.sectionTitles = self.sectionTitles.sorted( by: { $0 < $1 })
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.myTableView.reloadData()
}
})
}
fromSelectionCtrllr = false
}
CellForRowAtIndexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if (searchController.searchBar.text?.isEmpty)! {
if self.sectionTitles.isEmpty {
cell.textLabel?.text = "Nothing to display"
return cell
} else {
let mySectionIndex = self.sectionTitles[indexPath.section]
if mySectionIndex != "" {
let arrayOfPoints : [ChargingPoint] = self.chargingPointDict[mySectionIndex]!
let object : ChargingPoint = arrayOfPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
} else {
return cell
}
}
} else {
let object : ChargingPoint = self.filteredPoints[indexPath.row]
cell.textLabel?.text = object.chargeDeviceName
return cell
}
}
Model Class
class Model: NSObject {
var currentChargingPointArray : [ChargingPoint] = []
var newChargingPointArray : [ChargingPoint] = []
var latitude : Double?
var longitude : Double?
var annotationArray : [ChargingPointAnnotation] = []
var newAnnotationArray : [ChargingPointAnnotation] = []
static let downloader = Downloader.sharedInstance
var savedRegion : MKCoordinateRegion? = nil
/* The model class is a singleton */
static let sharedInstance : Model = {
let instance = Model()
return instance
}()
fileprivate override init( ) {} //This prevents others from using the default '()' initializer for this class.
func setLocation(lat: Double, long: Double) {
self.latitude = lat
self.longitude = long
}
func returnData(array: Array<ChargingPoint>) {
currentChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
currentChargingPointArray = unique
}
func returnNewData(array: Array<ChargingPoint>) {
newChargingPointArray = []
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in array {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
newChargingPointArray = unique
}
func provideData() -> [ChargingPoint] {
return currentChargingPointArray
}
func provideNewData() -> [ChargingPoint] {
return newChargingPointArray
}
func makeAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.annotationArray = []
for chargingPoint in currentChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.annotationArray.append(annotation!)
}
}
return self.annotationArray
}
func makeNewAnnotations() -> [ChargingPointAnnotation] {
let queue = DispatchQueue(label: "com.jackspacie.ChargeFinder", qos: .background, attributes: [])
queue.sync {
self.newAnnotationArray = []
for chargingPoint in newChargingPointArray {
let location = CLLocationCoordinate2D( latitude: chargingPoint.latitude!, longitude: chargingPoint.longitude!)
let annotation = ChargingPointAnnotation(location: location)
annotation?.title = chargingPoint.chargeDeviceName
annotation?.pointTitle = chargingPoint.chargeDeviceName
annotation?.chargingPoint = chargingPoint
self.newAnnotationArray.append(annotation!)
}
}
return self.newAnnotationArray
}
Downloader Class
var group = DispatchGroup()
var model = Model.sharedInstance
/* The downloader class is a singleton */
static let sharedInstance : Downloader = {
let instance = Downloader()
return instance
}()
fileprivate override init() {} //This prevents others from using the default '()' initializer for this class.
func download(lat: Double, long: Double, dist: Int) {
func recursive(lat: Double, long: Double, dist: Int) {
var chargeDeviceArray : [ChargingPoint] = []
let url = URL(string: “https://www.blah.com/lat/\(lat)/long/\(long)/dist/\(dist)/")!
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if error != nil {
print("urlSession Error")
recursive(lat: lat, long: long, dist: dist)
return
} else {
guard let unwrappedData = data else { return }
do {
let jsonDict : [String: Any] = try JSONSerialization.jsonObject(with: unwrappedData, options: [] ) as! [String : Any]
let arrayOfDicts = jsonDict["ChargeDevice"] as? [[String: Any]]
for value in arrayOfDicts! {
let chargePoint = ChargingPoint()
// process data into objects.
chargeDeviceArray.append(chargePoint)
}
var seen = Set<String>()
var unique = [ChargingPoint]()
for point in chargeDeviceArray {
if !seen.contains(point.chargeDeviceId!) {
unique.append(point)
seen.insert(point.chargeDeviceId!)
}
}
if self.model.currentChargingPointArray.isEmpty {
self.model.returnData(array: unique)
} else {
self.model.returnNewData(array: unique)
}
} catch {
print("json error: \(error)")
}
semaphore.signal()
}
//print(response)
}
task.resume()
semaphore.wait(timeout: .distantFuture)
}
self.group.enter()
let queue = DispatchQueue(label: "com.myapp.charge”, qos: .background, attributes: [])
queue.sync {
recursive(lat: lat, long: long, dist: dist)
}
self.group.leave()
}
In short, when it comes to synchronous networking there are very limited cases where you would prefer this. Why?
Because 'A synchronous request blocks the client until operation completes' and in general you don't want the client to freeze. The user can't do anything at that time since we are waiting for all the synchronous actions to complete before enabling the client.
When using asynchronous requests you can build UI, show spinners and depending on whether you already cached older data or have other functionalities, still let the user use your client.
When I have multiple view controllers using the same data this poses a problem, how do I make sure I don't have the view controller which
is opened accessing the data before it is downloaded? You might not be
able to tell which controller was opened first so this poses an issue,
how do you make sure they don't access the data until it is finished
downloading?
I suppose you solve this using a completion handler and a model which
then fires off a Key Value Observing notification calling the
controller when it is finished downloading (a push model).
But I ask this, what happens if the said controller is not loaded when
the notification is posted, does this mean it never gets the data?
Wouldn't it make more sense to use a pull model, so when the
controller is loaded it can check if the dat is available, if so how
do you handle this with an async paradigm?
Load UI-> Show Progress Dialog -> Perform ASYNC(LoadData,SetData intoController)-> DismissDialog
Idk if this is what you're refering
I really dont see an oportunity where you will prefer block UI Thread and freeze App until it loads the data synchronically
The article states that "networking is an inherently asynchronous operation" because networking is an inherently asynchronous operation.
Synchronous means "happening at the same time". Specifically in computing it means with reference to a specific time period or clock signal. For example, electrically, the CPU operates synchronously, which is why we talk about the clock speed of a computer.
Synchronous does not mean "blocking" although that is a common (mis)interpretation. In fact, Apple hasn't helped here with their function names. Technically they should be something like DispatchQueue.nonBlockingOperation() and DispatchQueue.blockingOperation() rather than async/sync.
Synchronous systems can operate at much higher speeds but require a very controlled environment, which is why you find synchronous operation at the core of a computer but not so much outside it.
Your code blocks a background queue waiting for the download to complete, but the download still completes asynchronously. If it were completing synchronously you wouldn't need the semaphore. You would know that the data would be available at the specified point in time (say 0.2 seconds from now or whatever).
Your Downloader class also still notifies the view controller about the available data asynchronously via the dispatch group notify.
From what I can see, you have added a bunch of complexity to the solution (By adding a dispatch group and a semaphore) and introduced the potential for deadlocks but you still have asynchronous code. You block the background queue waiting for the completion of the download, but you still notify the data consumers asynchronously.
The same outcome could be achieved using the standard delegation or completion handler patterns with much less complexity. If there are potentially multiple parties interested in knowing about the new data then you can use the NotificationCentre to "broadcast" that information.
By the way, loosely coupled code is generally considered more desirable.
I wan to use di. in flutter and I add this https://pub.dartlang.org/packages/di packages my project and I started to read this https://webdev.dartlang.org/angular/guide/dependency-injection article, but I don't fully understand.
So it's ok:
use #Injectable() annotation on services class(e.g: MyServices), but how to inject other class?
For example I would like to similar:
class MyClass{
//some variable
var asd = MyService.asd; //need inject var.
//maybe use injector.get(MyService).asd;
//but how add injector? (I don't add across constructor)
MyService.oneMethod;//need inject method
}
main(){
var injector = new ModuleInjector([new Module()
..bind(MyService)
]);
}
The point is, I don't want to use a constructor. I want to directly use injector. This is possible in flutter/dart?
There is a Flutter-compatible dependency injection framework that Google recently open-sourced. It is available here: https://github.com/google/inject.dart
This project provides static compile-time dependency injection, rather than relying on reflection to inject dependencies at run-time. If you are familiar with Dagger, it appears to be quite similar.
It's worth noting - this is not an official Google or Dart team project. At the time of writing, there is little documentation and it is currently considered a developer preview.
Angulars DI package can't be used independent of Angular.
The di package is quite outdated and depends on dart:mirrors which isn't available in Flutter
There seems to be a new DI package work in progress which is supposed to replace Angulars built-in DI and should also be useable standalone in Flutter or server-side applications, but there is no pre-release or source code available yet.
update
The announced DI package was delayed indefinitely.
If you want to use a class as a Singleton, from what I saw, the common usecase is to use a factory constructor.
How do you build a Singleton in Dart?
Never had to use any dependency injection system with Flutter for now.
Hello what about something like this? Very simple implementation, Injector itself is singleton and also added classes into it. Of course can be extended very easily. If you are looking for something more sophisticated check this package: https://pub.dartlang.org/packages/flutter_simple_dependency_injection
void main() {
Injector injector = Injector();
injector.add(() => Person('Filip'));
injector.add(() => City('New York'));
Person person = injector.get<Person>();
City city = injector.get<City>();
print(person.name);
print(city.name);
}
class Person {
String name;
Person(this.name);
}
class City {
String name;
City(this.name);
}
typedef T CreateInstanceFn<T>();
class Injector {
static final Injector _singleton = Injector._internal();
final _factories = Map<String, dynamic>();
factory Injector() {
return _singleton;
}
Injector._internal();
String _generateKey<T>(T type) {
return '${type.toString()}_instance';
}
void add<T>(CreateInstanceFn<T> createInstance) {
final typeKey = _generateKey(T);
_factories[typeKey] = createInstance();
}
T get<T>() {
final typeKey = _generateKey(T);
T instance = _factories[typeKey];
if (instance == null) {
print('Cannot find instance for type $typeKey');
}
return instance;
}
}
I was facing the same issue here so I created a dart package specifically for Flutter. It relies on factory methods rather than the mirrors api so it works in flutter. While still using a familiar IOC pattern. Hope this helps!
https://pub.dartlang.org/packages/flutter_simple_dependency_injection
import 'package:flutter_simple_dependency_injection/injector.dart';
void main() {
final injector = Injector.getInjector();
injector.map(Logger, (i) => new Logger(), isSingleton: true);
injector.map(String, (i) => "https://api.com/", key: "apiUrl");
injector.map(SomeService, (i) => new SomeService(i.get(Logger), i.get(String, "apiUrl")));
injector.get(SomeService).doSomething();
// passing in the [SomeService] as a generic parameter strongly types the return object.
injector.get<SomeService>(SomeService).doSomething();
}
class Logger {
void log(String message) => print(message);
}
class SomeService {
final Logger _logger;
final String _apiUrl;
SomeService(this._logger, this._apiUrl);
void doSomething() {
_logger.log("Doing something with the api at '$_apiUrl'");
}
}
This package ioc_container is a Dart Ioc Container. Technically, it's not an injector because it doesn't automate injection with anything like reflection. However, it does simplify and organize the process of injecting dependencies within your factories. Here is the code.
Code here
import 'package:meta/meta.dart';
class ServiceDefinition<T> {
bool isSingleton;
T Function(IocContainer container) factory;
ServiceDefinition(this.isSingleton, this.factory);
}
class IocContainer {
#visibleForTesting
final Map<Type, ServiceDefinition> serviceDefinitionsByType;
#visibleForTesting
final Map<Type, Object> singletons;
IocContainer(this.serviceDefinitionsByType, this.singletons);
///Get an instance of your dependency
T get<T>() => singletons.containsKey(T)
? singletons[T] as T
: serviceDefinitionsByType.containsKey(T)
? (serviceDefinitionsByType[T]!.isSingleton
? singletons.putIfAbsent(
T,
() =>
serviceDefinitionsByType[T]!.factory(this) as Object) as T
: serviceDefinitionsByType[T]!.factory(this))
: throw Exception('Service not found');
}
///A builder for creating an [IocContainer].
class IocContainerBuilder {
final Map<Type, ServiceDefinition> _serviceDefinitionsByType = {};
///Throw an error if a service is added more than once. Set this to true when
///you want to add mocks to set of services for a test.
final bool allowOverrides;
IocContainerBuilder({this.allowOverrides = false});
///Add a factory to the container.
void addServiceDefinition<T>(
///Add a factory and whether or not this service is a singleton
ServiceDefinition<T> serviceDefinition) {
if (_serviceDefinitionsByType.containsKey(T)) {
if (allowOverrides) {
_serviceDefinitionsByType.remove(T);
} else {
throw Exception('Service already exists');
}
}
_serviceDefinitionsByType.putIfAbsent(T, () => serviceDefinition);
}
///Create an [IocContainer] from the [IocContainerBuilder].
///This will create an instance of each singleton service and store it
///in an immutable list unless you specify [isLazy] as true.
IocContainer toContainer(
{
///If this is true the services will be created when they are requested
///and this container will not technically be immutable.
bool isLazy = false}) {
if (!isLazy) {
final singletons = <Type, Object>{};
final tempContainer = IocContainer(_serviceDefinitionsByType, singletons);
_serviceDefinitionsByType.forEach((type, serviceDefinition) {
if (serviceDefinition.isSingleton) {
singletons.putIfAbsent(
type, () => serviceDefinition.factory(tempContainer));
}
});
return IocContainer(
Map<Type, ServiceDefinition>.unmodifiable(_serviceDefinitionsByType),
Map<Type, Object>.unmodifiable(singletons));
}
return IocContainer(
Map<Type, ServiceDefinition>.unmodifiable(_serviceDefinitionsByType),
<Type, Object>{});
}
}
extension Extensions on IocContainerBuilder {
///Add a singleton object dependency to the container.
void addSingletonService<T>(T service) =>
addServiceDefinition(ServiceDefinition(true, (i) => service));
void addSingleton<T>(T Function(IocContainer container) factory) =>
addServiceDefinition(ServiceDefinition(true, factory));
void add<T>(T Function(IocContainer container) factory) =>
addServiceDefinition(ServiceDefinition(false, factory));
}