Minimalist dependency injection library for Kotlin.

Overview

Kapsule Build Status

Minimalist dependency injection library for Kotlin.

Why create another dependency injection library? Here are the objectives pursued by Kapsule:

  • Simple features that most projects will have use for
    • Alternative for projects whose dependency injection needs are quite basic
  • Keep the method count to a minimum
    • Dependency injection shouldn't take thousands of methods to implement
  • No annotation processing
    • No need for lateinit on properties and they can be private and read-only
  • No magic, keep everything as a hard reference
    • Reading code is easier when you can click through all the references in your IDE
  • Utilize the power of Kotlin
    • Use language features to simplify code instead of focusing on Java compatibility

To accomplish all of these, Kapsule is based on delegation and delegated properties.

Table of Contents

Getting Started

Download

To use Kapsule in your project, include it as a dependency.

Using Gradle:

dependencies {
    compile "net.gouline.kapsule:kapsule-core:1.1"
}

Or Maven:

<dependency>
    <groupId>net.gouline.kapsule</groupId>
    <artifactId>kapsule-core</artifactId>
    <version>1.1</version>
</dependency>

Create a Module

Define a module to provide the injected values.

This can be any Kotlin class, so feel free to initialize properties however you like (including lazy expressions and custom getters).

class Module {
    val name = "SomeName"
    val manager get() = Manager()
}

Our simple example provides the same instance of name and a new instance of Manager for every property that requires it.

Store Module Instance

Store the root module in your application context (this will depend on your framework).

On Android, you would use the Application instance for this. Don't forget to declare the CustomApplication class in AndroidManifest.xml.

class CustomApplication : Application() {
    private var module = Module()
	
    companion object {
        fun module(context: Context) = (context.applicationContext as Application).module
    }
}

The static function module() is how you will access the stored module from activities and fragments.

Inject Properties

Now the injection target needs to be adjusted as follows:

  • Implement Inject<Module> with the module you're injecting
  • Declare the dependencies using required (or optional) references
  • Retrieve the module instance from the application context
  • Call inject() on the module to initialise the values

Looking at the Android example, let's say you have a class ExampleActivity that needs these injected values. The function passed for each declaration retrieves the value from Module that the given property expects.

class ExampleActivity : AppCompatActivity(), Injects<Module> {
    private val name by required { name }
    private val manager by required { manager }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        inject(CustomApplication.module(this))
    }
}

That's it, properties name and manager can now be used!

Advanced Setup

The steps above show the most basic setup, which can be extended for more advanced use cases.

Module Implementations

The basic setup uses one module called Module, but what if you need another implementation that returns stub values for tests?

You can define an interface and provide two different implementations:

interface Module {
    val name: String
    val manager: Manager
}

The module used before can be the main implementation:

class MainModule : Module {
    override val name = "SomeName"
    override val manager get() = Manager()
}

And another TestModule can return the stub values:

class TestModule : Module {
    override val name = "SomeTestName"
    override val manager get() = TestManager()
}

Your injection routine stays the same, except now Module is a generic interface, you just have to provide the appropriate module through your application context (i.e. what is depicted as Application.module in the example).

Multiple Modules

You may also want to define multiple modules (with some logical separation) combined into one interface used for injection. Let's say you have a CoffeeModule as follows:

interface CoffeeModule {
    val coffeeType: String
}

And another TeaModule:

interface TeaModule {
    val teaType: String
}

As described in the previous section, you can have one or more implementations for each module (omitted for brevity) that you can combine them into one Module:

class Module(coffee: CoffeeModule, tea: TeaModule) : 
    CoffeeModule by coffee, TeaModule by tea

Now your Module contains all the properties and functions of CoffeeModule and TeaModule courtesy of Kotlin's delegation support.

When you're instantiating the global module (stored in your application context), you can provide the required implementation for each submodule:

class Application {
    val module = Module(
        coffee = MainCoffeeModule() // or TestCoffeeModule()
        tea = MainTeaModule() // or TestTeaModule()
    )
}

Optional Delegates

So far you've only seen non-null values, but what happens if you need to inject a nullable value? You can use the optional function on your Kapsule:

val firstName by required { firstName }
val lastName by optional { lastName }

Given both fields are strings, firstName is String, while lastName is String?.

Unlike non-null properties, nullable ones can be read even before injection (the former would throw KotlinNullPointerException), they will just be null.

Variable Delegates

In most cases you would make the injected properties val, however there's no reason it can't be a var, which would allow you to reassign it before or after injection.

var firstName by required { firstName }

init {
    firstName = "before"
    kap.inject(Application.module)
    firstName = "after"
}

Note that any delegates can be injected repeatedly, regardless of whether they're val or var, because the initialized value is contained within the delegate and it's a nullable var.

Transitive Dependencies

Consider UserDao and an authenticator Auth that depends on it. Except the former is provided by DataModule, but the latter comes from LogicModule.

class Module(
        data: DataModule, logic: LogicModule) : 
        DataModule by data, LogicModule by logic

interface DataModule {
    val userDao: UserDao
}

interface LogicModule {
    val auth: Auth
}

This is where the transitive injection comes in, the UserDao can be injected into the LogicModule.

class MainDataModule : DataModule {
    override val userDao get() = UserDao()
}

class MainLogicModule : LogicModule, Injects<Module> {
    private val userDao by required { userDao }
    override val auth get() = Auth(userDao)
}

Looks good, but it won't work without two modifications to Module:

  • It has to implement HasModules to suggest that it contains submodules
  • HasModules requires you to define the module instances
class Module(
        data: DataModule, logic: LogicModule) : 
        DataModule by data, LogicModule by logic,
        HasModules {
    override val modules = setOf(data, logic)
}

Finally, when instantiating the module, you need to call transitive() on the module to traverse the tree and inject the Module into any submodules depending on it.

val module = Module(MainDataModule(), MainLogicModule()).transitive()

Note that circular dependencies are not supported, because resolution is performed iteratively and in a single pass. Take care in defining your dependency structure to avoid this situation.

Complex Dependencies

Traditionally, injected values are kept in the same structure as they are provided by the modules. However, considering that injection functions in Kapsule are essentially just future values that will become accessible after injection happened, you can do anything you would do outside of that function, inside.

Consider the example module implementations from the transitive dependencies section:

class MainDataModule : DataModule {
    override val userDao get() = UserDao()
}

class MainLogicModule : LogicModule, Injects<Module> {
    private val userDao by required { userDao }
    override val auth get() = Auth(userDao)
}

You'll notice that the userDao is being provided by the MainDataModule as a new instance each time. What if you wanted to reuse the same instance through the whole life of the module? You would just assign it as a value, rather than return it from a custom getter.

class MainDataModule : DataModule {
    override val userDao = UserDao()
}

But what about if you want to do the same with auth in MainLogicModule? You can't just assign Auth(userDao) to auth, because Auth() constructor is called when the MainLogicModule is instantiated and by that time the userDao hasn't been injected yet (remember, that gets done in the transitive() call). That means you need to instantiate auth in the same future, when userDao will become available.

class MainLogicModule : LogicModule, Injects<Module> {
    override val auth by required { Auth(userDao) }
}

Notice that you no longer need the userDao separately for that. Also note that required/optional choice now depends on what the return of the new function is, rather than whether or not userDao by itself is.

You can do other stuff in your injection functions, like convert the injected value into something else that you need for the current context.

class ExampleActivity : AppCompatActivity(), Injects<Module> {
    private val authHttpClient by required { auth.httpClient }
    
    ...
}

In the above example, we're not just injecting the auth dependency from before, but also retrieving its httpClient property.

While you're not technically limited by what you can do inside these injection functions, try not to overdo it. It's similar to data bindings, where you can perform calculations and other complex calls right in the template file, but you know you shouldn't.

Manual Injection

While the more convenient way to inject modules is by implementing the Injects<Module> interface, you may want to split the injection of separate modules (e.g. for testing). This can be done by creating separate instances of Kapsule<Module> and calling the injection methods on it.

class Screen {
    private val kap = Kapsule<Module>()
    private val name by kap.required { name }
    private val manager by kap.required { manager }

    init {
        kap.inject(Application.module)
    }
}

Samples

For sample projects using Kapsule, see the samples directory.

Javadocs

License

This project is licensed under the terms of the MIT license. See the LICENSE file.

Comments
  • Exception using injected property to initialize other properties

    Exception using injected property to initialize other properties

    Here is a simple example with an injected property where I want to use the injected property to define another property:

    class TestModule
    {
        val bar = "bar"
    }
    
    class Test(module: TestModule): Injects<TestModule>
    {
        init { inject(module) }
        val foo by required { bar }
        val foobar = "testing " + foo
    }
    
    fun main(args: Array<String>) { println(Test(TestModule()).foobar) }
    

    Running this gives an exception:

    Exception in thread "main" kotlin.KotlinNullPointerException
    	at space.traversal.kapsule.Delegate$Required.getValue(Delegate.kt:44)
    	at com.nobleworks_software.gmail_handler.Test.getFoo
    	at com.nobleworks_software.gmail_handler.Test.<init>
    

    i have verified that the call to inject happens before the foobar expression is evaluated and that the lambda passed to required is never invoked.

    enhancement 
    opened by dalewking 8
  • Limitation in transitive dependencies or bug?

    Limitation in transitive dependencies or bug?

    Hello, It seems I found a bug or a limitation on transitive dependencies between 2 modules:

    
    class Deps(
            a: DependenciesA,
            b: DependenciesB
    ) : DependenciesA by a, DependenciesB by b, HasModules {
        override val modules = setOf(a, b)
    }
    
    interface DependenciesA {
        val foo: String
        val foobar: String
    }
    
    class ModuleA : DependenciesA, Injects<Deps> {
        override val foo: String = "foo"
        override val foobar: String by required { foo + bar }
    }
    
    interface DependenciesB {
        val bar: String
    }
    
    class ModuleB : DependenciesB, Injects<Deps> {
        override val bar: String by required { if (foo == "foo") "bar" else "i dont know" }
    }
    
    val deps = Deps(ModuleA(), ModuleB()).transitive()
    
    

    The dependency flow is: a.foobar -> b.bar -> a.foo Result: When injecting Deps.a.foobar, it throws a NPE because b.bar is null.

    I didn't dig into Kapsule, but I think it makes sense because when ModuleA is built, all dependencies are resolved directly, with a dependency on ModuleB which has itself a dependency on ModuleA.

    An easy fix would be to split dependencies into 3 modules, in order to not have a "cyclic dependency on Module level". However it would be nice to support this, because this is fine on dependency level.

    What do you think? Thanks.

    enhancement wontfix 
    opened by lukaspili 3
  • Possible leak in CallerMap?

    Possible leak in CallerMap?

    Hi!

    Really great stuff here, love how simple it is to use and how concise the code base is compared to other dependency injection libraries out there.

    However, looking through your code, I noticed you’re extending WeakHashMap to allow for a simple caching mechanism of the last value stored, and I couldn’t help myself but wondering if keeping this reference outside of the “actual” map implementation could result in a leak somehow. Imagine the following:

    class HomeModule(private val activity: HomeActivity) {
      // EDITED: Typo had this return a HomeModule before ...
      val model by lazy { HomeModel(activity) }
    }
    

    Now, in our activity:

    class HomeActivity : Activity, Injects<HomeModule> {
      private val model by required { model }
    }
    

    So far so good. However, when we move away from the HomeActivity, and given that we don’t do any injection in our next activity, then there will be a hard reference to the HomeModule, and therefore also the HomeActivity, which won’t get collected by the garbage collector.

    I suppose it's only a valid issue for the last stored key/value pair, seeing as you're extending WeakHashMap.I suspect that's also why you did just that.

    Now, if you already thought about this for the last key/value pair as well and have a solution already in your codebase, then please correct me. :smile:

    opened by sindrenm 2
  • Added Application inside 'Getting started' section

    Added Application inside 'Getting started' section

    Hi, thanks for making Kapsule! I just started adding it to an example application and detected that some details are missing in the Getting started section. Without inject(Application.module(this)) I cannot inject properties into my Activity.

    opened by timobaehr 1
  • Transitive dependencies

    Transitive dependencies

    With your solution, how would you manage the dependencies between modules?

    For example, ModuleA exposes a retrofit (Retrofit.Builder().[…].build()) and ModuleB exposes the API by using this retrofit object (retrofit.create(MyApi::class.java)).

    Should ModuleB implements Injects<ModuleA> or will you give someway ModuleA a reference to ModuleB?

    I’ve checked the samples on GitHub but I can’t see the proper way to manage this.

    Source: https://medium.com/@lcor1979/very-nice-solution-c4410253e712 (@lcor1979)

    enhancement question 
    opened by gouline 1
  • Transitive dependency support

    Transitive dependency support

    • Create HasModules interface for modules with transitive submodules
    • Transitive dependencies require separate transitive() call on the module
    • Move required(), optional() and inject() out of Injects<M> into extension functions to avoid multiple submodules clashing on common methods in one root module delegation
    opened by gouline 0
  • Automatic injectors

    Automatic injectors

    • Automatic retrieval of injectors from Injects<Module> interface
    • Caller map with last entry caching
    • Update sample projects
    • Tests and documentation
    opened by gouline 0
Releases(v1.1)
Owner
Mike Gouline
Mike Gouline
Compile-time dependency injection for Kotlin Multiplatform

Kinzhal Kinzhal is a Kotlin Multiplatform library for compile-time dependency injection. The goal is to emulate basic features of Dagger to achieve si

Artem Daugel-Dauge 59 Dec 21, 2022
A pragmatic lightweight dependency injection framework for Kotlin developers.

A pragmatic lightweight dependency injection framework for Kotlin developers. Koin is a DSL, a light container and a pragmatic API

insert-koin.io 11 Dec 14, 2022
Kotlin dropwizard app running on Java 11. With Guice injection loaded.

hello world How to start the hello world application Run mvn clean install to build your application Start application with java -jar target/dropwizar

null 0 Nov 24, 2021
A Zero-Dependency Kotlin Faker implementation built to leave you fully satisfied

Satisfaketion A Zero-Dependency Kotlin Faker implementation built to leave you fully satisfied ?? ... With your fake data How to Install ?? Satisfaket

Ryan Brink 7 Oct 3, 2022
This lib is the framework for dependency tasks, specially for the asynchronous tasks.

DependencyTask This lib is the framework for dependency tasks, specially for the asynchronous tasks. Backgroud Image that there is a progress with som

null 1 May 6, 2022
My own approach to what I think an Android MVVM project with Clean Architecture should look like with Dagger-Hilt as Dependency Injector engine

MVVM Project Hilt Introducción Este proyecto es mi visión particular, ni mejor ni peor (sólo una más) que cualquier otra aproximación a lo que yo enti

Antonio Fdez. Alabarce 7 Dec 16, 2022
Small kotlin library for persisting _single instances_ of kotlin data classes

PerSista Small library for persisting single instances of kotlin data classes. NB: PerSista uses typeOf() internally which is marked as @ExperimentalS

Eric Donovan 5 Nov 13, 2022
FlowExt is a Kotlin Multiplatform library, that provides many operators and extensions to Kotlin Coroutines Flow

FlowExt | Kotlinx Coroutines Flow Extensions | Kotlinx Coroutines Flow Extensions. Extensions to the Kotlin Flow library | kotlin-flow-extensions | Coroutines Flow Extensions | Kotlin Flow extensions | kotlin flow extensions | Flow extensions

Petrus Nguyễn Thái Học 151 Jan 1, 2023
Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Kotlin_practice_problems Repo: Programming problems with solutions in Kotlin to help avid Kotlin learners to get a strong hold on Kotlin programming.

Aman 0 Oct 14, 2021
Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP)

Mockative Mocking for Kotlin/Native and Kotlin Multiplatform using the Kotlin Symbol Processing API (KSP). Installation Mockative uses KSP to generate

Mockative 121 Dec 26, 2022
Kotlin-oop - Repositório criado para ser utilizado pelo projeto de Kotlin OOP desenvolvido em Kotlin nas aulas feitas através da plataforma Alura.

Projeto React OOP Repositório criado para ser utilizado pelo projeto de Kotlin OOP desenvolvido em Kotlin nas aulas feitas através da plataforma Alura

Marcos Felipe 1 Jan 5, 2022
Kotlin-koans - Kotlin Koans are a series of exercises to get you familiar with the Kotlin Syntax

kotlin-koans-edu Kotlin Koans are a series of exercises to get you familiar with

null 1 Jan 11, 2022
🔥The Android Startup library provides a straightforward, performant way to initialize components at the application startup. Both library developers and app developers can use Android Startup to streamline startup sequences and explicitly set the order of initialization.

??The Android Startup library provides a straightforward, performant way to initialize components at the application startup. Both library developers and app developers can use Android Startup to streamline startup sequences and explicitly set the order of initialization.

Rouse 1.3k Dec 30, 2022
Ksp-di-library - Small library for DI in KMM apps

DI-KSP Small library for DI in KMM apps. Uses KSP for processing DI annotations:

Anna Zharkova 3 Feb 6, 2022
An library to help android developers working easly with activities and fragments (Kotlin version)

AFM An library to help android developer working easly with activities and fragments (Kotlin) Motivation Accelerate the process and abstract the logic

Massive Disaster 12 Oct 3, 2022
Kotlin Multiplatform String markup library

Thistle Kotlin multiplatform String markup library, inspired by SRML. Thistle is a common parser which produces an AST that can be rendered to a varie

Copper Leaf 52 Dec 30, 2022
Amazing and easy to use Accordion Library for Android built with kotlin

AccoLib An easy-to-use, amazing Accordion Library for Android built with kotlin. It reduces the amount of code needed to make Accordions in android, w

Gourav Khunger 6 Jul 4, 2022
Kadrekka is a library that aims to make Kotlin more accessible to the northern italian population

Kadrekka Kadrekka is a library that aims to make Kotlin more accessible to the northern italian population. It provides lots of utility functions to m

Marco 8 May 9, 2021
This Kotlin Multiplatform library is for accessing the TMDB API to get movie and TV show content. Using for Android, iOS, and JS projects.

Website | Forum | Documentation | TMDb 3 API Get movie and TV show content from TMDb in a fast and simple way. TMDb API This library gives access to T

Moviebase 37 Dec 29, 2022