Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.

README.md

Build Status Kotlin version badge Maven Central License

🧲 Magnet

Magnet is a concise, scope tree based Dependency Injection (DI) library designed for highly modular Android applications. It consists of two parts: an annotation processor (Kotlin) and a reflection free runtime library (Java + Kotlin).

Design

Magnet defines and opetates on two core concepts: Scopes and Instances.

Scope is a container for instances. Scopes can be combined into a hierarchical tree by referencing parent scopes. The most top scope of the tree hierarchy, which has no parent scope, is called the root scope.

Instance is a concrete occurance of an injected type. Instances can be allocated in scopes (scoped instances) or outside of scopes (unscoped instances).

The Dependency Rule

Scopes depend on each other using the strong dependency rule - scope dependency can only point torwards its parent scope. The dependency direction between two scopes enforces the direction of dependencies between instances allocated in those scopes. Instances allocated in a parent scope can know nothing about instances allocated in its child scopes. This simple design rule helps preventing memory leaks and allows safe disposal of child scopes and garbage collecting instances allocated there.

Getting Started

In the example below we will compose a very naive MediaPlayer which loads media using a MediaLoader and then plays the media.

fun main() {
   val rootScope = MagnetScope.createRootScope()
   val playerScope = rootScope.createSubscope {
      bind(Uri.parse("https://my-media-file"))
   }
   
   // mark 1
   val mediaPlayer = playerScope.getSingle<MediaPlayer>()
   mediaPlayer.playWhenReady()
   
   // mark 2
   Thread.sleep(5000)
   playerScope.dispose()
   
   // mark 3
}

// MediaPlayer.kt
interface MediaPlayer {
   fun playWhenReady()
}

@Instance(type = MediaPlayer::class, disposer = "dispose")
internal class DefaultMediaPlayer(
   private val assetId: Uri,
   private val mediaLoader: MediaLoader
) : MediaPlayer {
   override fun playWhenReady() { ... }
   fun dispose() { ... }
}

// MediaLoader.kt
interface MediaLoader {
   fun load(mediaUri: Uri): Single<Media>
}

@Instance(type = MediaLoader::class)
internal class DefaultMediaLoader() : MediaLoader {
   override fun load(mediaUri: Uri): Single<Media> { ... }
}

The diagram below shows how Magnet manages the scope hierarchy when different marks of the main function are reached.

At Mark 1, two scopes are created and the Uri instance gets bound into the playerScope.

At Mark 2, mediaPlayer and mediaLoader instances get allocated in respective scopes. mediaPlayer is allocated in the playerScope because one of its dependencies, the Uri, is located in playerScope. Magnet cannot move mediaPlayer up to the rootScope because this would break the dependency rule described above. mediaLoader has no dependencies, that's why it is allocated in the rootScope. This instance allocation logic is specific to Magnet DI and is called auto-scoping. See developer documentation for more detail.

At Mark 3, the playerScope gets disposed and all its instances are garbage collected.

For more information refer to Magnet documentation.

Documentation

  1. Developer Guide
  2. Dependency auto-scoping
  3. Scope Inspection
  4. How to Inject Android ViewModels
  5. Blog: Magnet - an alternative to Dagger
  6. Co2Monitor sample app
  7. Another sample app

Features

  • Minimalistic API
  • Auto-scoping of instances
  • Hierarchical, disposable scopes
  • Kotlin friendly annotation
  • Injection into Kotlin constructors with default arguments
  • Injection from binary libraries
  • Dependency inversion
  • No direct references to Magnet generated code
  • No reflection for injection, apt generated factory classes
  • Extensible - some magnetx extensions are available
  • Customizable - custom factories and instance selectors

Why Magnet?

Magnet was crafted with simplicity and development speed in mind. It lets developers spend less time on DI configuration and do more other stuff, also more mistakes when used inattentively. Magnet motivates you writing highly modular apps because it makes DI so simple. It can even inject instances from the libraries added in build scripts without necessity to adapt source code. Magnet could be interesting for those, who needs an easy to configure and simple DI with more runtime control.

Why not Magnet?

If compile time consistency validation is your highest priority, I recommend using awesome Dagger2 instead. You will spend slightly more time on DI configuration but Dagger2 lets you keep it highly consistent and error prone (in most cases) very early in the development cycle - at compile time.

Peace ✌️ and have fun.

Gradle

Kotlin

repositories {
   mavenCentral()
}
dependencies {
   api 'de.halfbit:magnet-kotlin:<version>'
   kapt 'de.halfbit:magnet-processor:<version>'
}

Java

repositories {
   mavenCentral()
}
dependencies {
   api 'de.halfbit:magnet:<version>'
   annotationProcessor 'de.halfbit:magnet-processor:<version>'
}

Proguard & R8

-keep class magnet.internal.MagnetIndexer { *; }

License

Copyright 2018-2020 Sergej Shafarenka, www.halfbit.de

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.
You can’t perform that action at this time.