Testing Different Methods of Launching Android Activities In Kotlin
I learned the đ hard way: There should be better ways to launch the Kotlin Android activities. Passing extra’s data via intent for serialization, and de-serialization on target Activity. So, I have read a lot of articles and gather a couple of cool ways to launch Android activities. Launching activities in android app is a common task and different developer use different approaches. Now let’s see the different types of one-by-one.
Traditional Way
There are developers who still use the old way to launch an activity. Let’s look at the typical traditional way of launching an activity.
val intent = Intent(context, OtherActivity::class.java) startActivity(intent)
Now if we need to pass an intent argument it becomes messier and not developer friendly experience. The major point is argument serialization, deserialization, type-safety, and null checks.
val intent = Intent(context, OtherActivity::class.java) intent("name",user.name) intent("email",user.email) intent("id",user.uuid) startActivity(intent)
At the OtherActivity we need to do quite a few things. Let’s see:
class OtherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState : Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) val name = intent.getStringExtra("name") ?: throw IllegalStateException("field name missing in Intent") val email = intent.getStringExtra("email") ?: throw IllegalStateException("field email missing in Intent") val id = intent.getStringExtra("id") ?: throw IllegalStateException("field id missing in Intent") } }
The above example works for our contrived example but can we improve it? Because there is always a better way to solve the problem.
Creating a launch extension function with reified Keyword
The method we’re going to use is originally published here on this link.
In this method, we’ll create an extension function for Activity and Context class. I can show you how to launch activity with this approach.
inline fun <reified T : Any> Activity.launch( requestCode: Int = -1, options: Bundle? = null, noinline init: Intent.() -> Unit = {} ) { val intent = intent<T>(this) intent.init() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) startActivityForResult(intent, requestCode, options) else startActivityForResult(intent, requestCode) } inline fun <reified T : Any> Context.launch( options: Bundle? = null, noinline init: Intent.() -> Unit = {} ) { val intent = intent<T>(this) intent.init() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) startActivity(intent, options) else startActivity(intent) } inline fun <reified T : Any> intent(context: Context): Intent = Intent(context, T::class.java)
After adding the extension function, we can directly use the .launch()
into an Activity or Context class and use it like this:
// In Context reference class context.launch<OtherActivity>() // In Activity reference class activity.launch<OtherActivity>() // Pass arguments activity.launch<OtherActivity> { putExtra("name",user.name) putExtra("email",user.email) putExtra("id",user.id) } // launch activity for result activity.launch<OtherActivity>(requestCode = 25) // launch activity for result with arguments activity.launch<OtherActivity>(requestCode = 25){ putExtra("name",user.name) putExtra("email",user.email) putExtra("id",user.id) } // launch with shared transitions val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, profilePic, "profile") activity.launch<OtherActivity>(options = options)
Nailed it đ¨ right! But to me, it’s nothing more than a better startActivity()
because we still need to deserialize the extra’s parameter inside the OtherActivity class.
class OtherActivity : AppCompatActivity() { override fun onCreate(savedInstanceState : Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) val name = intent.getStringExtra("name") ?: throw IllegalStateException("field name missing in Intent") val email = intent.getStringExtra("email") ?: throw IllegalStateException("field email missing in Intent") val id = intent.getStringExtra("id") ?: throw IllegalStateException("field id missing in Intent") } }
Let’s give it one more try and reduce the boilerplate code in order to do deserialization.
Launch activity with the args parameter & intent deserialization
Let’s get crazy and create an ideal data class for our extra parameter in order to do serialization and deserialization.
data class user(val name : String, val email : String, val uuid : UUID) class OtherActivityArgs constructor(val user : User) { companion object { private const val NAME = "_name_" private const val EMAIL = "_email_" private const val ID = "_id_" fun deserializeFrom(intent: Intent): User { return User( name = intent.getStringExtra(NAME), email = intent.getStringExtra(EMAIL), uuid = UUID.fromString(intent.getStringExtra(ID)) ) } } }
Next, create a generic ActivityArgs interface with some custom methods.
interface ActivityArgs { fun intent(context: Context): Intent fun launch(context: Context, options: Bundle? = null) = context.launch(intent = intent(context), options = options) fun launch(activity: Activity, options: Bundle? = null, requestCode: Int = -1) = activity.launch(intent = intent(activity), requestCode = requestCode, options = options) }
Now in order to do intent creation, we must implement the ActivityArgs interface on OtherActivityArgs class.
class OtherActivityArgs constructor(private val user: User) : ActivityArgs { companion object { private const val NAME = "_name_" private const val EMAIL = "_email_" private const val ID = "_id_" fun deserializeFrom(intent: Intent): User { return User( name = intent.getStringExtra(NAME), email = intent.getStringExtra(EMAIL), uuid = UUID.fromString(intent.getStringExtra(ID)) ) } } override fun intent(context: Context) = Intent( context, OtherActivity::class.java ).apply { putExtra(NAME, user.name) putExtra(EMAIL, user.email) putExtra(ID, user.uuid.toString()) } }
Before to start using the ActivityArgs class we must update our extension functions for Context and Activity class.
fun Activity.launch( requestCode: Int = -1, options: Bundle? = null, intent: Intent ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) startActivityForResult(intent, requestCode, options) else startActivityForResult(intent, requestCode) } fun Context.launch( options: Bundle? = null, intent : Intent ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) startActivity(intent, options) else startActivity(intent) }
After this we can do some launching activities stuff of the previous example like below:
val args = OtherActivityArgs(user) // pass the user object for intent creation args.launch(activity = this)
Later, we can do deserialization easily in the OtherActivity class.
class OtherActivity : AppCompatActivity() { private val user by lazy { OtherActivityArgs.deserializeFrom(intent) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_other) // use the user object } }
Boom! we have a solution that it is easily maintained– there’s only one class that handle serializing and deserializing arguments for the OtherActivity class.
Conclusion:
The good thing is that, the Intent construction method doesnât require setting the Context explicitly. It can only be called in or on Context classes.
So, what do you think about start activities, serializing and deserializing the data? I excited to get your feedback or tips to improve those methods even further. Please let me know via comments section.
Thank you for being here and keep reading…