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.
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)
}
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.
You can navigate between Activities. But what about showing multiple screens within a single Activity — like tabs? In Step 5: Android Fragments — Modular UI Design with Lifecycle and Navigation we learn how to build reusable, modular screen components.
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