Skip to content

Instantly share code, notes, and snippets.

@EricKuck
Last active May 9, 2019 20:30
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save EricKuck/05887d898c85ae4c47bf88b2cd127e71 to your computer and use it in GitHub Desktop.
Save EricKuck/05887d898c85ae4c47bf88b2cd127e71 to your computer and use it in GitHub Desktop.
Kotterknife(ish) view binding for Conductor controllers
// Largely borrowed from Jake Wharton's Kotterknife (https://github.com/JakeWharton/kotterknife)
// and paweljaneczek's PR for resetting cached views (https://github.com/JakeWharton/kotterknife/pull/37)
package com.bluelinelabs.conductor.butterknife
import android.view.View
import com.bluelinelabs.conductor.Controller
import java.util.Collections
import java.util.WeakHashMap
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
object ViewBinder {
fun setup(target: Any, view: View) {
LiveBindings.register(target, view)
}
fun tearDown(target: Any) {
LiveBindings.reset(target)
}
}
public fun <V : View> Controller.bindView(id: Int)
: ReadOnlyProperty<Controller, V> = required(id, viewFinder)
public fun <V : View> Controller.bindOptionalView(id: Int)
: ReadOnlyProperty<Controller, V?> = optional(id, viewFinder)
public fun <V : View> Controller.bindViews(vararg ids: Int)
: ReadOnlyProperty<Controller, List<V>> = required(ids, viewFinder)
public fun <V : View> Controller.bindOptionalViews(vararg ids: Int)
: ReadOnlyProperty<Controller, List<V>> = optional(ids, viewFinder)
private val Controller.viewFinder: Controller.(Int) -> View?
get() = { LiveBindings.targetView(this)?.findViewById(it) }
private fun viewNotFound(id:Int, desc: KProperty<*>): Nothing =
throw IllegalStateException("View ID $id for '${desc.name}' not found.")
@Suppress("UNCHECKED_CAST")
private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
= Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
@Suppress("UNCHECKED_CAST")
private fun <T, V : View> optional(id: Int, finder: T.(Int) -> View?)
= Lazy { t: T, desc -> t.finder(id) as V? }
@Suppress("UNCHECKED_CAST")
private fun <T, V : View> required(ids: IntArray, finder: T.(Int) -> View?)
= Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } }
@Suppress("UNCHECKED_CAST")
private fun <T, V : View> optional(ids: IntArray, finder: T.(Int) -> View?)
= Lazy { t: T, desc -> ids.map { t.finder(it) as V? }.filterNotNull() }
// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
private object EMPTY
private var value: Any? = EMPTY
override fun getValue(thisRef: T, property: KProperty<*>): V {
LiveBindings.register(thisRef, this)
if (value == EMPTY) {
value = initializer(thisRef, property)
}
@Suppress("UNCHECKED_CAST")
return value as V
}
fun reset() {
value = EMPTY
}
}
private object LiveBindings {
private val viewMap = WeakHashMap<Any, View>()
private val bindingMap = WeakHashMap<Any, MutableCollection<Lazy<*, *>>>()
fun <T> targetView(target: T): View? {
return viewMap[target]
}
fun <T> register(target: T, view: View) {
viewMap.put(target, view)
}
fun <T> register(target: T, lazy: Lazy<T, *>) {
bindingMap.getOrPut(target, { Collections.newSetFromMap(WeakHashMap()) }).add(lazy)
}
fun <T> reset(target: T) {
viewMap.remove(target)
bindingMap[target]?.forEach { it.reset() }
}
}
package com.bluelinelabs.controller
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.rxlifecycle.RxController
import com.bluelinelabs.conductor.butterknife.ViewBinder
abstract class ViewBindingController : RxController {
constructor() : super()
constructor(args: Bundle?) : super(args)
final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
val view = inflateView(inflater, container)
ViewBinder.setup(this, view)
onBindView(view)
return view
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
override fun onDestroyView(view: View?) {
ViewBinder.tearDown(this)
}
open fun onBindView(view: View) { }
}
@nomisRev
Copy link

nomisRev commented Feb 21, 2017

Something like this would remove the overhead of setup and tearDown while also eliminating the viewMap and bindingMap. The difference in performance would probably be negligible. Thoughts? I'm moving away from Anko because I'm missing out on the improving lint checks with ConstraintLayout and since min APIs are getting upped and thus low end (crappy) device support is improving. And thus I'm on the look out for a good solution for view bindings for Kotlin.

Butterknife served me well in the passed but the required public backing field causes a lot of overhead for 'required' fields. I've not found a good solution in Kotlin beside bundling @JvmField and @BindView in 1 annotation and ending op with something like this @NewAnnotation var button: Button? = null (? = null could be replaced with either lateinit or Delegates.notNull but not a big fan of either if it can be avoided). Since Android extensions uses a maps to keep view references I'm also kinda skeptical about that.

private class ControllerLazy<out V>(private val initializer: (Controller, KProperty<*>) -> V) : ReadOnlyProperty<Controller, V> {
    private object EMPTY

    private var value: Any? = EMPTY

    override fun getValue(thisRef: Controller, property: KProperty<*>): V {
        if (value == EMPTY) {
            value = initializer(thisRef, property)
            thisRef.addLifecycleListener(object : Controller.LifecycleListener() {
                override fun postDestroyView(controller: Controller) {
                    super.postDestroyView(controller)
                    value = EMPTY
                    thisRef.removeLifecycleListener(this)
                }
            })
        }
        @Suppress("UNCHECKED_CAST")
        return value as V
    }
}

@fullkomnun
Copy link

As part of 1.1.4 they added more flexibility to "Synthetic Properties":
https://antonioleiva.com/kotlin-android-extensions/
This could be useful although still experimental.

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