Skip to content

Instantly share code, notes, and snippets.

@stephanenicolas
Last active April 25, 2019 17:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stephanenicolas/aa66901deaca65ed220df73d246ad531 to your computer and use it in GitHub Desktop.
Save stephanenicolas/aa66901deaca65ed220df73d246ad531 to your computer and use it in GitHub Desktop.
TP for Kotlin & java
import toothpick.Scope
import toothpick.Toothpick
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
annotation class ActivityScope
annotation class FreeScope
//-------------------------
// case: Entry point
//-------------------------
//the scope will have to be provided.
//we don't use a factory for entry points but member injector will be generated.
class FooActivity {
@Inject Bar bar;
void onCreate(Bundle state) {
getScope().inject(this);
}
public Scope getScope() {
Scope scope = Toothpick.openScope("blah")
scope.bindScopeAnnotation(ActivityScope.class)
//scope.installModule(...)
return scope;
}
}
//-------------------------
// case: Scoped Non Entry point
//-------------------------
//this is the case of a presenter for instance that should belong to
//the activity scope. Both the generated factory and member injectors are working.
//instead of using a custom scope like @ActivityScope, we could use @Scope("ActivityScope")
//but it breaks JSR 330 annotation has it has a value, or @Named("ActivityScope") @Scope
@ActivityScope
class Foo {
@Inject Bar bar;
}
//-------------------------
// case: Non Scoped Non Entry point with sub injection
//-------------------------
//this is the case of a business object that is not scoped
//we don't need a scope annotation because the @Inject annotation
//will triger the creation of a factory (and a member injector)
class Bar {
@Inject Bar bar;
}
//-------------------------
// case: Non Scoped Non-Entry point without sub injection
//-------------------------
//this is the case of a business object that is not scoped
//because an annotation is needed to get a factory,
//we have an explicit default constructor
class Qurtz {
@Inject Qurtz() {}
}
import toothpick.Scope
import toothpick.Toothpick
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
annotation class ActivityScope
annotation class FreeScope
//-------------------------
// case: Entry point
//-------------------------
//the scope will have to be provided.
//there is no annotation as we are not using member injectors
//and we don't use a factory for entry points
class FooActivity {
val bar: Bar by scope.inject()
val scope: Scope
get() {
if (Toothpick.isScopeOpen("blah)) return Toothpick.openScope("blah")
val scope = Toothpick.openScope("blah")
scope.bindScopeAnnotation(ActivityScope::class.java)
//scope.installModule(...)
return scope
}
}
//-------------------------
// case: Scoped Non Entry point
//-------------------------
//this is the case of a presenter for instance that should belong to
//a given scope, and it will retrieve it from the scoped passed as parameter
//the scope is used to create the members
//the factory generated by @ActivityScope
// (nuts it's a custom annotation, it has to be passed to kapt, or we use a fixed one ? @Scope("Activity"),
//actually we could use the pattern .*Scope in the AP to deal with all of them, that could be a convention)
//will make sure we are using a scope that supports this annotation, passed as a param to the constructor.
//this scope will be used for field injection
@ActivityScope
class FooByFieldsDI(scope: Scope) {
val bar: Bar by scope.inject()
}
//the same achieved by constuctor DI for non entry points (dagger paradigm)
//makes the object simpler and indepent from the scope type, which is way better.
//we don't need to keep a reference to the scope, properties are already injected not deferred,
//even the lazies (and they do contain the scope used to build Foo)
//this syntax is also closer to JSR 330
@ActivityScope
class FooByConstructorDI @Inject constructor(val bar: Bar, val bar2: Lazy<Bar>)
//-------------------------
// case: Non Scoped Non Entry point with sub injection
//-------------------------
//this is the case of a business object that is not scoped
//we need to pass the scope as a constructor to get lazy field injection
//we need to annotate now so that we can get a factory
@FreeScope
class BarByFieldsInjectionDI(scope: Scope) {
val qurtz: Qurtz by scope.inject()
}
//same as before. The annotation FreeScope is not even needed to produce a factory here.
class BarByConstructorInjectionDI @Inject constructor(val qurtz: Qurtz, val qurtz2: Qurtz)
//-------------------------
// case: Non Scoped Non-Entry point without sub injection
//-------------------------
//this is the case of a business object that is not scoped
//we do not need to pass the scope as no field is injected,
//we can detect this constructor case for code gen of the factory
//we need to annotate now so that we can get a factory
//an alternative could be to use `@Inject constructor()`
@FreeScope
class Qurtz
//-------------------------
// TP Kotlin Extensions
//-------------------------
//used for the `by` fields injection
class TPDelegate<OWNER, T>(val scope: Scope, val clazz: Class<T>) : ReadOnlyProperty<OWNER, T> {
override fun getValue(thisRef: OWNER, property: KProperty<*>): T {
return scope.getInstance(clazz)
}
}
//provides a delegate per field
//I actually wonder about the byte code cost in business classes
//that would use a scope as a parameter, it seems to me the cost
//of so many inline function overrides for reified types can be huge
inline fun <reified T> Scope.inject(): TPDelegate<Any, T> {
return TPDelegate(this, T::class.java)
}
//the convenient `getInstance<Foo>` instead of `getInstance(Foo::class.java)`
inline fun <reified T> Scope.getInstance(): T {
return getInstance(T::class.java)
}
fun main() {
val scope: Scope = Toothpick.openScope("")
val bar = scope.getInstance<Bar>()
bar.qurtz
}
//Generated: basically factories of TP2/3, we don't really need anything else
//if only there would be a syntax for factories we wouldn't need them..
@stephanenicolas
Copy link
Author

That's a an example of a simple syntax for TPK.
The worse of it is the constructor but we can enforce it at build time. Classes / Objects would depend directly on TP in their API.

The syntax is simple though. I am not sure we can achieve a library that would also work well for java and kotlin at the same time.

@zerobasedindex
Copy link

Interesting approach, overall I like it. I would argue that for Foo would be a place where we could be Kotlin opinionated and recommend the use of constructor injection as that doubles for defining them as fields. I'm not sure I follow the Bar example and why one would need to create a factory at all if passing the scope. Is the idea that you would still essentially getInstance(Bar::class.java) and allow TP to create Bar instead of doing Bar(scope = myScope)

@stephanenicolas
Copy link
Author

stephanenicolas commented Apr 10, 2019

@zerobaseindex, I was gonna write the gist using constructor DI for kotlin as it helps removing the scope from the constructor / class definition.

For Bar, we need a factory to pass the scope too. Indeed we just use factories with a parameter type that we always know how to inject properly. It just reuses the factory code to instantiate an object. We keep the factory mechanism because scope.getinstance(clazz: Class<T>) cannot invoke a constructor T() or T(scope), it's just not possible to invoke a constructor this way in java or in kotlin. And the factories also enable to use any other kind of parameters, in java or in kotlin.

@zerobaseindex gist updated...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment