Creating Bundles nicely with Kotlin

Or how to improve Android KTX approach

Michael Spitsin
6 min readMay 26, 2018

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!

--

--

Michael Spitsin
Michael Spitsin

Written by Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways

Responses (3)