Migrating to Kotlin Android Extensions
At Zola Electric, we recently migrated completely off ButterKnife to Kotlin Android Extensions and this article shows the common use-cases we had.
ButterKnife was very useful for us while we had it until it wasn’t. Our gradle build speeds were slowing down and avoiding annotation processors was a small step in the right direction. We also were happening on a lot of errors that were usually resolved with a clean and rebuild, but sometimes required invalidating caches and restarting Android Studio.
There are a lot of articles that already explain what Kotlin Android Extensions are and how to set it up (I recommend this article by Antonio Leiva) so I won’t be covering that here. We used Butterknife in a lot of different ways in our app—to bind values, bind views and set listeners — and I’ll explain how they were replaced in this article.
Binding values (e.g. booleans, dimensions)
Here’s an example of how we used the BindDimen
annotation.
@JvmField
@BindDimen(R.dimen.height)
var dialogSizePx: Int = 0dialog.window?.setLayout(MATCH_PARENT, dialogSizePx)
And here’s how it was replaced:
dialog.window?.setLayout(MATCH_PARENT,
context?.dimen(R.dimen.height))
Binding views and setting listeners
In Activities
We now call views in anActivity
with their ids. The major things to watch out for are the listeners which were previously called with an annotation on a method but now have to be set manually.
Here’s how we did that with Butterknife:
class MainActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
@OnClick(R.id.cancel_button)
fun handleCancelClick() {
// do something
}
}
And how we do that now with the Kotlin Android Extensions:
class MainActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cancel_button.setOnClickListener { handleCancelClick() }
} fun handleCancelClick() {
// do something
}
}
In Fragments
This works the same way as in an Activity but with one major difference. A NullPointerException occurs whenever a view is called from the onCreateView
method. All views should be called from the onViewCreated
method instead.
In Custom DialogFragments
Even though the DialogFragment
extends the Fragment
, the onViewCreated
method is never called by default. This is because the onViewCreated
method should be called immediately after onCreateView
but since the onCreateDialog
is used instead in aDialogFragment
then that never happens.
So, overriding the onViewCreated
method here will not result in any action. Instead, we can override the getView
method and pass in a containerView
. This will allow us call the views normally from the onCreateDialog
method.
val containerView by lazy {
inflate(context, R.layout.fragment_dialog, null) as ViewGroup
}
override fun getView() = containerViewoverride fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(requireContext())
text_view.setOnFocusChangeListener { v, _ -> closeKeyboard(v) }
builder.setView(containerView)
return builder.create()
}
In Custom Views
We implement a LayoutContainer
and override the containerView
. The layout is inflated in the initializer block and listeners are set in the onFinishInflate
method.
class LoginMethodView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.style.AppTheme
) : ScrollView(context, attrs, defStyleAttr), LayoutContainer {
override val containerView: View?
get() = this
init {
inflate(context, R.layout.view_login_method, this)
}
public override fun onFinishInflate() {
super.onFinishInflate()
hello_button.setOnClickListener { sayHello() }
}
}
In ViewHolders
We implement a LayoutContainer
in the ViewHolder
class, override the containerView
in the constructor and handle any listeners with an initializer block.
With ButterKnife:
inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v), View.OnClickListener {
@BindView(R.id.appliance_image) lateinit var applianceImage: ImageView
init {
ButterKnife.bind(this, v)
v.setOnClickListener(this)
}
override fun onClick(v: View?) {
// handle view click
}
}
With KAE:
inner class ViewHolder(override val containerView: View)
: RecyclerView.ViewHolder(containerView),
View.OnClickListener,
LayoutContainer {init {
containerView.setOnClickListener(this)
}
override fun onClick(v: View?) {
// handle view click
}
}
This was a very huge change for us and it took some time to complete, but we believe it’s a step in the right direction. Have you found other ways to use the Kotlin Android Extensions? Please let us know in the comments!