DI can be simple. Forget about modules and components. Just use it!

Overview

Maven Central Kotlin GitHub license Slack channel

PopKorn - Kotlin Multiplatform DI

PopKorn is a simple, powerful and lightweight Kotlin Multiplatform Dependency Injector. It doesn't need any modules or components, just use it without writing a single extra file! It supports AND, IOS, JVM, JS and NATIVE.

Download

Get it with Gradle:

implementation 'cc.popkorn:popkorn:2.1.1'
kapt 'cc.popkorn:popkorn-compiler:2.1.1'

The Kotlin Gradle Plugin 1.4.0 will automatically resolve platform dependent implementations (jvm, js, iosX64...). But if you are using Kotlin Gradle Plugin below 1.4.0 you will have to specify the platform yourself. In the case of Android/JVM is the following:

implementation 'cc.popkorn:popkorn-jvm:2.1.1'
kapt 'cc.popkorn:popkorn-compiler:2.1.1'

Working with Scopes and Environments

Scopes are the way to define the life span of an instance. There are 4 types of scopes:

  • Scope.BY_APP (default) -> Instance will be created only once, for hence will live forever. Normally for classes that have heavy construction or saves states (Retrofit, OkHttp, RoomDB, etc)
  • Scope.BY_USE -> Instance will be created if no one is using it, meaning will live as long as others are using it. Normally for classes that are just like helpers (dataSources, repositories, useCases, etc...)
  • Scope.BY_HOLDER -> Instance will be created if used with a different holder, will live as long as its holder (container). Normally for instances that needs to be shared by a common parent (presenters, viewModels, etc...)
  • Scope.BY_NEW -> Instance will be created every time it's needed, so won't live at all. Normally for instances that doesn't make sense to reuse (presenters, viewModels, screens, etc...)

Environments allow you to have multiple instances of the same object, but in a complete different configuration. For example, you can have 2 different and persistent Retrofit instances. See more examples at bottom.

val r1 = inject<Retrofit>("pro") // This will inject a persistent instance of Retrofit attached to "pro"
val r2 = inject<Retrofit>("des") // This will inject a persistent instance of Retrofit attached to "des"
// r1 !== r2 as they have different environments.

Injecting Project Classes

Just add @Injectable to any class...

@Injectable
class HelloWorld

...and inject it anywhere you want:

val helloWorld = inject<HelloWorld>()

// Or by lazy
val helloWorld by popkorn<HelloWorld>()

By default HelloWorld will be Scope.BY_APP, but we can change it:

@Injectable(scope = Scope.BY_NEW)
class HelloWorld

Also, if HelloWorld has injectable constructor dependencies, PopKorn will automatically resolve them

@Injectable
class HelloWorld(val helloBarcelona: HelloBarcelona, val helloParis: HelloParis)

and if we have different constructors for the class, we can define environments to distinguish them:

@Injectable
class HelloWorld {

    @ForEnviornmen("europe")
    constructor(val helloBarcelona: HelloBarcelona, val helloParis: HelloParis) : this()

    @ForEnviornmen("usa")
    constructor(val helloNewYork: HelloNewYork, val helloLosAngeles: HelloLosAngeles) : this()
}

and then can inject it like this:

val helloWorld = inject<HelloWorld>() // Will inject a HelloWorld instance without parameters
val helloWorld = inject<HelloWorld>("europe") // Will inject a HelloWorld instance with parameters HelloBarcelona and HelloParis
val helloWorld = inject<HelloWorld>("usa") // Will inject a HelloWorld instance with parameters HelloNewYork and HelloLosAngeles

Using Interfaces

Let's now define an interface:

interface Hello

and use it in our example

@Injectable
class HelloWorld : Hello

We can now inject by an interface:

val helloWorld = inject<Hello>() // This will inject a HelloWorld instance 

And just like before, if you have different implementations of the same interface, you can distinguish them with environments

@Injectable
@ForEnvironment("planet")
class HelloPlanet : Hello

so,

val hello = inject<Hello>("planet") // This will return an instance of HelloPlanet
val hello = inject<Hello>() // This will return an instance of HelloWorld

Using runtime arguments (or assisted dependencies)

For injectable classes with BY_NEW scope, you can have assisted arguments

@Injectable(BY_NEW)
class HelloViewModel(@Assisted val id: Long, val param2: HelloBarcelona, val param2: HelloNewYork) : Hello

that you provide in runtime as:

val id = 4
val hello = inject<Hello> {
    assist(id)
}

Injecting External Classes

If you want to inject a class out of your code, just define a class and annotate it with @InjectableProvider. Notice that you can use as many injectable objects as you need defining them as parameters of your method.

@InjectableProvider(scope = Scope.BY_APP)
class MyRetrofitProvider {

    fun createRetrofit(client: OkHttp): Retrofit {
        return Retrofit.Builder()
            .baseUrl("my.url")
            .client(client)
            .build()
    }
}

and use it the same way:

val hello = inject<Retrofit>() // This will inject a persistent instance of Retrofit

Injecting Runtime Instances

There is also a way to use custom injection. You can take control of when an instance is injectable and when is not:

val someInstance = SomeType()

popKorn().addInjectable(someInstance)

val copy1 = inject<SomeType>() // Will inject someInstance

popKorn().removeInjectable(someInstance)

val copy2 = inject<SomeType>() // Will fail, because SomeType is not injectable anymore

In Android this is very useful when injecting the Context (An instance that is provided and cannot be created)

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        popKorn().addInjectable(this, Context::class)
    }

}

Testing

PopKorn also offers the ability to create injectable classes at any time (ignoring its scope), overriding any dependency you like

class Hello(val param: HelloBarcelona, val param: HelloParis)

val hello = popKorn().create<Hello> {
    override(HelloTestBarcelona())
}

This will create a hello instance using HelloTestBarcelona instead of the default HelloBarcelona

Using Android / JVM

PopKorn provides full support to Android platforms. You don't need to initialize anything. Just use it as described above.

To use it from pure java classes, use PopKornCompat:

HelloWorld helloWorld = PopKornCompat.inject(HelloWorld.class);

To prevent you to exclude lots of classes from obfuscation, PopKorn saves some mappings that needs to be merged when generating the APK. If you are using multiple modules, Android will take only the last one by default ( or throw a compilation error depending on the Gradle version), unless the following option it's set in the build.gradle:

android {
    packagingOptions {
        merge 'META-INF/popkorn.provider.mappings'
        merge 'META-INF/popkorn.resolver.mappings'
    }
}

This is the error that the above fixes:

Execution failed for task ':app:mergeDebugJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
   > More than one file was found with OS independent path 'META-INF/popkorn.provider.mappings'

Using IOS

PopKorn provides full support to Objective C / Swift platforms. You will need to do the following:

A) In your multiplatform project, write a File (Bridge.kt) on your IOS module and add this 2 functions:

fun init(creator: (ObjCClass) -> Mapping) = cc.popkorn.setup(creator)

fun getInjector() = InjectorObjC(popKorn())

B) From your IOS project you will need to initialize PopKorn at the beginning of your app (AppDelegate):

BridgeKt.doInit { (clazz) -> PopkornMapping in
            return clazz.alloc() as! PopkornMapping
        }

C) To be used anywhere like this in ObjectiveC / Swift code

let injector = BridgeKt.getInjector()

let helloWorld = injector.inject(clazz: HelloWorld.self) as! HelloWorld

You can also use runtime injections

let someInstance = SomeType()

injector.addInjectable(instance: someInstance, clazz: SomeType.self)

let copy1 = injector.inject(clazz: SomeType.self) as! SomeType // Will inject someInstance

injector.removeInjectable(clazz: SomeType.self)

let copy2 = injector.inject(clazz: SomeType.self) as! SomeType // Will fail, because SomeType is not injectable anymore

Using JS / Native

PopKorn provides basic support to JS / Native platforms. In your multiplatform project, write a File on your JS / Native module and add this function:

fun init() {
    val resolvers: Set<Mapping> = hashSetOf(/* LOCATE ALL RESOLVER CLASSES OF TYPE MAPPING THAT POPKORN AUTOGENERATED */)
    val providers: Set<Mapping> = hashSetOf(/* LOCATE ALL PROVIDER CLASSES OF TYPE MAPPING THAT POPKORN AUTOGENERATED */)
    cc.popkorn.setup(resolvers, providers)
}

then call it somewhere to initialize PopKorn. For now, injections for JS / Native can only be done from your multiplatform project. Injections from JS / Native code is not yet available.

More Examples

You can find out more examples in popkorn-example project

interface Location

@Injectable
class RealLocation : Location {

    constructor() : this() {
        // Get LocationManager.GPS_PROVIDER
    }

    @ForEnvironemnt("network")
    constructor() : this() {
        // Get LocationManager.NETWORK_PROVIDER
    }
}

@Injectable(scope = Scope.BY_NEW)
@ForEnvironment("fake")
class FakeLocation : Location

and then

val r1 = inject<Location>() // This will inject a persistent instance of RealLocation to get GPS locations
val r2 = inject<Location>("network") // This will inject a persistent instance RealLocation to get Network locations
val r2 = inject<Location>("fake") // This will inject a volatile instance of FakeLocation

or use it in any constructor of other injectable classes:

constructor(real:Location, @WithEnvironment("fake") fake:Location, @WithEnvironment("network") network:Location) {}

License

Copyright 2019 Pau Corbella

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Comments
  • Assisted Inject

    Assisted Inject

    Would be nice if you added something like Assisted Inject.

     @AssistedInject
    class MyPresenter(foo:String, @Assisted bar:String) {
      
      @AssistedInject.Factory
      interface Factory {
       fun create(bar:String): MyPresenter
      }
    }
    
    enhancement 
    opened by rvdsoft 2
  • Windows Improvements

    Windows Improvements

    • Fix a bug when using PopKorn on Windows machines
    • Check PopKorn works in all machines via GitHub Actions
    • Enable Windows x64 target

    To publish PopKorn in MavenCentral with all target we will need a new GitHub Action which uses a Matrix (macOS and Windows OS at least) so we can get all artifacts (macOS + iOS + Windows + Linux + JVM)

    opened by JavierSegoviaCordoba 0
  • Add a GitHub Actions workflow to build all projects and run all tests

    Add a GitHub Actions workflow to build all projects and run all tests

    To be able to build all projects the pk-publish precompiled plugin was forcing the usage of a local.properties file plus multiple properties.

    Now this file is optional (necessary for GitHub Actions because it doesn't exist). And if it exists and some property is missing (for example, local.properties is automatically created in Windows), the projects build instead of failing.

    This allows to the contributors build all projects without having to use fake properties in local.properties.

    opened by JavierSegoviaCordoba 0
  • [SOLVED] iOS: Could not find Provider for this class. Did you forget to add @Injectable?

    [SOLVED] iOS: Could not find Provider for this class. Did you forget to add @Injectable?

    I'm following the setup instructions but running into an issue trying to inject a dependency on iOS in a kotlin multiplatform project. I have:

    • added the Bridge.kt file in my iOS source set with the following contents:
    fun init(creator: (ObjCClass) -> Mapping) = setup(creator)
    
    fun getInjector() = InjectorObjC(popKorn())
    
    • created a sample file PopKoIos.kt in my common source set:
    @Injectable
    class PopKoIos {
        fun te() = println("TE")
    }
    
    • initialized PopKorn from AppDelegate:
    BridgeKt.doInit { (clazz) -> PopkornMapping in
        return clazz.alloc() as! PopkornMapping
    }
    
    • injected into my swift file:
    let injector = BridgeKt.getInjector()
    let helloWorld = injector.inject(clazz: PopKoIos.self) as! PopKoIos <- THIS FAILS
    helloWorld.te()
    

    I am linking my KMM module using the following command ./gradlew :kmm-core:embedAndSignAppleFrameworkForXcode but I don't see kapt running in the logs as with Android.

    Am I missing something?

    opened by gchristov 1
  • [Question] Is the project dead ?

    [Question] Is the project dead ?

    I was looking for a DI lib working with KMM and found this one very attractive with the "no boilerplate" policy. Making me think about the awesome service locator get_it for Flutter where everything can be done with just annotation like here.

    Is the project still maintained ?

    opened by tperraut 1
  • Error when try to inject a class that inherits an generic abstract class

    Error when try to inject a class that inherits an generic abstract class

    Hello,

    I am getting an error when trying to make @Injectable a class that inherits an abstract class that has generic parameters.

    This is the class injectable:

    @Injectable(scope = Scope.BY_USE)
    class GetContactListUseCase(private val repository: ContactRepository): UseCase<Contact, Filter>() {
    
        override suspend fun run(params: Filter): User {
            return repository.getContactList(params)
        }
    
    }
    
    
    abstract class UseCase<Type, in Params> where Type: Any {
    
        protected abstract suspend fun run(params: Params): Type
    
        suspend operator fun invoke(params: Params): Result<Type> {
            return withContext(ioDispatcher) {
                try {
                    successOf(run(params))
                } catch (e: Exception) {
                    failureOf(RuntimeException("Use case error => ${e.message}"))
                }
            }
        }
    }
    

    And this is the error I am getting when I try to compile.

    2 type arguments expected for class UseCase<Type : Any, in Params>

    opened by danielriverolosa 0
  • Compile time safety

    Compile time safety

    If an @Injectable is missing, the app should not compile instead of crashing in runtime.

    Because PopKorn doesn't need components or modules, it is relatively strange that it happens, but it is a highly demanded feature and, for example, it can happen that someone forgets to annotate as @Injectable an implementation.

    opened by JavierSegoviaCordoba 0
Owner
Pau Corbella
Pau Corbella
An Easy-to-use Kotlin based Customizable Modules Collection with Material Layouts by BlackBeared.

Fusion By BlackBeared An Easy-to-use Kotlin based Customizable Library with Material Layouts by @blackbeared. Features Custom Floating Action Buttons

Sandip Savaliya 38 Oct 5, 2022
A smart colored time selector. Users can select just free time with a handy colorful range selector.

Colored Time Range Selector A smart colored time range selector. Users can select just free time with a handy colorful range selector. Screen Shots Fe

Ehsan Mehranvari 154 Oct 3, 2022
An Android app with many challenge modules and SOLID at all

android-super-app An Android app with many challenge modules and SOLID at all. Features Kotlin Coroutines with Flow (State Flow) Kotlin Serialization

Thiago Santos 21 Nov 28, 2022
A FDPClient fork , It aims to add more modules.

LightClient A FDPClient fork , It aims to add more modules. You can download development version at Github-Actions , Release at Release Only running o

Ad973_ 3 Aug 26, 2021
A library for building Java only Zygisk/Riru modules.

A library for building Java only Zygisk/Riru modules.

Kr328 19 Dec 17, 2022
A plugin for Termux to use native Android GUI components from CLI applications.

Termux:GUI This is a plugin for Termux that enables command line programs to use the native android GUI. In the examples directory you can find demo v

null 342 Dec 23, 2022
ZoomHelper will make any view to be zoomable just like Instagram pinch-to-zoom

ZoomHelper ZoomHelper will make any view to be zoomable just like the Instagram pinch-to-zoom. ?? Installation ZoomHelper is available in the JCenter,

AmirHosseinAghajari 238 Dec 25, 2022
Just an app with lame dad jokes content to fill up your day.

Just an app with lame dad jokes content to fill up your day. MVP This MVP version features: Feed walks you through the latest dad jokes, Browse back s

null 240 Dec 20, 2022
Minecraft Server Software specially designed for Thicc SMP. Here on GitHub without the private patches, just a normal hybrid JettPack-Pufferfish-Empirecraft fork

AlynaaMC A private, custom server software for Thicc SMP and a fork of Pufferfish. Here on GitHub with patches from JettPack, Airplane and Pufferfish

ThiccMC 14 Dec 31, 2021
LifecycleMvp 1.2 0.0 Kotlin is MVP architecture implementation with Android Architecture Components and Kotlin language features

MinSDK 14+ Download Gradle Add to project level build.gradle allprojects { repositories { ... maven { url 'https://jitpack.io' }

Robert 20 Nov 9, 2021
Kotlin Multiplatform lifecycle-aware business logic components (aka BLoCs) with routing functionality and pluggable UI (Jetpack Compose, SwiftUI, JS React, etc.), inspired by Badoos RIBs fork of the Uber RIBs framework

Decompose Please see the project website for documentation and APIs. Decompose is a Kotlin Multiplatform library for breaking down your code into life

Arkadii Ivanov 819 Dec 29, 2022
📚 Sample Android Components Architecture on a modular word focused on the scalability, testability and maintainability written in Kotlin, following best practices using Jetpack.

Android Components Architecture in a Modular Word Android Components Architecture in a Modular Word is a sample project that presents modern, 2020 app

Madalin Valceleanu 2.3k Jan 4, 2023
To illustrate the clean architecture and modularisation with other components.

CleanNews A news app that provides data from mediastack API using clean architecture, kotlin coroutines, architectural pattern (MVI) with Mavericks. .

Yves Kalume 4 Feb 13, 2022
Multi module architecture Android template project using MVVM, Dagger-Hilt, and Navigation Components

ModularAppTemplate An Android template project following a multi module approach with clean architecture. It has been built following Clean Architectu

Mbuodile Obiosio 7 May 23, 2022
Example Multi module architecture Android project using MVVM, Dynamic Features, Dagger-Hilt, Coroutines and Navigation Components

ModularDynamicFeatureHilt An Android template project following a multi module approach with clean architecture. It has been built following Clean Arc

Mbuodile Obiosio 25 Nov 23, 2022
This Project for how to use MVVM , state flow, Retrofit, dagger hit, coroutine , use cases with Clean architecture.

Clean-architecture This Project for how to use MVVM , state flow, Retrofit, dagger hit, coroutine , use cases with Clean architecture. Why i should us

Kareem Aboelatta 10 Dec 13, 2022
AbstractMvp 0.8 0.0 Kotlin is a library that provides abstract components for MVP architecture realization, with problems solutions that are exist in classic MVP.

MinSDK 14+ AbstractMvp AbstractMvp is a library that provides abstract components for MVP architecture realization, with problems solutions that are e

Robert 12 Apr 5, 2022
A fork of our clean architecture boilerplate, this time using the Android Architecture Components

Android Clean Architecture Components Boilerplate Note: This is a fork of our original Clean Architecture Boilerplate, except in this repo we have swi

Buffer 1.3k Jan 3, 2023