ButterKt, yet another view binding library
There are many view binding libraries in Android, especially when you use kotlin. This article will compare view binding libraries, problems of each library has, and how ButterKt solves those problems.
Link to ButterKt Github : https://github.com/Rajin9601/ButterKt
View binding methods in android with kotlin
No library
Without library, you can call findViewById
and assigns the view into variable.
class MainActivity: Activity {
lateinit var idEditText: EditText
override onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
idEditText = findViewById(R.id.id_input)
}
}
We don’t want this kind of code because declaration and assignment are seperated. We can forget to assign value with findViewById.
ButterKnife
ButterKnife solves this problem with apt and code generation.
class MainActivity: Activity {
@BindView(R.id.id_input)
lateinit var idEditText: EditText
override onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
ButterKnife.bind(this)
}
}
Using apt, code is generated for view binding. When ButterKnife#bind
is called, all views in the class is bound.
Even though ButterKnife#bind
needs to be explicitly called, this is good enough.
KotterKnife
KotterKnife uses kotlin’s property delegation to make declaration more simpler than ButterKnife.
class MainActivity: Activity {
val idEditText: EditText by bindView(R.id.id_input)
override onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
}
}
There is no need to call ButterKnife#bind
. Only declaration is enough. This is possible because view binding happens lazy, which means view is bound at the first access to idEditText
.
Kotlin Android Extension
Using Kotlin Android Extension, additional code for binding is not needed. View is accessble as if it already exists in the class.
class MainActivity: Activity {
override onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
id_input.setText("test")
}
}
Kotlin Android Extension is really convienient because you don’t need to do anything for the binding to work. All you need to do is import specific package for layout file and views are accessible with the name same as id in xml file.
import kotlinx.android.synthetic.main.<layout>.*
Features wanted from view binding libraries
In this article, 4 methods for view binding are mentioned. View#findViewById, ButterKnife, KotterKnife and Kotlin Android Extension. While considering which method to use, there are 3 features I wanted from View Binding Libraries.
Declaration and assignment should be together
If you do not use any library, you should declare variable and assign the view by findViewById in onCreate method. Not only this is cumbersome, but also it reduces code’s readability. To find out which view is bound to idEditText, programmer needs to go to the actual assignment, which is in onCreate method. Also by putting declaration and assignment together, programmer is not likely to forget assignment.
Binding timing should be earlier than dynamic creation of other view
In KotterKnife and Kotlin Android Extension, programmer don’t need to write any code in onCreate method. This is because these two library binds view lazily. When code access the view for the first time, it binds the view to a variable. After first access, it uses the binded view. This could cause problem when layout id overlaps.
This code example explains problem of lazy binding. Note that unnecessary codes are deleted to shorten the code.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
// activity_main has recycler_view and text_view
recycler_view.adpater = MyAdapter()
recyclerView.postDelayed({
textView.setText(R.string.testing_str)
// BUG HERE : this text_view accesses to item view in recycler view.
}, 1000)
}
}
class MyAdapter: RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): MyAdapter.ViewHolder {
val textView = LayoutInflater.from(parent.context)
.inflate(R.layout.my_text_view, parent, false)
// my_text_view has TextView with id of text_view
return ViewHolder(textView)
}
}
To prevent this, you can a) use distinct layout id or b) binds view before dynamic view creation. Using distinct layout id can be one solution, but layout id can be unnecessarily long. To me, binded view’s name should not have context of which layout it exists because class name already have that context.
To see the actual behavior of this code, visit https://github.com/Rajin9601/ButterKt/tree/master/lazy-binding-sample-app
If possible, no apt and code generation
By using apt for code generation, programmer can reduce repeated code. This is good, but if same feature can be implemented without code generation, I prefer not to use apt and code generation. Annotation processing have some overload of going through the source. (which is really small but still).
Overall view for binding library.
Library | Declaration and assignment | Binding Timing (Not lazy) |
No APT |
---|---|---|---|
View#findViewById | x | o | o |
ButterKnife | o | o | x |
KotterKnife | o | x | o |
Kotlin Android Extension | o | x | o |
Not a single library don’t fill my needs for view binding. ButterKnife would be my choice, since no apt is the least important. However with kotlin, ButterKnife could be implemented without code generation, which means view binding library with all these 3 features can be implemented in kotlin.
Introducing ButterKt
Code itself is similar to KotterKnife, but usage syntax is more like ButterKnife.
class MainActivity : AppCompatActivity() {
private val title by bindView(R.id.item_title)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ButterKt.bind(this)
}
}
This has additional benefits other than mentioned features above.
- can use view as private field
- no need for lateinit var
- can use different naming from layout id
How does it work?
ButterKt keeps track of ViewHolder - ReadOnlyProperty pair with singleton multimap, view holder as key. When ButterKt#bind is called, ButterKt gets all ReadOnlyProperty related to the view holder and binds it to view.
In case programmer forget to add ButterKt#bind
or bind call wasn’t excuted because of some reason, ButterKt uses WeakHashmap and WeakReference for GC to work properly and supports lazy loading if it can.
For more information, visit https://github.com/Rajin9601/ButterKt and see actual code.
Sidenote about Android Kotlin Extension (AKE)
Many people prefer Android Kotlin Extension over other view binding library. This is understandable because it doesn’t need any code for view binding to work. For someone who is interested in AKE, this section provides comparison between ButterKt and AKE.
Points where AKE is better than ButterKt
ButterKt | AKE |
---|---|
Creates one object per binding | No extra instance |
Need explicit binding (and unbind in Fragment#onDestroyView) | No code needed for binding and unbinding |
ButterKt has some drawbacks. It creates one object and indirection per binding because of how kotlin’s property delegation works. Also, you may forget to unbind view in Fragment#onDestoryView
. These drawbacks don’t exist in AKE.
Points where ButterKt is better than AKE
ButterKt | AKE |
---|---|
Free from lazy binding problem | Need to use distinct layout id accross all xmls |
Can rely on IDE auto-complete | Need to check which package is imported |
Can use short naming (e.g. title) | Use distinct naming (e.g. todoTaskItemTitle) or becareful of lazy binding problem |
What I most disliked about AKE is that it is implemented as extension of Activity(View, Fragment, etc). Any activity have access to an xml’s view if xml’s package is imported. We generally don’t rely on which import is used in kotlin file which makes AKE dangerous of importing wrong package. By making it as extension of Activity, AKE doesn’t use the benefit of type system, which is sad.
Another point I’m against AKE is that I personally think that view’s naming should not contain additional info about which xml it is in, which means I prefer naming such as title
over todoTaskItemTitle
. Because Code scope already defines which item the class represents, view naming does not need to contain information about it.