Creating Bundles nicely with Kotlin
Or how to improve Android KTX approach
Android KTX is a great idea, but it’s realisation simple and obvious right now. I’m pretty sure, that guys from Google will add more interesting extensions and mechanisms, which will help to reduce huge amount of code in every project overall, but also those approaches will not break security, access level or unnecessary movement compilation errors to runtime layer.
But for now let’s stop on KTX approach to work with bundles. It pretty interesting, since more declarative way to build up a bundle was proposed:
bundleOf(
"age" to 25,
"name" to "Michael",
"skill" to null
)
In the same time that way is not so elegant as it could be, since it gives an opportunity to put everything in a bundle, and not so attentive programmer, who will write something like:
data class Model(
val id: Long,
val data: String
)bundleOf(
"age" to 25,
"name" to "Michael",
"skill" to null,
"payload" to Model(0L, "")
)
will see an error only in runtime (if you will fill bundle with values, using just class Bundle
with it method, you will just not able to place instance of Model
in it, so it will be compile time error). Or when he(she) will write a test. If he will :)
Also, if we will look at source code, we will see, that one function bundleOf
takes about 70 lines of code, which from my opinion is Bloater.
Thus, we will try to improve that approach. I will show you, like 2 years before in the first article, how refactoring works and how it can improve your code quality with giving more great possibilities to the user.
Add set operator
I think, you agree, that this is pretty obvious request (to add set operators) for classes like Bundle
. It is just logic, since Bundle
, ContentValues
and e.t.c are just key-value storages, like Map
. Actually they are just wrappers for Map
, but it just a characteristic of a realisation. From the official documentation of Bundle
we can see the definition:
A mapping from String keys to various
Parcelable
values.
Which just confirms aforementioned words. Thus, it is pretty straightforward to give to user a possibility to write:
bundleOf {
it["age"] = 25
it["name"] = "Michael"
it["skill"] = null
}
For that we need just to split KTX bloated function into two functions:
fun bundleOf(vararg pairs: Pair<String, Any?>) =
Bundle(pairs.size).apply {
for ((key, value) in pairs) {
putSmart(key, value)
}
}fun Bundle.putSmart(key: String, value: Any?) = when (value) {
//all parsing goes here
}
Then we need to introduce just 2 new one line functions:
inline fun bundleOf(initFun: (Bundle) -> Unit) =
Bundle().also(initFun)operator fun Bundle.set(key: String, value: Any?) =
putSmart(key, value)
That’s all, such simple changes lead us to:
- Treatment of
bundleOf
ktx function by splitting it into 2 functions - Introduction of new cool feature
But still or “put everything in bundle” problem exists.
Add overloads to set operator
Now, let’s think. We have a Bundle
with bunch of put
methods to give strict rules of what we can put in it. So as we remember it is just a wrapper for Map
that hides its put
function and instead of that provides us about 30 methods to tell the user, what types of objects he can put. So inattentive programmer will not be able to put Model(0L, "")
since this class does not satisfy the conditions set by Bundle
. And now we want to erase those conditions, by providing fun Bundle.set(key: String, value: Any?)
. I think this is not acceptable.
Apparently, more elegant and right solution will be provide set
operator for each type of bundle possible values:
We have about 30 overloads of set operator. Just 30 smallest, one-line functions, which not using flawed putSmart
method. We now able to write:
bundleOf {
it["age"] = 25
it["name"] = "Michael"
it["skill"] = null
}
Without afraid of putting wrong type as a value
parameter.
Add BundlePair typealias
Now let’s make one more step forward. We want to provide similar method syntactically and semantically, as it it in KTX:
fun bundleOf(vararg pairs: Pair<String, Any?>)
But as a difference we want to give user strict set of types, that it can provide. So in the one side we want to go through vararg/array/iterable of params, in the other side we want to limit user with types of those parameters, and in the third side we want to work with put
-methods defined in the Bundle
class (or with our new set
operators). As for me, it is time to release the Command pattern.
First, let’s add new typealias:
private typealias BundlePair = (Bundle) -> Unit
Then we need to define all command, each will work with related put
-method of Bundle
class. For example:
infix fun String.bundleTo(value: Boolean): BundlePair = { it.putBoolean(this, value) }
As you can see here we operate with putBoolean
method and provide for it two parameters: String
key and Boolean
value. Also, we provide infix
keyword in order to reach same syntax as it was to building Pair
. So for now, instead of writing:
"age" to 25
You will need to write
"age" bundleTo 25
Yes, now you need to write on a 5 symbols more, but you can invent any name for that function as you want ;)
Here is a list of all bundleTo
method overloads:
And the main method:
inline fun bundle(initFun: Bundle.() -> Unit) =
Bundle().apply(initFun)
fun bundleOf(vararg pairs: BundlePair) = bundle {
pairs.forEach { it.invoke(this) }
}
So now, you able to write:
bundleOf(
"age" bundleTo 25,
"name" bundleTo "Michael",
"skill" bundleTo null
)
And you will not able to write:
bundleOf(
"age" to 25,
"name" to "Michael",
"skill" to null,
"payload" to Model(0L, "")
)
And, as a bonus, we don’t need more bloated putSmart
method, since we have a lot of tiny concise “single responsible” method. Cool!
Convert BundlePair typealias to class (if you need more security)
Now we have one problem left. With new approach we can write next snippet of code:
bundleOf(
"age" bundleTo 25,
"name" bundleTo "Michael",
{ /*do whatever we want with bundle*/ }
)
It is not a nightmare, but no so go in order to rules, we want to provide with bundleOf
method. We want strict build construction, so we must restrict usage of BundlePair
. For that we need to convert it to normal class with internal constructor and method apply
. I marked this section with (if you need more security) postfix, because the fee for limitation and short coding will be the creation of extra object. So if you agree to live with example, above, you can just skip this section.
Let’s introduce new class:
class BundlePair(
private val block: (Bundle) -> Unit
) {
fun apply(bundle: Bundle) = block(bundle)
}
And let’s update our bundleTo
methods:
And our main function:
fun bundleOf(vararg pairs: BundlePair) = bundle {
pairs.forEach { it.apply(this) }
}
Now thats all. Let’s see the result of our refactoring:
As we can see, we used 90 lines of code, instead of 70, but
- We have more features now for user
- We don’t loose limitations for parameters, that are passed to bundle
- We refactored code, and now we have no long method smell
Yes, we still have pretty big amount of code duplication, but it is limitation of language. I wish to have some mechanism to write method overloads in more brief way, but currently there is no such approach :(
Afterwords
Refactoring is a big thing. Refactoring is a breath of life. Refactoring is a key. Refactoring is a tool, you must always to keep with yourself.
This was the first part of refactoring. Here you can see the second part, where we apply operators. :)
Thank you.
P.S.: As I said in the middle of article, it past almost two years since my first post. I want to thank all of you, how read my modest, sometime clumsy and sloppy texts written in bad-bad English. Especially I want to thanks all, who claps. Thank you guys. Really!