Your app has multiple screens. Right now they're all isolated — tapping a button does nothing. Intents are what connect them. They're the messaging system that tells Android "open this screen", "share this text", or "open a browser to this URL".
This step covers everything you need to navigate your app confidently — explicit intents between your own screens, implicit intents to trigger system apps, passing data with extras, and getting a result back from another screen using the modern ActivityResultContracts API.
Step 3 of the Learn Android with Kotlin series — Android Intents
What Is an Intent?
An Intent is a message object that describes what you want to do. Android reads the Intent and decides who handles it — either a specific component you named (explicit) or the best-matching app on the device (implicit).
| Type | What it does | Example |
|---|---|---|
| Explicit | Navigate to a specific Activity in your own app | Open ProfileActivity from MainActivity |
| Implicit | Ask the OS to find an app that can handle an action | Open a URL, share text, dial a number |
1. Explicit Intents — Navigate Between Your Screens
Use explicit intents to move between Activities inside your own app. You specify the exact class you want to open.
Basic Navigation
// In MainActivity.kt — navigate to ProfileActivity
binding.btnOpenProfile.setOnClickListener {
val intent = Intent(this, ProfileActivity::class.java)
startActivity(intent)
}
Passing Data with Extras
Intents carry data as key-value pairs called extras. Pass them when starting the Activity, retrieve them on the other end:
// Sending data from LoginActivity
binding.btnLogin.setOnClickListener {
val username = binding.etUsername.text.toString()
val age = 25
val intent = Intent(this, ProfileActivity::class.java).apply {
putExtra("USERNAME", username)
putExtra("USER_AGE", age)
putExtra("IS_PREMIUM", true)
}
startActivity(intent)
}
// Receiving data in ProfileActivity
class ProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
// Retrieve extras — provide defaults in case they're missing
val username = intent.getStringExtra("USERNAME") ?: "Guest"
val age = intent.getIntExtra("USER_AGE", 0)
val isPremium = intent.getBooleanExtra("IS_PREMIUM", false)
binding.tvWelcome.text = "Welcome, $username!"
binding.tvAge.text = "Age: $age"
binding.tvPlan.text = if (isPremium) "Premium ⭐" else "Free"
}
}
"USERNAME" in both Activities is fragile. Define them as constants in a companion object to avoid typo bugs:
// In ProfileActivity
companion object {
const val EXTRA_USERNAME = "extra_username"
const val EXTRA_AGE = "extra_age"
}
// Sending — use the constant
intent.putExtra(ProfileActivity.EXTRA_USERNAME, username)
// Receiving — same constant
val username = intent.getStringExtra(ProfileActivity.EXTRA_USERNAME) ?: "Guest"
2. Getting a Result Back from an Activity
Sometimes you need information back from the Activity you opened — a photo the user picked, a contact they selected, a setting they changed. The modern way to do this is ActivityResultContracts — startActivityForResult() is deprecated.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// Register the launcher — must be at class level, not inside a function
private val editProfileLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val updatedName = result.data?.getStringExtra("UPDATED_NAME") ?: return@registerForActivityResult
binding.tvUsername.text = updatedName
Toast.makeText(this, "Profile updated!", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnEditProfile.setOnClickListener {
val intent = Intent(this, EditProfileActivity::class.java)
editProfileLauncher.launch(intent)
}
}
}
// In EditProfileActivity — send result back
class EditProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityEditProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnSave.setOnClickListener {
val updatedName = binding.etName.text.toString()
// Package the result and send it back
val resultIntent = Intent().apply {
putExtra("UPDATED_NAME", updatedName)
}
setResult(RESULT_OK, resultIntent)
finish() // Close this Activity and return to the caller
}
binding.btnCancel.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
}
}
}
3. Implicit Intents — Use System Apps
Implicit intents let you hand off tasks to other apps — the browser, camera, dialer, share sheet. You describe what you want to do and Android finds the right app.
Common Implicit Intents
// Open a URL in the browser
fun openWebPage(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
// Share text via share sheet
fun shareText(text: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
}
startActivity(Intent.createChooser(intent, "Share via"))
}
// Open email composer
fun sendEmail(to: String, subject: String) {
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
putExtra(Intent.EXTRA_SUBJECT, subject)
}
startActivity(intent)
}
// Open phone dialer with a number pre-filled
fun openDialer(phoneNumber: String) {
val intent = Intent(Intent.ACTION_DIAL).apply {
data = Uri.parse("tel:$phoneNumber")
}
startActivity(intent)
}
// Open Google Maps to a location
fun openMaps(query: String) {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("geo:0,0?q=$query")
}
startActivity(intent)
}
How an implicit intent is delivered through the system to start another activity
Always Check if an App Can Handle the Intent
On Android 11+ (API 30+), if no app is installed to handle an implicit intent, startActivity() throws an ActivityNotFoundException and crashes your app. Always check first - and for sensitive implicit intents like ACTION_CALL, see Android Runtime Permissions:
fun openWebPageSafely(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
// Check if any app can handle this intent before launching
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
Toast.makeText(this, "No browser app found", Toast.LENGTH_SHORT).show()
}
}
resolveActivity() to work on API 30+, you must declare the intent's action in your AndroidManifest.xml under a <queries> block:
<!-- AndroidManifest.xml -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
4. Intent Flags — Control the Back Stack
Intent flags control how activities are added to the back stack — the history of screens the user can go back through. Two flags you'll use regularly:
// After login — clear the back stack so user can't press back to login screen
fun navigateToHome() {
val intent = Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
startActivity(intent)
finish()
}
// Navigate to an existing instance of an Activity instead of creating a new one
fun navigateToMain() {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)
}
| Flag | What it does | Use when |
|---|---|---|
FLAG_ACTIVITY_NEW_TASK |
Clear entire back stack, start fresh | After login — prevent going back to login screen |
FLAG_ACTIVITY_CLEAR_TOP |
Clear all activities above the target in the stack | Navigate back to home from deep in the app |
FLAG_ACTIVITY_SINGLE_TOP |
Reuse existing instance if already on top | Prevent duplicate screens being opened |
Real-World Example — Login to Profile Flow
// LoginActivity.kt — complete with ViewBinding
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnLogin.setOnClickListener {
val username = binding.etUsername.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
if (username.isEmpty()) {
binding.etUsername.error = "Username required"
return@setOnClickListener
}
if (password.length < 6) {
binding.etPassword.error = "Minimum 6 characters"
return@setOnClickListener
}
// Navigate to ProfileActivity and clear back stack
// User cannot press back to return to LoginActivity
val intent = Intent(this, ProfileActivity::class.java).apply {
putExtra(ProfileActivity.EXTRA_USERNAME, username)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
startActivity(intent)
}
}
}
// ProfileActivity.kt
class ProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityProfileBinding
companion object {
const val EXTRA_USERNAME = "extra_username"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
val username = intent.getStringExtra(EXTRA_USERNAME) ?: "Guest"
binding.tvWelcome.text = "Welcome, $username!"
// Share profile button
binding.btnShare.setOnClickListener {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "I'm using this awesome app! My username: $username")
}
startActivity(Intent.createChooser(intent, "Share profile via"))
}
}
}
Try It Yourself
- Pass data both ways — open a settings screen from MainActivity, let the user change their display name, return it using
ActivityResultContracts, and update the name shown in MainActivity. - Build a share button — add a share button to any screen that shares the blog post URL
https://androidacademic.blogspot.comusingIntent.ACTION_SENDwith a chooser. - Safe implicit intent — add a "Visit our website" button that opens a URL. Wrap it in a
resolveActivity()check and show a Toast if no browser is found.
Frequently Asked Questions
What is the difference between explicit and implicit intents?
Explicit intents name the exact Activity to open — used within your own app. Implicit intents describe an action (view a URL, share text) and Android finds the right app to handle it.
How do I pass data between Activities?
Use intent.putExtra(key, value) when starting the Activity and intent.getStringExtra(key) to retrieve it. Define keys as constants in a companion object to avoid typo bugs.
What replaced startActivityForResult?
ActivityResultContracts.StartActivityForResult() — register it at class level with registerForActivityResult(). Call launcher.launch(intent) to start and handle the result in the callback.
Why does my implicit intent crash on Android 11?
API 30+ requires a <queries> block in AndroidManifest.xml declaring which intents your app uses. Also always call resolveActivity(packageManager) before startActivity() to check if a handler exists.
- Explicit intent —
Intent(this, TargetActivity::class.java)for your own screens - Implicit intent — describe an action, OS finds the right app to handle it
- Pass data with
putExtra(key, value)— use constants for keys - Get data back with
ActivityResultContracts— not deprecatedstartActivityForResult() - Call
setResult(RESULT_OK, intent)andfinish()to return a result - Always check
resolveActivity()before implicit intents — Android 11+ crashes without it - Declare intent queries in
<queries>in AndroidManifest for API 30+ - Use
FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASKafter login to clear back stack