Android Intent Handling Between Activities Using Kotlin

In Android development, Intents play a crucial role in facilitating communication between different components of an application, especially between activities. They allow you to start a new activity, pass data between them, and handle various types of requests. In this blog, we'll explore how to handle Intents between activities using Kotlin, covering the basics, common practices, and best practices along the way.

Table of Contents#

  1. What is an Intent?
  2. Starting a New Activity with an Intent
  3. Passing Data Using Intents (Extras)
  4. Receiving Data in the Target Activity
  5. Common Practices
  6. Best Practices
  7. Example Usage
  8. References

1. What is an Intent?#

An Intent is an abstract description of an operation to be performed. It can be used with startActivity() to launch an activity, startActivityForResult() (deprecated in Android 11, use ActivityResultContracts instead) to launch an activity and get a result back, or with startService() to communicate with a background service. Intents can carry data (called extras) that can be used by the target component.

2. Starting a New Activity with an Intent#

To start a new activity, you create an Intent object that specifies the source (current activity) and the destination (the activity you want to start). Here's a simple example:

// In the source activity (e.g., MainActivity)
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)

In the above code:

  • this refers to the current context (the activity itself).
  • SecondActivity::class.java is the class of the activity you want to start.

3. Passing Data Using Intents (Extras)#

You can pass data between activities using extras. Extras are key-value pairs. For example, let's say you want to pass a string:

// In the source activity
val intent = Intent(this, SecondActivity::class.java)
val message = "Hello from MainActivity"
intent.putExtra("message_key", message)
startActivity(intent)

Here, "message_key" is the key, and message is the value. You can pass different data types like Int, Boolean, Bundle (which can hold multiple values), etc.

4. Receiving Data in the Target Activity#

In the target activity (e.g., SecondActivity), you can retrieve the data as follows:

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
 
        val receivedMessage = intent.getStringExtra("message_key")
        if (receivedMessage!= null) {
            // Do something with the received message, like display it in a TextView
            val textView: TextView = findViewById(R.id.text_view)
            textView.text = receivedMessage
        }
    }
}

If you passed an Int, you would use intent.getIntExtra("key", default_value) (where default_value is what you get if the key is not found).

5. Common Practices#

  • Using Constants for Keys: Instead of hardcoding string keys for extras everywhere, define them as constants in a companion object or a separate utility class. This reduces the chance of typos. For example:
class MainActivity : AppCompatActivity() {
    companion object {
        const val MESSAGE_KEY = "message_key"
    }
 
    //...
    val intent = Intent(this, SecondActivity::class.java)
    val message = "Hello from MainActivity"
    intent.putExtra(MESSAGE_KEY, message)
    startActivity(intent)
}
  • Checking for Nulls: Always check if the extra value is null before using it, as shown in the SecondActivity example. You never know if the intent was started without the expected extra.

6. Best Practices#

  • Using ActivityResultContracts for Result Handling (instead of deprecated startActivityForResult): If you need to get a result back from the launched activity (e.g., the user selects something in the second activity and you want to know what), use ActivityResultContracts. Here's a simple example:

In the source activity:

class MainActivity : AppCompatActivity() {
    private lateinit var resultLauncher: ActivityResultLauncher<Intent>
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data: Intent? = result.data
                val resultMessage = data?.getStringExtra("result_key")
                if (resultMessage!= null) {
                    // Do something with the result message
                    val textView: TextView = findViewById(R.id.text_view)
                    textView.text = resultMessage
                }
            }
        }
 
        val button: Button = findViewById(R.id.button)
        button.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            resultLauncher.launch(intent)
        }
    }
}

In the target activity (SecondActivity), when you want to return a result:

class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
 
        val button: Button = findViewById(R.id.button)
        button.setOnClickListener {
            val resultIntent = Intent()
            val resultMessage = "Result from SecondActivity"
            resultIntent.putExtra("result_key", resultMessage)
            setResult(RESULT_OK, resultIntent)
            finish()
        }
    }
}
  • Using Explicit Intents for Starting Known Activities: When you know exactly which activity you want to start (like in our examples where we specified the class directly), use explicit intents. Implicit intents (where you define an action and let the system find a component to handle it) are useful in other scenarios (like opening a web page in the browser), but for internal activity navigation, explicit is more straightforward and less error-prone.

7. Example Usage#

Let's put it all together in a more complete example. Assume we have two activities: MainActivity and DetailActivity. MainActivity has a list of items (let's just simulate it with some hardcoded data for simplicity), and when an item is clicked, it launches DetailActivity with the item's details.

MainActivity.kt#

class MainActivity : AppCompatActivity() {
    companion object {
        const val ITEM_KEY = "item_key"
    }
 
    private lateinit var resultLauncher: ActivityResultLauncher<Intent>
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
 
        resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == RESULT_OK) {
                val data: Intent? = result.data
                val updatedItem = data?.getParcelableExtra<Item>("updated_item_key")
                if (updatedItem!= null) {
                    // Update UI or do something with the updated item (simulated here)
                    val textView: TextView = findViewById(R.id.text_view)
                    textView.text = "Updated item: ${updatedItem.name}"
                }
            }
        }
 
        val itemList = listOf(
            Item(1, "Item 1", "Description 1"),
            Item(2, "Item 2", "Description 2")
        )
 
        val recyclerView: RecyclerView = findViewById(R.id.recycler_view)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = object : RecyclerView.Adapter<ViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
                return ViewHolder(view)
            }
 
            override fun onBindViewHolder(holder: ViewHolder, position: Int) {
                val item = itemList[position]
                holder.textView.text = item.name
                holder.itemView.setOnClickListener {
                    val intent = Intent(this@MainActivity, DetailActivity::class.java)
                    intent.putExtra(ITEM_KEY, item)
                    resultLauncher.launch(intent)
                }
            }
 
            override fun getItemCount(): Int {
                return itemList.size
            }
        }
    }
}
 
data class Item(val id: Int, val name: String, val description: String) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readInt(),
        parcel.readString()!!,
        parcel.readString()!!
    )
 
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(id)
        parcel.writeString(name)
        parcel.writeString(description)
    }
 
    override fun describeContents(): Int {
        return 0
    }
 
    companion object CREATOR : Parcelable.Creator<Item> {
        override fun createFromParcel(parcel: Parcel): Item {
            return Item(parcel)
        }
 
        override fun newArray(size: Int): Array<Item?> {
            return arrayOfNulls(size)
        }
    }
}
 
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val textView: TextView = view.findViewById(R.id.item_text_view)
}

DetailActivity.kt#

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)
 
        val item = intent.getParcelableExtra<Item>(MainActivity.ITEM_KEY)
        if (item!= null) {
            val nameTextView: TextView = findViewById(R.id.name_text_view)
            val descriptionTextView: TextView = findViewById(R.id.description_text_view)
            nameTextView.text = item.name
            descriptionTextView.text = item.description
 
            val updateButton: Button = findViewById(R.id.update_button)
            updateButton.setOnClickListener {
                val updatedItem = item.copy(name = "Updated ${item.name}")
                val resultIntent = Intent()
                resultIntent.putExtra("updated_item_key", updatedItem)
                setResult(RESULT_OK, resultIntent)
                finish()
            }
        }
    }
}

In this example:

  • We pass a custom Item object (which implements Parcelable for easy serialization/deserialization) as an extra.
  • DetailActivity can update the item (simulated here by just changing the name) and return it as a result.

8. References#

This blog has covered the essentials of handling Intents between activities in Kotlin. By following these practices and examples, you can build more interactive and data-driven Android applications.