In Android, Bundle is used for sending data between Activities and Fragments.
For example, to pass userName in Activity, we use Intent:
1 2 3 |
Intent(this, MainActivity::class.java).apply { putExtras(bundleOf(“userName” to userName)) } |
In a Fragment we can use Bundle directly:
1 2 3 |
MainFragment().apply { arguments = bundleOf(“userName” to userName) } |
The following code allows us to extract passed argument in a fragment:
1 |
private val userName: String by lazy { arguments!!.getString(“userName”)!! } |
In the same line we have to write userName twice, and this is too much in Kotlin world.
What about the following variant:
1 |
private val userName: String by argumentDelegate() |
How we can implement it in Kotlin?
It can be done with delegated properties that allow us to implement our own getters/setters.
Firstly, let’s implement argumentDelegate() for a certain type parameter, for example a String. We can represent our delegate for Fragment as follows:
1 2 3 4 5 6 7 8 |
fun argumentDelegate(): ReadOnlyProperty<Fragment, String> = object : ReadOnlyProperty<Fragment, String> { override operator fun getValue(thisRef: Fragment, property: KProperty<*>): String? { return thisRef.arguments?.getString(property.name) } } |
This function returns an anonymous object that implements ReadOnlyProperty interface. This fact allows us to use this function together with Kotlin ‘by’ keyword in our fragment. Okay, well done, but what about all variety of types that could be passed to Bundle?
Of course, we need generic version of our delegate. Using reified type parameter, it’s pretty easy to do:
1 2 3 4 5 6 7 |
inline fun <reified T> Fragment.argumentDelegate(): ReadOnlyProperty<Fragment, T?> = object : ReadOnlyProperty<Fragment, T> { override operator fun getValue(thisRef: Fragment, property: KProperty<*>): T { return thisRef.arguments?.get(property.name) as T } } |
Since we also use it in Activity, we cannot focus on Fragment::arguments only. We could write an extension for Activity in the same way, but it would lead to code duplication and this is not that we had actually wanted. It doesn’t matter if you use Activity or Fragment, at the end data is saved in Bundle. So, we can extract a piece of code that works directly with arguments into a separate function.
1 2 3 4 5 6 7 8 9 10 11 12 |
inline fun <reified T> Fragment.argumentDelegate(): ReadOnlyProperty<Fragment, T?> = argumentDelegate { arguments!! } inline fun <F, reified T> argumentDelegate( crossinline provideArguments: (F) -> Bundle ): ReadOnlyProperty<F, T> = object : ReadOnlyProperty<F, T> { override operator fun getValue(thisRef: F, property: KProperty<*>): T { val bundle = provideArguments(thisRef) return bundle.get(property.name) as T } } |
Now we can easily create an Activity extension for the same purpose:
1 2 |
inline fun <reified T> Activity.argumentDelegate(): ReadOnlyProperty<Activity, T> = argumentDelegate { it.intent!!.extras!! } |
One more thing. In that case, each time, when you accessing to the userName, bundle::get will be called. That we need to improve is a fact that it would be great to use some kind of caching if we use extracted variables many times. For this case Kotlin has another nice feature – Lazy. So let’s try with it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface LazyProvider<A, T> { operator fun provideDelegate(thisRef: A, prop: KProperty<*>): Lazy<T> } inline fun <F, reified T> argumentDelegate( crossinline provideArguments: (F) -> Bundle ): LazyProvider<F, T> = object : LazyProvider<F, T> { override fun provideDelegate(thisRef: F, prop: KProperty<*>) = lazy { val bundle = provideArguments(thisRef) bundle[prop.name] as T } } |
And as the last tiny edit, we have to do something in edge cases, like when no arguments were passed.
1 2 3 4 5 6 7 8 9 |
inline fun <reified T> Activity.argumentDelegate(): ReadOnlyProperty<Activity, T> = argumentDelegate { it.intent?.extras ?: throw java.lang.RuntimeException("No arguments passed") } inline fun <reified T> Fragment.argumentDelegate(): ReadOnlyProperty<Fragment, T> = argumentDelegate { it.arguments ?: throw RuntimeException("No arguments passed") } |
All the code written above is available at ArgumentDelegate library. Looking forward to getting your feedback.