OOverkommelig

Manageable dependency injection for Kotlin

Welcome to OOverkommelig

Dependency injection that is easy to read and write. Written in and for Kotlin.

Current status: beta. API is stable. Documentation is needed.

To use OOverkommelig in your projects, add the following to your Gradle scripts:

repositories {
    mavenCentral()
}

dependencies {
    // For JavaScript (which does not support all features):
    implementation 'org.ooverkommelig:ooverkommelig-js:1beta4'
    // For the JVM:
    implementation 'org.ooverkommelig:ooverkommelig-jvm:1beta4'
}

And if you use Kotlin for your Gradle scripts:

repositories {
    mavenCentral()
}

dependencies {
    // For JavaScript (which does not support all features):
    implementation("org.ooverkommelig:ooverkommelig-js:1beta4")
    // For the JVM:
    implementation("org.ooverkommelig:ooverkommelig-jvm:1beta4")
}

Get notified of new releases:

Example

Here is some (very simplistic) example code to give you a taste. For more complex object graphs see the examples:

// An object graph definition is a self-contained unit consisting of multiple
// object definitions, i.e. it can be instantiated.
//
// A single interface parameter with one or more functions provides external
// objects (or object definitions), allowing configuration of the object
// graph.
class ApplicationOgd(provided: Provided) : ObjectGraphDefinition() {
    // Explicit declaration of the dependencies of an object graph, so clients
    // of the graph quickly know what they need to provide to be able to use
    // the graph.
    //
    // Functions instead of values are used, to reduce the boilerplate in
    // certain situations. For example, when using a definition of one sub
    // graph for the dependency of another sub graph of the same parent graph.
    // See `org.ooverkommelig.examples.dagger.simple.CoffeeAppOgd` in the
    // examples.
    //
    // "Definition" can occur quite a number of times in (sub) graph
    // definitions. Its type alias "D" can be used to reduce the amount of
    // boilerplate.
    interface Provided {
        fun applicationLogDirectory(): Definition<File>
    }

    // This is "public", which is not a requirement but reduces the amount of
    // boilerplate.
    //
    // In this case all dependencies have to be provided by the client of
    // "ApplicationOgd". But it is also possible that "ApplicationOgd"
    // provides dependencies by using multiple sub graph definitions, and
    // using an object definition from one for the dependency of another.
    val appSgd = add(ApplicationSgd(object : ApplicationSgd.Provided {
        override fun logDirectory() = provided.applicationLogDirectory()
    }))

    // By creating an instance of this class and requesting one or more
    // objects, the defined object will actually be created. Note that
    // OOverkommelig also allows for specifying eagerly created objects that
    // will be created once a graph instance is created.
    //
    // The graph instance implements "Closeable", which enables clean-up of
    // objects in the graph. When you only need a graph for a single operation
    // you can use the following code. This ensures that all objects are
    // disposed of once the operation finished:
    //     ApplicationOgd(object : ApplicationOgd.Provided { ... })
    //             .Graph().use { graph ->
    //                  graph.someService().doYourThing()
    //             }
    //
    // In cases where you need the graph for multiple operations, you create
    // and store it. When it is no longer needed, you dispose of it.
    inner class Graph : DefinitionObjectGraph() {
        fun someService() = req(appSgd.someService)
    }
}

// A sub graph definition is not self-contained, i.e. it is intended to be
// used in one or more object graphs. The object graph must provide the
// dependencies for the sub graphs it uses, either by using object definitions
// of other sub graphs or by requiring the object graph client to provide
// them.
//
// Sub graph definitions are the reusable pieces of objects and wiring that
// can be used in multiple contexts. For example:
// - The object definitions that are the same for both Android and iOS apps.
// - Infrastructure objects that are the same for all applications that you
//   build.
class ApplicationSgd(provided: Provided) : SubGraphDefinition() {
    interface Provided {
        fun logDirectory(): Definition<File>
    }

    // This is a very simple object definition as it only specifies the
    // creation lambda. OOverkommelig also support wiring (allowing object
    // cycles to be created), initialization and disposal.
    //
    // The creation lambda of this object will be invoked only once, and its
    // result will be returned to any other object needing the log file.
    // OOverkommelig supports a range of object definitions: constant, once,
    // always, parameterized once, parameterized always, aspect once (JVM
    // only) and aspect always (JVM only).
    //
    // Again: The values are public to reduce the amount of boilerplate, but
    // this is not a requirement.
    //
    // "req(...)" indicates that the dependency is required. OOverkommelig
    // also supports optional dependencies using "opt(...)".
    //
    // The object definitions allow enormous flexibility: You can use all
    // language constructs and all APIs to determine what should be
    // constructed, how it should be configured, etc. You can do much more
    // than shown in this simplistic example.
    val logFile by Once { File(req(provided.logDirectory()), "log.txt") }

    val log by Once { Log(req(logFile)) }

    val someService by Once { SomeService(req(log)) }
}

The example above and other examples can be found in the repository.

Advantages

Some of the big advantages of OOverkommelig compared to other dependency injection containers are:

  • The ability to quickly navigate to the definition of a dependency.

  • The ability to quickly navigate from a dependency to its usages.

  • Compile-time check for graph completeness, i.e. compile errors if you reference a dependency that is not defined.

  • Compile-time check of type correctness, i.e. you cannot use an object definition for another type for a dependency.

  • Compile-time check of required dependencies, i.e. you cannot use an optional object definition for a required dependency.

  • Compile-time check of unused dependencies, i.e. your IDE will mark object definitions that are not used by other object definitions.


Download .zip Download .tar.gz
Fork me on GitHub