-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial Kotlin Extensions for Toothpick #324
Initial Kotlin Extensions for Toothpick #324
Conversation
@code-twister with modules split |
scope.installModules(module { String::class named "SpecialString" providedBy "My Special String" }) | ||
super.onCreate(savedInstanceState) | ||
setContentView(R.layout.simple_activity) | ||
ButterKnife.bind(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need butterknife ? It seems to me we're using a simple lazy to assign the views.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, was just copying the Java examples over for parity.
if (injectConfigBlock != null) { | ||
injectConfig.apply(injectConfigBlock) | ||
} | ||
val scope = injectConfig.scope ?: scope ?: getScope(thisRef, property) ?: throw IllegalStateException("No valid scope available when attempting to inject $property in $thisRef") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's finish with a .
and explain how to fix the issue. If there is more than one alternative, let's explain them all.
|
||
if (thisRef is HasScope) { | ||
return thisRef.scope | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got an issue with this. No idea if it is a standard way to implement other DI engines usually.
But it seems very hacky for a few reasons. I understand that we have this mechanism, and the fallback of creating a new scope right under the app for activities, because we want to be more concise and not pass the scope when doing by inject(scope)
But first things first, I am not sure I understand how lazy and inject differ now. Isn't the delegated property going to be "computed" only when the code accesses it ? if so, what's the difference with a lazy injection, and if not, when is it gonna be computed?
I don't really like the interface, and the fallback neither. It assumes that the activity as a given property, implements a given interface. An activity could decide to call it's scope scopeA
and forget about the interface and no injection would take place.
Same for the fallback, we don't want to consider the activity's scope is placed right under the application by default. And for fragments, I am not even sure that the activity scope should be the default.
I was thinking that it would be maybe cleaner:
- to pass the scope all the time when injecting / lazying a property.
by inject(scope)
. We could also prefer to use a scope nameby inject(this)
- the activity can itself have a getter/delegate to create it's scope when needed
lateinit var scope: Scope
get() {
TP.createScope(...)
scope.installModules(...)
return scope
}
- if it's not convenient to pass the scope to every inject call. Can't we have another mechanism where we would restor the call to TP.inject(this, scope) and this would provide the scope to
by inject()
's delegate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And there is another issue. IMHO. We're loosing the @Inject
annotation for fields. It seems to me there is a tension in your PR between what happened to butterknife (replaced by lateinit by findViewByID
) and the classic @Inject
annnotation. I prefer the annotation, but why did you prefer the other way, for speed ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of this hack. I haven't found a way to do this better without passing scope with every call. At one point, I had scope as nullable, so if you didn't provide it the delegate would try to use this
. I didn't really like that either but it may be better than this route.
In most cases, I don't want to have to pass scope ever unless I'm doing something fancy.
I like no longer having the @Inject
annotations, less non-code in my code to read around. We gain a few things with it:
- No longer need an annotation processor, quicker build time
- No longer need member injectors, quicker build time, no java leaking into pure Kotlin modules causing more compilation
- Private injected members, no need to weaken access modifier to allow an APT generated class access.
- No longer need to set as
lateinit var
or nullable - IMO, reads cleaner
- Puts all injection logic in one spot as you can put scope and name in there as well, it's all done in one go instead of multiple annotations or split of logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- we still need the factories to be created.
- factories could be implemented in kotlin using extension function but I don't know any AP doing this. Not sure the build times would be any better.
- that one ok
- I am really motivated by keystroke count.
@Inject lateinit var foo
vslateinit var foo by inject()
5... - It also changes the logic of the instantiation and scopes. Now the scope would be decided by the client whereas in java it's set by the entity itself. I feel that it might be conflicting in some cases too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Yup, fair point for some classes, others won't need it like activities and fragments (nice for Android land, not applicable for Java land).
- Sure, factories are still needed for most, and there is still Java leakage, but less and less to get rid of if TP ever moves to a kotlin based solution.
- A big plus in my eyes as it influences 4
- Actual code:
@Inject lateinit var foo: Foo
private val foo: Foo by inject()
I really like the second vs the first, and in the day of IDEs and code complete, keystroke count alone is a poor argument loosening access restrictions and adding mutability to the variable. Big philosophy of Kotlin is immutability, and this change moves TP in that direction.
-
- In my use cases, it hasn't been much of a big deal as I rarely am doing anything special with scopes. And when I am, I'm already paying extra attention to how I'm using the scope. I'm not sure I follow what you mean by
client
andentity
, could you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do kotlin people really talk about java leakage ? I find it funny ;)
I get it for #4. That's a good point (sorry I am really new to kotlin, it takes some time to put the pieces together ;)). Yes for immutability, it's a big win. But I like my so much my little cute annotations ! Snifff...
- I understand why the PR goes a little too far with scopes now. ;)
Toothpick.inject(toInject, this) | ||
} | ||
|
||
fun <T : Any> Scope.getInstance(clazz: KClass<T>, name: String? = null, annotationName: KClass<out Annotation>? = null): T { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need the non reified ones ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you want to inject a subclass into a super.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an example in your PR ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
divide into two overloads to disambiguate name or annotationName, not both
return module.bind(T::class.java) named name | ||
} | ||
|
||
inline infix fun <reified T : Any> KClass<T>.providedAs(clazz: KClass<out T>): Binding<T>.BoundStateForClassBinding { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all the fluent API has been renamed entirely. Can we keep the exact same method names for the bindings ?
I am not opposed to renaming them if needed, but it's a big change for TP. Are there any kotlin "way of doing things" that are at stake with these new names ?
Oh.. to
is a kotlin keyword... right ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the DSL should look more like:
bind T::class.java toInstance t0
bind T::class.java toClass T1::class.java
bind T::class.java withName "foo" toInstance t0
bind T::class.java toProviderInstance p0
bind T::class.java toProviderClass P::class.java
for providesSingletonInScope
, instancesInScope
, singletonInScope
, I agree we have an issue that a infix method needs a param...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I went back and forth on this one a lot. Poked around in kodein and other kotlin DSLs to find a common naming scheme and didn't really see anything definitive. I settled on what I did as it read the most naturally to me as a human, but I'm not hugely attached to it. I like the examples you've provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also like the with style ... but really the main problem is that to
is a reserved word.. I don't find it great I must say.
Same goes for as
, otherwise we could have had construct like
bind T::class.java to instance t0
bind T::class.java to instance T0::class.java as singleton
This would be the more natural solution.
Would it work if we would do
bind T::class.java `to` instance t0
bind T::class.java `to` instance T0::class.java `as` singleton
It's ugly, but I am trying to see all our options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could actually allow the to operator if module would support adding pairs or something that converts to a pair (destructuring)
module {
bind Class<T> to instance(t0)
bind Class<T> to Class<T1>
bind Class<T> to providerInstance(p0)
bind Class<T> to Class<Provider<P1>>
}
we could maybe even use the as
cast opertator:
bind Class<T> to Class<T1> as Singleton
For this to work, we would need to introduce a hierarchy like:
Singleton: BindingPair
where BindingPair would be convertable to a Pair.
We definitely need to have a clearer idea of what our current fluent API do / can do / should do. It's not clear yet and it's part of the problem we're having.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I'll have to play with this to see if how it feels in real code. We're using a variant of the DSL internally so I can see how it goes with real world modules.
// Lazy inject with scope search | ||
class ScopeSearch : HasScope { | ||
override lateinit var scope: Scope | ||
protected set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer an open get() that subclasses can override than a set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow the comment. I settled with this as it provided universal/public access to the getter but only protected access to the setter. This keeps any ole class changing the scope.
One side effect of this PR is that member injectors are not needed for kotlin code. |
@chenthorne can you have a look at kodeint DSL for bindings / module. They use the method with elegantly : https://github.com/Kodein-Framework/Kodein-DI |
Thanks for the review! @stephanenicolas Sure thing, I'll look through that again, anything in Kodein you'd like me to look at in particular? Did you look at Koin? Some of the stuff I did was driven by things I liked in there. Looking forward to chatting with you about this soon. |
Yeah I like the binding DSL in kodein.
bind Foo::class.java with singleton { lambda }
…On Sun., Mar. 31, 2019, 20:52 Cody Henthorne, ***@***.***> wrote:
Thanks for the review!
@stephanenicolas <https://github.com/stephanenicolas> Sure thing, I'll
look through that again, anything in Kodein you'd like me to look at in
particular? Did you look at Koin? Some of the stuff I did was driven by
things I liked in there.
Looking forward to chatting with you about this soon.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#324 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABv33WLHzudZgTOpc53cXuqrHUDAZYL8ks5vcYKNgaJpZM4a3a5S>
.
|
@@ -70,3 +78,9 @@ dependencies { | |||
androidTestImplementation deps.easymock | |||
androidTestImplementation deps.dexmaker | |||
} | |||
|
|||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should use Task Configuration Avoidance.
Closing. We already released KTP. Thx :) |
Adds two new modules,
toothpick-kotlin
andtoothpick-kotlin-androidx
, to provide kotlin friendly interfaces to common Toothpick use cases. Also provides some syntactic sugar for using Toothpick with Android Kotlin.Both modules contain README's with examples on how to use the extensions and the
smoothie-sample
project has been updated to have a mirror of the existing samples but in kotlin.See issues #305 and #316 or the draft extensions at https://github.com/Leafly-com/toothpick-extensions-sample
Things remaining: