
Background:
As Jetpack Compose slowly taking over the Android world, you may be familiar with Navigation pattern with Compose.
However, if your app still has many legacy fragments/activities and you need to pass in parameters to those, it would need to pass bundle/arguments to do so, check out this stack overflow post.
My biggest complain is that when you have many parameters, you would have to first:
startActivity(Intent(this, SecondActivity::class.java).apply {
putExtra("EXTRA_NAME", "john")
putExtra("EXTRA_SEX", "male")
putExtra("EXTRA_AGE", 24)
})
And in Fragment/Activity’s onCreate or onAttach, get those parameters:
val name = intent.extras?.getString("EXTRA_NAME") ?: ""
val sex = intent.extras?.getString("EXTRA_SEX") ?: "male"
val age = intent.extras?.getInt("EXTRA_AGE") ?: 0
//See how the nullability of the getString() / getInt() causes default data?
Consider another situation that we have two ways to initialize the Activity that one needs user’s age another doesn’t. It would be a headache to do the initialization, because how would you know if a field is supposed to exist or it is simply not present because of some errors. Thus this creates lots of confusions, especially when others need to read the code you write.
For example, on version 1 of SecondActivity we need users’ name, sex and age, but on version 2 we will need only name and sex, deprecating the age attribute. Then how can we identify the difference between version 1 and 2?
Solution:
Instead of passing in each attributes individually, we can make use of the combination of Parcelable and Sealed Class.
sealed class IntermediateData: Parcelable {
abstract val name: String
@Parcelize
class NewIntermediateData(override val name: String, val sex: String): IntermediateData()
@Parcelize
class OldIntermediateData(override val name: String, val sex: String, val age: Int): IntermediateData()
}
using either NewIntermediateData or OldIntermediateData depending on the version, we can send this as a Parcelable with the Intent:
findViewById<Button>(R.id.go_to_next_screen_button).setOnClickListener {
val newIntermediateData = SecondActivity.IntermediateData.NewIntermediateData(name = "John Foo", sex = "Male")
startActivity(Intent(this, SecondActivity::class.java).apply {
putExtra("EXTRA", newIntermediateData)
})
}
findViewById<Button>(R.id.go_to_next_screen_button_old).setOnClickListener {
val oldIntermediateData = SecondActivity.IntermediateData.OldIntermediateData(name = "Mary Ann", sex = "Female", age = 65)
startActivity(Intent(this, SecondActivity::class.java).apply {
putExtra("EXTRA", oldIntermediateData)
})
}
// please forgive me using findViewById, as that is not the focus of these code
on the receiving Activity/Fragment, we can proceed with reading the parcelable and do the parsing by its type:
private fun processData(newIntermediateData: IntermediateData?){
when(newIntermediateData){
is IntermediateData.NewIntermediateData -> {
intermediateData = newIntermediateData
viewModel.init(intermediateData as IntermediateData.NewIntermediateData)
}
is IntermediateData.OldIntermediateData -> {
intermediateData = newIntermediateData
viewModel.init(intermediateData as IntermediateData.OldIntermediateData)
}
else -> {
Log.e("SecondActivity", "failed to init second activity")
}
}
}
//In onCreate/onAttach:
processData(intent.extras?.getParcelable("EXTRA", IntermediateData::class.java))
Then you can pass those data to viewmodels’ init methods with confidence!
In my opinion, even for creating Fragments/Activity without only one version of constructor, we can still benefit from better readability as well as confidence on accessing non-nullable fields .
checkout Github repo here: https://github.com/q58wu/multiple_constructor_for_activity_and_fragment
Thanks for reading and let’s Keep On Truckin!