Refactoring Android activities with Custom Views
If you're not careful, your Android activities can get crippled with too much functionality. And the hard part is refactoring the code that manages views. That's what the Activity is supposed to do.
Google developers recommend organizing views in Fragments. All the Activity should do is manage the Fragments' lifecycle.
But some developers are favouring custom views. The Square development team wrote about it in Advocating Against Android Fragments (the discussion). The main argument is the over-complicated Fragment lifecycle.
So, what are Custom Views?
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
init {
inflate(context, R.layout.view_custom, this)
}
}
Custom views are sub-classes of Android View classes with custom functionality. That's just it!
You can now add your functionality inside of this class, and then include it in your layout XML file, or instantiate it in your Activity.
At Bloco we like to create a BaseView that all our custom views extend. That way, we can simplify dependency injection, as well as do other useful things that we will leave out for this article.
abstract class BaseView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
protected open val activity get() = context as BaseActivity
protected val component get() = activity.component
}
Now let's imagine we have a view with a custom border.
It can also be useful to use custom attributes on a XML.
<io.bloco.template.shared.ui.AvatarImageView
android:id="@+id/avatar"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@string/avatar"
app:hasBorder="false" />
Having declared the custom attribute "hasBorder", we can use it with our custom view:
class AvatarImageView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : BaseView(context, attrs, defStyleAttr, defStyleRes) {
private var hasBorder = true
init {
inflate(context, R.layout.view_avatar_image, this)
attrs?.let {
val a = context.obtainStyledAttributes(it, R.styleable.AvatarImage)
if (a.hasValue(R.styleable.AvatarImage_border)) {
hasBorder = a.getBoolean(R.styleable.AvatarImage_hasBorder, true)
}
}
borderView.isVisible = hasBorder
}
}
Need to handle configuration changes? There's something similar to Activities, using Parcelable:
override fun onSaveInstanceState(): Parcelable? {
return super.onSaveInstanceState()
// Save data here
}
override fun onRestoreInstanceState(state: Parcelable?) {
super.onRestoreInstanceState(state)
// Restore data here
}
Conclusion
While Fragments have a lot of things to offer, we believe that custom views are easier to write and maintain most of the times, and they are lighter. We use this approach on all our production apps and so far have not encountered any setbacks.
This blog post was updated on 23/06/2021 with the intent of keeping this post information useful and relevant.