Android UI, Activities & Navigation

Project: A List-Detail Android App

18 min Lesson 10 of 12

Project: A List-Detail Android App

The list-detail pattern is one of the most universal patterns in mobile development. A scrollable list of items sits on one screen; tapping any row navigates to a second screen that shows the full details of that item. You have seen this in email clients, news readers, e-commerce catalogues, and nearly every CRUD application ever built. In this capstone lesson you will wire together everything from the tutorial — layouts, RecyclerView, adapters, Intent-based navigation, and data passing — into one coherent, production-shaped app.

What We Are Building

A simple Book Catalogue app with two screens:

  • BookListActivity — shows a RecyclerView of book cards (title + author). Each card is tappable.
  • BookDetailActivity — receives the selected book via an Intent extra and renders its full details (title, author, year, synopsis).

The data layer is a plain Java list — no database yet — so you can focus entirely on the UI and navigation plumbing.

Step 1 — The Data Model

Create a plain Java class. Keep it Serializable so the whole object can be bundled into an Intent extra without manual field extraction.

// Book.java package com.example.booklist.model; import java.io.Serializable; public class Book implements Serializable { private final String title; private final String author; private final int year; private final String synopsis; public Book(String title, String author, int year, String synopsis) { this.title = title; this.author = author; this.year = year; this.synopsis = synopsis; } public String getTitle() { return title; } public String getAuthor() { return author; } public int getYear() { return year; } public String getSynopsis() { return synopsis; } }
Serializable vs Parcelable: Serializable is slower than Android's Parcelable but requires zero boilerplate for small model classes. For a real app with large lists passed between activities, implement Parcelable (or use the @Parcelize plugin if you were using Kotlin). For this project Serializable is perfectly fine.

Step 2 — The List Item Layout

Each row in the RecyclerView needs its own layout file. Create res/layout/item_book.xml:

<!-- res/layout/item_book.xml --> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" android:layout_marginVertical="6dp" app:cardCornerRadius="8dp" app:cardElevation="3dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/tvTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="17sp" android:textStyle="bold" android:textColor="@color/black" /> <TextView android:id="@+id/tvAuthor" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textSize="14sp" android:textColor="#666666" /> </LinearLayout> </androidx.cardview.widget.CardView>

Step 3 — The RecyclerView Adapter

The adapter bridges your List<Book> to the RecyclerView. The click callback is injected via the constructor so the adapter stays reusable and the Activity keeps control of navigation.

// BookAdapter.java package com.example.booklist.adapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.booklist.R; import com.example.booklist.model.Book; import java.util.List; public class BookAdapter extends RecyclerView.Adapter<BookAdapter.BookViewHolder> { public interface OnBookClickListener { void onBookClick(Book book); } private final List<Book> books; private final OnBookClickListener listener; public BookAdapter(List<Book> books, OnBookClickListener listener) { this.books = books; this.listener = listener; } @NonNull @Override public BookViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_book, parent, false); return new BookViewHolder(view); } @Override public void onBindViewHolder(@NonNull BookViewHolder holder, int position) { Book book = books.get(position); holder.tvTitle.setText(book.getTitle()); holder.tvAuthor.setText(book.getAuthor()); holder.itemView.setOnClickListener(v -> listener.onBookClick(book)); } @Override public int getItemCount() { return books.size(); } static class BookViewHolder extends RecyclerView.ViewHolder { final TextView tvTitle; final TextView tvAuthor; BookViewHolder(@NonNull View itemView) { super(itemView); tvTitle = itemView.findViewById(R.id.tvTitle); tvAuthor = itemView.findViewById(R.id.tvAuthor); } } }

Step 4 — BookListActivity

The list screen inflates a RecyclerView, seeds it with sample data, wires the adapter, and on item click builds an explicit Intent that carries the selected Book as a serializable extra.

// BookListActivity.java package com.example.booklist; import android.content.Intent; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.booklist.adapter.BookAdapter; import com.example.booklist.model.Book; import java.util.ArrayList; import java.util.List; public class BookListActivity extends AppCompatActivity { // Key constant shared by both activities — define once to avoid typos public static final String EXTRA_BOOK = "extra_book"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_book_list); List<Book> books = buildSampleBooks(); RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(new BookAdapter(books, this::onBookSelected)); } private void onBookSelected(Book book) { Intent intent = new Intent(this, BookDetailActivity.class); intent.putExtra(EXTRA_BOOK, book); // Book is Serializable — this just works startActivity(intent); } private List<Book> buildSampleBooks() { List<Book> list = new ArrayList<>(); list.add(new Book("Clean Code", "Robert C. Martin", 2008, "A handbook of agile software craftsmanship covering naming, functions, comments, and more.")); list.add(new Book("Effective Java", "Joshua Bloch", 2018, "Best practices for the Java platform, covering language idioms and API design.")); list.add(new Book("The Pragmatic Programmer", "Andrew Hunt & David Thomas", 1999, "Timeless advice on software development philosophy and professional habits.")); list.add(new Book("Design Patterns", "Gang of Four", 1994, "Canonical reference for the 23 foundational object-oriented design patterns.")); list.add(new Book("Refactoring", "Martin Fowler", 2018, "Improving the design of existing code through disciplined, step-by-step transformations.")); return list; } }
Define the extra key as a public constant on the sending Activity. BookDetailActivity imports and uses BookListActivity.EXTRA_BOOK to read the extra. This single-source-of-truth approach eliminates the classic "different string, nothing shows up" bug.

Step 5 — The Detail Layout

<!-- res/layout/activity_book_detail.xml --> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tvDetailTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:textStyle="bold" android:textColor="@color/black" /> <TextView android:id="@+id/tvDetailAuthorYear" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:textSize="16sp" android:textColor="#777777" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginVertical="16dp" android:background="#DDDDDD" /> <TextView android:id="@+id/tvDetailSynopsis" android:layout_width="match_parent" android:layout_height="wrap_content" android:lineSpacingMultiplier="1.5" android:textSize="16sp" /> </LinearLayout> </ScrollView>

Step 6 — BookDetailActivity

// BookDetailActivity.java package com.example.booklist; import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import com.example.booklist.model.Book; public class BookDetailActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_book_detail); // Retrieve the Book object passed from BookListActivity Book book = (Book) getIntent().getSerializableExtra(BookListActivity.EXTRA_BOOK); if (book == null) { // Defensive: should never happen from our own UI, but guard it finish(); return; } // Enable the Up button in the action bar if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(book.getTitle()); } TextView tvTitle = findViewById(R.id.tvDetailTitle); TextView tvAuthorYear = findViewById(R.id.tvDetailAuthorYear); TextView tvSynopsis = findViewById(R.id.tvDetailSynopsis); tvTitle.setText(book.getTitle()); tvAuthorYear.setText(book.getAuthor() + " · " + book.getYear()); tvSynopsis.setText(book.getSynopsis()); } @Override public boolean onSupportNavigateUp() { // Back arrow in toolbar returns to the list finish(); return true; } }

Step 7 — Register Both Activities in AndroidManifest.xml

Android must know about every Activity before you can start it. Declare BookDetailActivity with a parentActivityName so the system knows how to handle the Up button correctly:

<!-- AndroidManifest.xml (inside <application>) --> <activity android:name=".BookListActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".BookDetailActivity" android:exported="false" android:parentActivityName=".BookListActivity" />
Forgetting to declare an Activity in the manifest throws an ActivityNotFoundException at runtime. The app will crash the instant you call startActivity(intent). This is one of the most common beginner mistakes in Android development — always register every Activity.

How It All Fits Together

Here is the complete data flow:

  1. BookListActivity.onCreate() builds the list and hands it to BookAdapter alongside a lambda for click events.
  2. The user taps a row. The adapter calls listener.onBookClick(book), forwarded to onBookSelected() in the Activity.
  3. An explicit Intent is constructed targeting BookDetailActivity, with the Book object attached as a serializable extra.
  4. Android starts BookDetailActivity, calling its onCreate(). The activity reads the extra, guards against null, populates its views, and configures the toolbar title and Up button.
  5. Pressing Back or the Up arrow finishes BookDetailActivity and the back-stack returns the user to the list.

Summary

You have built a complete, two-screen Android application in Java. The design choices used here — a callback interface on the adapter, a shared key constant, Serializable model, explicit Intent, parentActivityName in the manifest — are the same choices you will see in professional codebases and open-source Android projects. From here, natural next steps are replacing the static list with a Room database, adding search/filter logic, and sharing the data model between screens via a ViewModel. The structural skeleton, however, stays exactly as you have written it today.