Refactor your BaseFragment class
Or how to organize service layer and eliminate boilerplate
When we create an android application based on Fragments structure (when each page means separate fragment) at some point we have, let’s call, BaseFragment
which holds very base logic, that used in all or in most inherited fragments.
One day you sit in front of your IDE. You open BaseFragment
class and become frightened, because you have a lot of logics in it. For example:
- It can execute tasks on ui with attaching them to lifecycle.
- It works with permissions
- It helps save data in instance state
- It works with Navigation
You stunned with size of it and amount of code it contains. So today we try to organize our code in order to split logic and eliminate boilerplate code from BaseFragment
Say Yes to Services
And I’m talking not about Android Services, but about special entities (classes or group of classes) that do Single Working Unit and helps to make live easier. The examples you can see above.
Base fragment as for me is a class that must provide only list of such services or possibilities to plug in them, no other logic. That’s leads us to successful understanding each feature, that provided be each service and the code became much clearer. Just imagine, for instance, that your BaseFragment can work with navigation and at the same time work with google maps. You will have 200–300 lines of mixed code (some for navigation and building it, and some for recognizing current location and connecting maps API). Obviously it is a bloater. And your first step every time you have base fragment, or base activity is to split it inner logic to services. Thus every class will have its own responsibility.
In the end of this step we will have something like this:
open class BaseFragment : Fragment() {
protected val instanceState = InstanceState()
protected val permissionManager = PermissionManager()
protected val uiExecutor = UIExecutor()
init {
lifecycle.perform {
addObserver(uiExecutor)
addObserver(permissionManager, this@BaseFragment1)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
instanceState.onRestoreInstanceState(savedInstanceState)
super.onCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
instanceState.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
permissionManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
Unfortunately for now with using Lifecycle components we can not incapsulate saving and restoring state and e.t.c, only base lifecycle events. But we can try to do it in the next articles, if you interested. :)
Give features to Base Fragment
Approach above is already good, since you have splitted logic and access to properties of BaseFragment
but for me it is more interesting to have direct access to methods of each service. So instead of writing
var isSomething by instanceState.instanceState(false)
//or
permissionManager.requestThenRun(listOf(READ_SMS), { /*fail*/ }, { /*success*/ })
we will write
var isSomething by instanceState(false)
//or
requestThenRun(listOf(READ_SMS), { /*fail*/ }, { /*success*/ })
1. Organize delegation interfaces
To do this we need to organize a bunch of interfaces with needed methods. Each interface will be attached to each service. I used postfix Delegate
for each interface. For example, InstanceStateDelegate
. You service is not neccesary be realization of that interface, you can create a special adapter class for that. It will delegate or possibly adapt methods of service for using them through Delegate
-interface.
In our case, I’m ussing services as realisations of that Delegate
, but one again it is not necessary, since service can be, for example, from the library, so you can not change its semantics.
In the end of this step we will have
interface InstanceStateDelegate {
fun <T> instanceState(): InstanceStateProvider.Nullable<T>
fun <T> instanceState(defaultValue: T): InstanceStateProvider.NotNull<T>
fun <T> lateinitInstanceState(): InstanceStateProvider.Lateinit<T>
}interface PermissionManagerDelegate {
fun requestAndRun(permissions: List<String>, failAction: () -> Unit, action: () -> Unit)
fun requestThenRun(permissions: List<String>, failAction: () -> Unit, action: () -> Unit)
}interface UIExecutorDelegate {
fun runOnUi(command: Runnable)
fun postOnUi(command: Runnable)
fun delayOnUi(afterMs: Long, command: Runnable)
fun runOnUi(command: () -> Unit)
fun postOnUi(command: () -> Unit)
fun delayOnUi(afterMs: Long, command: () -> Unit)
}//Declaration of my realisations of Delegates
class UIExecutor : Executor, UIExecutorDelegate, LifecycleObserver
class PermissionManager : FragmentLifecycleObserver, ActivityLifecycleObserver, PermissionManagerDelegate {
class InstanceState : InstanceStateDelegate {
2. Implement delegate interfaces in fragment
Now we can implement interfaces in fragment. As a result we will have
Now we have many syntatic sugar lines with delegating logic to services. When you will have 5–8 services it can lead to enormous lines of useless code just for not using names of properties in inheritors. Looks so-so.
3. Implement kotlin Delegates.
To eliminate this obvious boilerplate code we can use kotlin delegation. To do that we need to move all our service properties to the constructor of base fragment an give them default implementation. As the result we will have:
open class BaseFragment @JvmOverloads constructor(
private val instanceState: InstanceState = InstanceState(),
private val permissionManager: PermissionManager = PermissionManager(),
private val uiExecutor: UIExecutor = UIExecutor()
) : Fragment(),
InstanceStateDelegate by instanceState,
PermissionManagerDelegate by permissionManager,
UIExecutorDelegate by uiExecutor {
init {
lifecycle.perform {
addObserver(uiExecutor)
addObserver(permissionManager, this@BaseFragment1)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
instanceState.onRestoreInstanceState(savedInstanceState)
super.onCreate(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
instanceState.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
permissionManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
This trick gives us an ability to use service methods in iheritors of BaseFragment
directly, and with this not bloat base fragment. Super.
If you afraid of using constructor with default parameters, not worries. With @JvmOverloads
we will still have default empty constructor that is needed by the system.
One rule is do not using any constructors of fragment, except default empty one. But this rule is important every time, not only with that service approach.
Afterwords
Now we have small and powerful base fragment with bunch of services and features, that inheritors can use directly, without writing name of properties. And also it consice since it uses kotling delegation. In the next time I will try to enhance lifecycle architect approach, given by android team, but without using code generation.