Skip to content
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

Conversation

chenthorne
Copy link
Collaborator

@chenthorne chenthorne commented Feb 12, 2019

Adds two new modules, toothpick-kotlin and toothpick-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:

  • Get community and project maintainer buy-in
  • Add tests for both modules
  • Determine if kotlin samples should be split off into their own module
  • Make sure CI is happy and checking

@chenthorne chenthorne requested a review from dlemures February 12, 2019 17:17
@chenthorne
Copy link
Collaborator Author

@code-twister with modules split

@coveralls
Copy link

Coverage Status

Coverage decreased (-3.4%) to 92.323% when pulling f91a39f on Leafly-com:chenthore/kotlin-extensions into 54476ca on stephanenicolas:master.

scope.installModules(module { String::class named "SpecialString" providedBy "My Special String" })
super.onCreate(savedInstanceState)
setContentView(R.layout.simple_activity)
ButterKnife.bind(this)
Copy link
Owner

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.

Copy link
Collaborator Author

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")
Copy link
Owner

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
}
Copy link
Owner

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:

  1. to pass the scope all the time when injecting / lazying a property. by inject(scope). We could also prefer to use a scope name by inject(this)
  2. 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
    }
  1. 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?

Copy link
Owner

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 ?

Copy link
Collaborator Author

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:

  1. No longer need an annotation processor, quicker build time
  2. No longer need member injectors, quicker build time, no java leaking into pure Kotlin modules causing more compilation
  3. Private injected members, no need to weaken access modifier to allow an APT generated class access.
  4. No longer need to set as lateinit var or nullable
  5. IMO, reads cleaner
  6. 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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. we still need the factories to be created.
  2. 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.
  3. that one ok
  4. I am really motivated by keystroke count.@Inject lateinit var foo vs lateinit var foo by inject()
    5...
  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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yup, fair point for some classes, others won't need it like activities and fragments (nice for Android land, not applicable for Java land).
  2. 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.
  3. A big plus in my eyes as it influences 4
  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.

  1. 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 and entity, could you elaborate?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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...

  1. 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 {
Copy link
Owner

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 ?

Copy link
Collaborator Author

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.

Copy link
Owner

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 ?

Copy link
Collaborator Author

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 {
Copy link
Owner

@stephanenicolas stephanenicolas Apr 1, 2019

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 ?

Copy link
Owner

@stephanenicolas stephanenicolas Apr 1, 2019

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...

Copy link
Collaborator Author

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.

Copy link
Owner

@stephanenicolas stephanenicolas Apr 1, 2019

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.

Copy link
Owner

@stephanenicolas stephanenicolas Apr 1, 2019

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.

Copy link
Collaborator Author

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
Copy link
Owner

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.

Copy link
Collaborator Author

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.

@stephanenicolas
Copy link
Owner

One side effect of this PR is that member injectors are not needed for kotlin code.

@stephanenicolas
Copy link
Owner

@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

@chenthorne
Copy link
Collaborator Author

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.

@stephanenicolas
Copy link
Owner

stephanenicolas commented Apr 1, 2019 via email

@@ -70,3 +78,9 @@ dependencies {
androidTestImplementation deps.easymock
androidTestImplementation deps.dexmaker
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
Copy link
Owner

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.

@dlemures
Copy link
Collaborator

Closing. We already released KTP.

Thx :)

@dlemures dlemures closed this Oct 18, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants