Android UI, Activities & Navigation

Navigation Between Screens

18 min Lesson 7 of 12

Navigation Between Screens

Every non-trivial Android app has more than one screen. Android gives you two complementary tools for moving between them: the classic Intent API — available since Android 1.0 — and the Navigation Component, a Jetpack library that imposes a predictable, graph-based structure over fragment and activity transitions. You need to understand both: Intents are unavoidable (they handle inter-app navigation and system actions), and the Navigation Component is now the standard for in-app navigation.

Intents: the Foundation of Android Navigation

An Intent is a message object that describes an action you want performed. When you pass it to startActivity(), the Android system finds an appropriate component to handle it and starts it for you.

There are two kinds of intent:

  • Explicit intent — you name the exact target Activity class. Used for in-app navigation.
  • Implicit intent — you declare an action (e.g. Intent.ACTION_VIEW) and let the system find an app that can handle it. Used to open a URL in a browser, share content, dial a number, and so on.

Explicit Intents: Moving to Another Activity

The minimal pattern to open DetailActivity from MainActivity is straightforward:

// Inside MainActivity.java public void openDetail(View view) { Intent intent = new Intent(this, DetailActivity.class); startActivity(intent); }

The first argument to the Intent constructor is a Context — here this refers to the current Activity, which is a Context. The second is the class object of the destination Activity.

Back stack: every call to startActivity() pushes the new activity onto the back stack. When the user presses the system Back button (or calls finish()), the top activity is popped and the previous one resumes. This stack is managed automatically; you rarely need to manipulate it directly.

Passing Data with Extras

Intents carry a Bundle of key/value pairs called extras. Use them to send primitive data to the destination activity:

// In the sending Activity Intent intent = new Intent(this, DetailActivity.class); intent.putExtra("EXTRA_ITEM_ID", 42); intent.putExtra("EXTRA_ITEM_NAME", "Laptop"); startActivity(intent);
// In DetailActivity.java (inside onCreate) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detail); Intent incoming = getIntent(); int itemId = incoming.getIntExtra("EXTRA_ITEM_ID", -1); String name = incoming.getStringExtra("EXTRA_ITEM_NAME"); TextView title = findViewById(R.id.tvTitle); title.setText(name + " (id=" + itemId + ")"); }
Define extra keys as constants. Declare them as public static final String fields on the destination Activity class — for example DetailActivity.EXTRA_ITEM_ID. Both sender and receiver then reference the same constant, eliminating typo bugs.

Implicit Intents: Leaving Your App

To open a web page you do not write a browser — you fire an implicit intent and the system routes it to the installed browser:

Uri page = Uri.parse("https://developer.android.com"); Intent browserIntent = new Intent(Intent.ACTION_VIEW, page); startActivity(browserIntent);

Before calling startActivity() on an implicit intent you should verify that some app can handle it, otherwise you get an ActivityNotFoundException crash:

if (browserIntent.resolveActivity(getPackageManager()) != null) { startActivity(browserIntent); } else { Toast.makeText(this, "No app can handle this action", Toast.LENGTH_SHORT).show(); }

Getting a Result Back: ActivityResult API

Sometimes you open a second activity and need a result back — for example a photo picker or a settings screen. The modern way (replacing the deprecated startActivityForResult) uses ActivityResultLauncher:

public class MainActivity extends AppCompatActivity { private ActivityResultLauncher<Intent> pickItemLauncher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Register BEFORE the activity is started pickItemLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { String picked = result.getData().getStringExtra("PICKED_VALUE"); // use picked value } } ); } public void launchPicker(View view) { Intent intent = new Intent(this, PickerActivity.class); pickItemLauncher.launch(intent); } }

In PickerActivity, call setResult() before finish():

Intent result = new Intent(); result.putExtra("PICKED_VALUE", "Item A"); setResult(RESULT_OK, result); finish();
Never call startActivityForResult(). It is deprecated in API 29+ and removed conceptually in newer Jetpack. Always register an ActivityResultLauncher in onCreate() — registering it later (after onStart()) throws an IllegalStateException.

The Navigation Component

When your app has many screens, managing a web of explicit intents and back-stack flags becomes error-prone. The Jetpack Navigation Component addresses this by representing your app's navigation as a declarative navigation graph — an XML file that lists destinations (Fragments) and the actions (edges) connecting them.

Add the dependency to build.gradle (app):

dependencies { def nav_version = "2.7.7" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" }

Creating a Navigation Graph

Right-click res → New → Android Resource File, choose type Navigation, and name it nav_graph.xml. In the XML editor you declare destinations and the actions between them:

<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/listFragment"> <fragment android:id="@+id/listFragment" android:name="com.example.app.ListFragment" android:label="Item List"> <action android:id="@+id/action_list_to_detail" app:destination="@id/detailFragment" /> </fragment> <fragment android:id="@+id/detailFragment" android:name="com.example.app.DetailFragment" android:label="Item Detail"> <argument android:name="itemId" app:argType="integer" android:defaultValue="-1" /> </fragment> </navigation>

Hosting the Graph: NavHostFragment

Your main activity layout needs a NavHostFragment — the container that swaps fragments in and out as navigation happens:

<!-- activity_main.xml --> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" />

app:defaultNavHost="true" intercepts the system Back button, so the Navigation Component handles back-stack popping automatically.

Navigating with NavController

Inside any Fragment hosted by the graph, obtain the NavController and call navigate() with an action ID:

// In ListFragment.java import androidx.navigation.Navigation; public class ListFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_list, container, false); Button btnGo = view.findViewById(R.id.btnGoToDetail); btnGo.setOnClickListener(v -> { // Pass arguments using a Bundle Bundle args = new Bundle(); args.putInt("itemId", 101); Navigation.findNavController(v) .navigate(R.id.action_list_to_detail, args); }); return view; } }

In DetailFragment, retrieve the argument from getArguments():

// In DetailFragment.java @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); int id = getArguments() != null ? getArguments().getInt("itemId", -1) : -1; // use id to load data }
Use Safe Args for type safety. The Navigation Component includes a Gradle plugin called Safe Args that generates typed argument classes (e.g. ListFragmentDirections.actionListToDetail(101)). This eliminates stringly-typed bundle keys and gives you compile-time errors for wrong argument types. Enable it by adding the plugin to your project-level build.gradle and applying it in the app module.

Intent vs Navigation Component: When to Use Each

  • Use Intents for: starting another app (browser, camera, phone), deep links from notifications, and moving between top-level Activities (e.g. onboarding → main app).
  • Use Navigation Component for: all in-app screen-to-screen navigation within a single Activity that hosts multiple Fragments. It handles the back stack, animated transitions, and deep-link URLs in one place.

Summary

Intents are Android's messaging system — explicit intents name a class, implicit intents name an action and let the system route them. The ActivityResultLauncher API is the current way to receive data back from a screen. The Jetpack Navigation Component replaces a tangle of intent calls with a single navigation graph: declare your destinations in XML, host them in a NavHostFragment, and navigate by calling NavController.navigate() with an action ID. Together these two mechanisms cover every navigation scenario you will encounter in a real Android application.