Activities & Intents
An Activity represents one single screen with a user interface — the list of
trails, a single trail's detail view, a settings screen. Every screen the user actually looks at
in a TrailLog-style app is backed by an Activity (or, as you'll see two lessons from now, a
Fragment hosted inside one). Almost every Activity you write subclasses
AppCompatActivity, which gives you a consistent look across old and new versions of
Android plus a handful of compatibility helpers you'd otherwise have to write yourself.
A Second Screen for TrailLog: TrailDetailActivity
Right now imagine TrailLog has exactly one screen: a list of logged trails. The moment you want
to tap a trail and see its full notes, photo, and GPS coordinates, you need a second screen. That
second screen is a new Activity — TrailDetailActivity.
class TrailDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_trail_detail)
val trailId = intent.getIntExtra("trail_id", -1)
// look up the Trail with this id and bind it to the views below
}
}
TrailDetailActivity has its own layout, its own lifecycle, and its own job: show one
Trail in full. It doesn't know or care how it got launched — that's the job of an
Intent.
What an Intent Actually Is
An Intent is a message object that requests an action from another Android
component. Think of it as an envelope: you address it (which Activity should handle this?), you
can put things inside it (data to hand over), and then you send it off with a system call like
startActivity(). The system reads the envelope and delivers it to the right place.
Explicit Intents: Navigating Inside Your Own App
An explicit intent names the exact component you want to start — you already
know which class should handle it, because it's a screen you wrote yourself. This is the normal
way to move between screens inside a single app. Here's the trail list handing off to
TrailDetailActivity when the user taps a trail:
private fun openTrailDetail(trail: Trail) {
val intent = Intent(this, TrailDetailActivity::class.java)
startActivity(intent)
}
Intent(this, TrailDetailActivity::class.java) is the explicit form — the first
argument is a Context, the second is the exact class to launch. Calling
startActivity(intent) hands that envelope to the system, which creates (or resumes)
TrailDetailActivity and brings it to the foreground.
Passing Data Between Activities with Extras
An empty Intent only tells the system which screen to open — it doesn't carry any data on its
own. To tell TrailDetailActivity which trail to display, you attach
key-value pairs called extras:
Sending the trail's id
private fun openTrailDetail(trail: Trail) {
val intent = Intent(this, TrailDetailActivity::class.java)
intent.putExtra("trail_id", trail.id)
startActivity(intent)
}
Reading it back inside TrailDetailActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_trail_detail)
val trailId = intent.getIntExtra("trail_id", -1)
if (trailId == -1) {
finish() // no valid id was passed in, bail out
return
}
// fetch the matching Trail (from a repository, in later lessons) and render it
}
The second argument to getIntExtra() is a default value used if the key is missing
— a small but important safety net so a malformed launch never crashes the screen outright.
Getting a Result Back: ActivityResultContracts
Sometimes you don't just want to open a screen — you want an answer back from it. Say
TrailDetailActivity has an "Edit" mode, and after the user saves changes you want the
list screen to know a trail was updated. Older Android code handled this with
startActivityForResult() and onActivityResult(), but that API is
deprecated — it's easy to misuse, and result codes/request codes were just loosely-typed
integers you had to keep track of by hand.
The modern, type-safer replacement is ActivityResultContracts, registered once per launching component:
private val editTrailLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
// a trail was edited — refresh the list
reloadTrails()
}
}
private fun openTrailForEditing(trail: Trail) {
val intent = Intent(this, TrailDetailActivity::class.java)
intent.putExtra("trail_id", trail.id)
editTrailLauncher.launch(intent)
}
This genuinely matters for a modern app: registerForActivityResult is the API
Android currently recommends, and it's worth flagging clearly that anything you find online using
startActivityForResult is legacy and should not be used in new code.
Implicit Intents: A Quick Preview
Explicit intents work when you know exactly which of your own Activities to launch. An implicit intent is different — instead of naming a component, you describe an action (view this, share this, dial this number) and let the Android system find whichever installed app can handle it. If TrailLog later lets users share a trail summary to any app they choose, that's an implicit intent — we cover that fully in Sharing & Implicit Intents later in the track.
Declaring an Activity in AndroidManifest.xml
Every Activity — no exceptions — must be declared in AndroidManifest.xml, or the
system will refuse to start it and crash with an ActivityNotFoundException.
<application android:label="TrailLog">
<activity android:name=".TrailListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".TrailDetailActivity" />
</application>
Only TrailListActivity carries the launcher intent-filter — that's what
makes it the screen that opens when the user taps the TrailLog app icon.
TrailDetailActivity just needs a plain declaration; it's only ever reached through an
explicit intent from inside the app.