`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 occurrence 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 towards 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.
```kotlin
fun main() {
val rootScope = MagnetScope.createRootScope()
val playerScope = rootScope.createSubscope {
bind(Uri.parse("https://my-media-file"))
}
// mark 1
val mediaPlayer = playerScope.getSingle
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](https://www.halfbit.de/magnet/developer-guide/)
2. [Dependency auto-scoping](https://github.com/beworker/magnet/wiki/Dependency-auto-scoping)
3. [Scope Inspection](https://github.com/beworker/magnet/wiki/Scope-Inspection)
4. [How to Inject Android ViewModels](https://github.com/beworker/magnet/issues/69#issuecomment-468033997)
5. [Blog: Magnet - an alternative to Dagger](https://www.thomaskeller.biz/blog/2019/10/09/magnet-an-alternative-to-dagger/)
6. [Co2Monitor sample app](https://github.com/beworker/co2monitor/tree/master/android-client)
7. [Another sample app](https://github.com/beworker/g1)
# 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](https://github.com/google/dagger) 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
```gradle
repositories {
mavenCentral()
}
dependencies {
api 'de.halfbit:magnet-kotlin:
* // bind instances
* scope.bind(Context.class, application, "app-context");
* scope.bind(Context.class, activity, "activity-context");
*
* // get instances directly
* Context app = scope.getSingle(Context.class, "app-context");
* Context activity = scope.getSingle(Context.class, "activity-context");
*
* // get instances via constructor injection
* @Instance(type = MyImplementation.class)
* public MyImplementation(
* @Classifier("app-context") Context app,
* @Classifier("activity-context") Context activity,
* ) {
* ...
* }
*
*/
@Retention(CLASS)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface Classifier {
String NONE = "";
String value();
}
================================================
FILE: magnet/src/main/java/magnet/Factory.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet;
import org.jetbrains.annotations.NotNull;
/** Custom factory to be used with {@link Instance#factory()}. */
public interface Factory* In the example below we declare two dependent types and instantiate them in scope. * *
* @Instance(type=TypeA.class)
* class TypeA {
* TypeA() {}
* }
*
* @Instance(type=TypeB.class)
* class TypeB {
* final TypeA typeA;
* TypeB(@NonNull TypeA dependency) {
* typeA = dependency;
* }
* }
*
* ...
*
* // get instance of typeB
* TypeB typeB = scope.getSingle(TypeB.class);
*
* // typeA has been provided by Magnet
* typeB.typeA != null
*
*
*
*
* Nullability. Magnet is capable of detecting whether dependency can or cannot be null.
* If a constructor's parameter is annotated as nullable and Magnet cannot provide instance of
* parameter's type, then null is provided instead.
*
*
* Classification. Same interface can be implemented by many classes. Classifier is used * to differentiate between those implementations. See {@link Classifier} for more detail. * *
* Scoping. Magnet can bind created instances into scope for reuse. Instance can * specify whether and how its instances should be bound into the scope. See {@link Scoping} * for more detail. */ @Retention(CLASS) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Instance { /** * Type to use when annotated instance gets registered in scope. Annotated class must * actually implement this type. */ Class> type() default void.class; /** * Multiple types to use when annotated instance gets registered in scope. Annotated * class must actually implement all these types. Properties {@link #type()} and * {@code #types()} are exclusively mutual and cannot be used together. */ Class>[] types() default void.class; /** * Classifier to use when annotated instance gets registered in scope. */ String classifier() default Classifier.NONE; /** * Scoping rule to be applied when instance of annotated class gets created. */ Scoping scoping() default Scoping.TOPMOST; /** * Limit tag to be used with {@link Scoping#TOPMOST} algorithm. If a limit tag is * preset then the instance will be placed at or below the scope having same limit * tag applied to it. If multiple scopes have the same limit tag, then the closest scope * to the scope where the instance gets requested is used. */ String limitedTo() default ""; /** * Experimental. Magnet will only create instance of the annotated class if * this selector expression is true after evaluation. Magnet currently support single * type of expression: *
* android.api (comparison operator) (api version)
*
* For instance, the expression android.api >= 28 will only create
* annotated instance if Build.VERSION.SDK_INT >= 28. For the other versions
* null is returned. Make sure to use optional injection to handle
* this case.
*/
String selector() default SelectorFilter.DEFAULT_SELECTOR;
/**
* Custom factory to be used for creating instance instead of the generated one.
*/
Class extends Factory> factory() default Factory.class;
/**
* Name of optional disposer method to be called, when whole scope gets disposed.
*/
String disposer() default "";
/**
* Magnet ignores this annotation when this flag is set to true.
*/
boolean disabled() default false;
}
================================================
FILE: magnet/src/main/java/magnet/Magnet.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet;
import magnet.internal.InternalFactory;
import org.jetbrains.annotations.NotNull;
public final class Magnet {
private Magnet() { }
public static @NotNull Scope createRootScope() {
return InternalFactory.createRootScope();
}
}
================================================
FILE: magnet/src/main/java/magnet/Registry.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.CLASS;
/**
* This marker annotation instructs Magnet to generate a registry indexing all
* instances available in classpath. Index applies to sources as well as libraries.
* Apply this annotation to any interface or class in your main application module.
*/
@Retention(CLASS)
@Target({ElementType.TYPE})
public @interface Registry {}
================================================
FILE: magnet/src/main/java/magnet/Scope.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* Scope is a container for objects which are stored there at runtime.
*
*
* Binding. Binding is the way of putting objects into the scope. * *
* Scope root = Magnet.createScope() * .bind(app, Application.class) * .bind(String.class, "#FF0000", "red-color"); ** *
* Chaining. Scopes can be chained using * parent-child relation, or rather a child-parent relation because * a child scope (aka subscope) holds a reference to its parent and not * the other way around. * *
* Scope subscope = root.createSubscope() * .bind(activity, Context.class) * .bind(String.class, "#F30303", "red-color") * .bind(String.class, "#00FF00", "green-color"); ** * Provisioning. Scope has multiple get-methods for providing * objects it stores. Magnet will look through the whole scope's chain up * to the root parent scope to provide an object. First found match gets * returned. * *
* // object overwriting * String red = root.getSingle(String.class, "red-color"); // "#FF0000" (from root) * String red = subscope.getSingle(String.class, "red-color"); // "#F30303" (from subscope) * * // scope chaining * Application app = subscope.getSingle(Application.class); // app (from root) * Context context = subscope.getSingle(Context.class); // activity (from subscope) * String green = subscope.getSingle(String.class, "green-color"); // "#00FF00" (from subscope) * * // optional provisioning * String red = root.getOptional(String.class, "red-color"); "#FF0000" (from root) * String yellow = subscope.getSingle(String.class, "yellow-color"); // throws IllegalStateException * String yellow = subscope.getOptional(String.class, "yellow-color"); // null, optional was not found ** *
* Automatic binding (injection). * Magnet can instantiate {@link Instance}-annotated classes and bind their instances * into respective scopes automatically. If instantiated classes have dependencies, Magnet * will resolve those dependencies too. In this respect Magnet works as dependency injection * library. * *
In the example below Magnet will create instance of {@code toaster} by taking required * dependencies from the scopes. * *
* @Instance(type = Toaster.class)
* class Toaster {
* Toaster(
* Application app,
* @Classifier("red-color") String red,
* @Classifier("green-color") String green
* ) { ... }
*
* Toaster toaster = subscope.getSingle(Toaster.class);
* toaster != null
*
*/
public interface Scope {
/** Returns an object from the scope or {@code null}, if object was not found. */
@Nullable * In the example below, both instances {@code typeA} and {@code typeB} are * not bound into the {@code scope}. Each new {@code scope.getSingle(TypeB.class)} * call will return new instances of {@code typeA} and {@code typeB}. * *
*
* @Instance(type = TypeA.class, scoping = Scoping.UNSCOPED)
* class TypeA {
* TypeA() {}
* }
*
* @Instance(type = TypeB.class, scoping = Scoping.UNSCOPED)
* class TypeB {
* final TypeA typeA;
* TypeB(@NonNull TypeA dependency) {
* typeA = dependency;
* }
* }
*
* ...
*
* Scope scope = Magnet.createScope();
*
* TypeB typeB = scope.getSingle(TypeB.class);
* TypeA typeA = typeB.typeA;
* TypeA typeA2 = scope.getSingle(TypeA.class);
* typeA !== typeA2 // different instances
*
*
*/
UNSCOPED,
/**
* Magnet will bind created instance into the most top scope in the chain of scopes,
* where all dependencies for the created instance are still fulfilled.
*
* * Scopes in Magnet can build a chain of parent-child relations (see {@link Scope} for * more detail). This option allows binding instances of annotated class into a one * of parent scopes. Magnet goes up the scope chain and checks, whether all dependencies * for the instance can still be satisfied in that scope. The most top reached scope * with satisfied dependencies is the scope, into which the instance gets bound. * *
* In the example below both instances {@code typeA} and {@code typeB} are bound into * {@code root}. * *
*
* @Instance(type = TypeA.class, scoping = Scoping.TOPMOST)
* class TypeA {
* TypeA() {}
* }
*
* @Instance(type = TypeB.class)
* class TypeB {
* final TypeA typeA;
* TypeB(@NonNull TypeA dependency) {
* typeA = dependency;
* }
* }
*
* ...
*
* Scope root = Magnet.createScope();
* Scope scope = root.createSubscope();
*
* TypeB typeB = scope.getSingle(TypeB.class);
* TypeA typeA = typeB.typeA;
* TypeA typeA2 = scope.getSingle(TypeA.class);
* typeA === typeA2 // same instance
*
*
*
* Explanation:
*
* * In the example below we bind instance of {@code typeA} into {@code scope} and * {@code typeB} instance gets bound into the {@code scope} too. * *
*
* @Instance(type = TypeA.class, scoping = Scoping.TOPMOST)
* class TypeA {
* TypeA() {}
* }
*
* @Instance(type = TypeB.class, scoping = Scoping.TOPMOST)
* class TypeB {
* final TypeA typeA;
* TypeB(@NonNull TypeA dependency) {
* typeA = dependency;
* }
* }
*
* ...
*
* Scope root = Magnet.createScope();
* Scope scope = root.createSubscope().bind(TypeA.class, new TypeA());
*
* TypeB typeB = scope.getSingle(TypeB.class);
* TypeA typeA = typeB.typeA;
* TypeA typeA2 = scope.getSingle(TypeA.class);
* typeA === typeA2 // same instance
*
*
*
* Explanation:
*
* * In the example below, both instances {@code typeA} and {@code typeB} are bound * into {@code scope}. Each new {@code scope.getSingle(TypeB.class)} call on the * same instance of {@code scope} will return same instances of {@code typeA} and * {@code typeB}. * *
*
* @Instance(type = TypeA.class, scoping = Scoping.DIRECT)
* class TypeA {
* TypeA() {}
* }
*
* @Instance(type = TypeB.class, scoping = Scoping.DIRECT)
* class TypeB {
* final TypeA typeA;
* TypeB(@NonNull TypeA dependency) {
* typeA = dependency;
* }
* }
*
* ...
*
* Scope root = Magnet.createScope();
* Scope scope = root.createSubscope();
*
* TypeB typeB = scope.getSingle(TypeB.class);
* TypeA typeA = typeB.typeA;
* TypeA typeA2 = scope.getSingle(TypeA.class);
* typeA === typeA2 // same instance
*
*
*/
DIRECT
}
================================================
FILE: magnet/src/main/java/magnet/SelectorFilter.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet;
import org.jetbrains.annotations.NotNull;
/** Selector handler for processing {@link Instance#selector()} value at runtime. */
public abstract class SelectorFilter {
public static final String DEFAULT_SELECTOR = "";
public abstract boolean filter(@NotNull String[] selector);
}
================================================
FILE: magnet/src/main/java/magnet/Visitor.java
================================================
package magnet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Implementation of this interface should be used with {@link magnet.Scope#accept(Visitor, int)}
* for iterating though instances and subscopes of a scope. Scope visiting begins with iterating
* through all instances and then through all subscopes.
*/
public interface Visitor {
/** Provision type. */
enum Provision {BOUND, INJECTED}
/** Visited instance. */
interface Instance {
@NotNull Scoping getScoping();
@NotNull String getClassifier();
@NotNull String getLimit();
@NotNull Class> getType();
@NotNull Object getValue();
@NotNull Provision getProvision();
}
/** Visited scope. */
interface Scope {
@Nullable String[] getLimits();
}
/**
* Called when new scope is entered.
*
* @param scope entered scope.
* @param parent parent scope of the entered scope.
* @return true to visit instances of this scope, false to skip instances.
*/
boolean onEnterScope(@NotNull Scope scope, @Nullable Scope parent);
/**
* Called when visiting new instance between {@link #onEnterScope(Scope, Scope)}
* and {@link #onExitScope(Scope)} calls.
*
* @param instance visited instance.
* @return true to visit the next instance in the scope or false
* to skip all other instances in this scope.
*/
boolean onInstance(@NotNull Instance instance);
/**
* Called when previously entered scope is exited.
*
* @param scope exited scope.
*/
void onExitScope(@NotNull Scope scope);
}
================================================
FILE: magnet/src/main/java/magnet/internal/FactoryFilter.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet.internal;
/* Subject to change. For internal use only. */
interface FactoryFilter {
boolean filter(InstanceFactory factory);
}
================================================
FILE: magnet/src/main/java/magnet/internal/Generated.java
================================================
package magnet.internal;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.CLASS;
/** Subject to change. For internal use only. */
@Retention(CLASS)
@Target({ElementType.TYPE})
public @interface Generated {}
================================================
FILE: magnet/src/main/java/magnet/internal/ImmutableArrayList.java
================================================
/*
* Copyright (C) 2018 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.
*/
package magnet.internal;
import java.util.AbstractList;
/* Subject to change. For internal use only. */
class ImmutableArrayList