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:
BookListActivity.onCreate() builds the list and hands it to BookAdapter alongside a lambda for click events.
- The user taps a row. The adapter calls
listener.onBookClick(book), forwarded to onBookSelected() in the Activity.
- An explicit
Intent is constructed targeting BookDetailActivity, with the Book object attached as a serializable extra.
- 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.
- 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.