Topic 14: Mini Project — 1-Week Duration Until Launch in the Play Store
📖 16 min read · 🎯 advanced · 🧭 Prerequisites: application-architecture-using-mvcmvpmvvm-each-topic-covered-on-one-day, operators-on-collections
Why this matters
Here's the thing — most beginners finish a course and still feel like they can't build anything real. That gap between learning and doing is exactly what this week closes. You have seven days, one goal: take a simple To-Do List app from zero lines of code to a live listing on the Play Store. No shortcuts, no half-finished screens. By the end, you'll have touched RecyclerView, SQLite persistence, dialogs, and the full Play Store submission pipeline — and more importantly, you'll have proof on your phone and your portfolio that you can actually ship.
What You'll Learn
- Scaffold and wire a full Android project from project creation through Play Store submission in seven structured days
- Build a RecyclerView-backed task list with a custom adapter, ViewHolder, and delete callback interface
- Persist tasks across app restarts using a SQLite database with
SQLiteOpenHelper,TaskDbHelper, and aTaskRepositorylayer - Launch a dialog for adding tasks, integrate input validation, and use Snackbar for user feedback
- Generate a signed APK, prepare a store listing, and publish through Google Play Console
The Analogy
Think of this week like opening a food truck. Day one you park the truck and paint the exterior — that's your layout. Day two you install the kitchen equipment — RecyclerView and adapters. Day three you practice taking orders and throwing them out — adding and deleting tasks. Day four you wire in the freezer so food survives overnight — SQLite persistence. Day five you do a full soft-open with friends — testing and debugging. Day six you tweak the menu font and hang fairy lights — UI polish. And on day seven you roll up to the busiest street corner in town, flip the sign to OPEN, and start serving the public — that's your Play Store launch. Every day has a job; skip one and the truck stalls.
Chapter 1: Project Ideas to Choose From
Before committing to a week of work, pick a project that is small enough to finish but real enough to teach you something. Here are five solid candidates:
- To-Do List App — manage daily tasks with add, edit, and delete functionality
- Weather App — fetch weather data from a public API and display current conditions
- Expense Tracker — log daily expenses with category tagging and a running total
- Quiz App — multiple-choice questions with a scoring system
- Recipe App — a browsable list of recipes with a detail view per recipe
This lesson walks through the To-Do List App in full — it hits RecyclerView, dialogs, SQLite, and the Play Store pipeline all in one week.
Chapter 2: Day 1 — Setup and Basic UI
Step 1 — Create the Android Studio Project
- Open Android Studio and choose New Project > Empty Views Activity.
- Set the package name (e.g.,
com.example.todolist) and minimum SDK. - Confirm Gradle syncs cleanly before touching any layout files.
Step 2 — Design the Main Layout
Create activity_main.xml with a RecyclerView to display tasks and a FloatingActionButton to trigger task creation.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:layout_above="@+id/fab" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"
android:src="@drawable/ic_add"
android:contentDescription="Add Task"
app:backgroundTint="@color/colorAccent" />
</RelativeLayout>
Step 3 — Create the Task Item Layout
Create item_task.xml for each row rendered by the RecyclerView.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/task_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Task Name"
android:textSize="16sp" />
<Button
android:id="@+id/delete_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete" />
</LinearLayout>
Chapter 3: Day 2 — Implementing RecyclerView and Task Model
Step 1 — Create the Task Model
Task.java is a plain data class with a single name field.
package com.example.todolist;
public class Task {
private String name;
public Task(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Step 2 — Create the RecyclerView Adapter
TaskAdapter.java binds each Task to an item_task.xml row and fires a delete callback through the OnTaskDeleteListener interface.
package com.example.todolist;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.TaskViewHolder> {
private List<Task> tasks;
private OnTaskDeleteListener listener;
public interface OnTaskDeleteListener {
void onTaskDelete(int position);
}
public TaskAdapter(List<Task> tasks, OnTaskDeleteListener listener) {
this.tasks = tasks;
this.listener = listener;
}
public List<Task> getTasks() {
return tasks;
}
@NonNull
@Override
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_task, parent, false);
return new TaskViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
Task task = tasks.get(position);
holder.taskName.setText(task.getName());
holder.deleteButton.setOnClickListener(v -> listener.onTaskDelete(position));
}
@Override
public int getItemCount() {
return tasks.size();
}
static class TaskViewHolder extends RecyclerView.ViewHolder {
TextView taskName;
Button deleteButton;
TaskViewHolder(@NonNull View itemView) {
super(itemView);
taskName = itemView.findViewById(R.id.task_name);
deleteButton = itemView.findViewById(R.id.delete_button);
}
}
}
Step 3 — Wire RecyclerView in MainActivity
This initial MainActivity.java hard-codes task creation via the FAB to verify the RecyclerView renders correctly before the real dialog is built on Day 3.
package com.example.todolist;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity
implements TaskAdapter.OnTaskDeleteListener {
private RecyclerView recyclerView;
private TaskAdapter taskAdapter;
private List<Task> taskList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
FloatingActionButton fab = findViewById(R.id.fab);
taskList = new ArrayList<>();
taskAdapter = new TaskAdapter(taskList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(taskAdapter);
fab.setOnClickListener(v -> {
taskList.add(new Task("New Task"));
taskAdapter.notifyItemInserted(taskList.size() - 1);
});
}
@Override
public void onTaskDelete(int position) {
taskList.remove(position);
taskAdapter.notifyItemRemoved(position);
}
}
Chapter 4: Day 3 — Adding and Deleting Tasks with a Dialog
Replace the hard-coded FAB behavior with a proper dialog so users can type a real task name.
Step 1 — Create AddTaskDialog
package com.example.todolist;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.NonNull;
public class AddTaskDialog extends Dialog {
private EditText taskNameEditText;
private Button addButton;
private OnTaskAddListener listener;
public interface OnTaskAddListener {
void onTaskAdd(String taskName);
}
public AddTaskDialog(@NonNull Context context, OnTaskAddListener listener) {
super(context);
this.listener = listener;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_add_task);
taskNameEditText = findViewById(R.id.task_name_edit_text);
addButton = findViewById(R.id.add_button);
addButton.setOnClickListener(v -> {
String taskName = taskNameEditText.getText().toString();
if (!taskName.isEmpty()) {
listener.onTaskAdd(taskName);
dismiss();
}
});
}
}
Step 2 — Create dialog_add_task.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/task_name_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Task Name"
android:inputType="text" />
<Button
android:id="@+id/add_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Task" />
</LinearLayout>
Step 3 — Update MainActivity to Use AddTaskDialog
package com.example.todolist;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity
implements TaskAdapter.OnTaskDeleteListener, AddTaskDialog.OnTaskAddListener {
private RecyclerView recyclerView;
private TaskAdapter taskAdapter;
private List<Task> taskList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
FloatingActionButton fab = findViewById(R.id.fab);
taskList = new ArrayList<>();
taskAdapter = new TaskAdapter(taskList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(taskAdapter);
fab.setOnClickListener(v -> {
AddTaskDialog dialog = new AddTaskDialog(this, this);
dialog.show();
});
}
@Override
public void onTaskDelete(int position) {
taskList.remove(position);
taskAdapter.notifyItemRemoved(position);
}
@Override
public void onTaskAdd(String taskName) {
taskList.add(new Task(taskName));
taskAdapter.notifyItemInserted(taskList.size() - 1);
}
}
Chapter 5: Day 4 — Persisting Data with SQLite
In-memory lists vanish on app restart. Day 4 wires in a SQLite database so tasks survive process death.
Step 1 — Create TaskDbHelper
TaskDbHelper extends SQLiteOpenHelper and owns schema creation and migration.
package com.example.todolist;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class TaskDbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "tasks.db";
private static final int DATABASE_VERSION = 1;
public TaskDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
final String SQL_CREATE_TASKS_TABLE =
"CREATE TABLE tasks (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"name TEXT NOT NULL" +
");";
db.execSQL(SQL_CREATE_TASKS_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS tasks");
onCreate(db);
}
}
Step 2 — Create TaskRepository
TaskRepository encapsulates all SQL operations — insert, delete, and bulk read — so MainActivity never touches raw cursors.
package com.example.todolist;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;
public class TaskRepository {
private TaskDbHelper dbHelper;
public TaskRepository(Context context) {
dbHelper = new TaskDbHelper(context);
}
public void addTask(Task task) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", task.getName());
db.insert("tasks", null, values);
}
public void deleteTask(String taskName) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.delete("tasks", "name = ?", new String[]{taskName});
}
public List<Task> getAllTasks() {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query("tasks", null, null, null, null, null, null);
List<Task> tasks = new ArrayList<>();
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
tasks.add(new Task(name));
}
cursor.close();
return tasks;
}
}
Step 3 — Integrate Repository into MainActivity
package com.example.todolist;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List;
public class MainActivity extends AppCompatActivity
implements TaskAdapter.OnTaskDeleteListener, AddTaskDialog.OnTaskAddListener {
private RecyclerView recyclerView;
private TaskAdapter taskAdapter;
private TaskRepository taskRepository;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = findViewById(R.id.recyclerView);
FloatingActionButton fab = findViewById(R.id.fab);
taskRepository = new TaskRepository(this);
List<Task> taskList = taskRepository.getAllTasks();
taskAdapter = new TaskAdapter(taskList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(taskAdapter);
fab.setOnClickListener(v -> {
AddTaskDialog dialog = new AddTaskDialog(this, this);
dialog.show();
});
}
@Override
public void onTaskDelete(int position) {
Task task = taskAdapter.getTasks().get(position);
taskRepository.deleteTask(task.getName());
taskAdapter.getTasks().remove(position);
taskAdapter.notifyItemRemoved(position);
}
@Override
public void onTaskAdd(String taskName) {
Task task = new Task(taskName);
taskRepository.addTask(task);
taskAdapter.getTasks().add(task);
taskAdapter.notifyItemInserted(taskAdapter.getTasks().size() - 1);
}
}
SQLite Flow Diagram
sequenceDiagram
participant User
participant MainActivity
participant TaskRepository
participant SQLiteDatabase
User->>MainActivity: taps FAB → types task name → confirms
MainActivity->>TaskRepository: addTask(task)
TaskRepository->>SQLiteDatabase: INSERT INTO tasks (name) VALUES (?)
SQLiteDatabase-->>TaskRepository: row inserted
TaskRepository-->>MainActivity: returns
MainActivity->>MainActivity: notifyItemInserted()
User->>MainActivity: taps Delete on a row
MainActivity->>TaskRepository: deleteTask(task.getName())
TaskRepository->>SQLiteDatabase: DELETE FROM tasks WHERE name = ?
SQLiteDatabase-->>TaskRepository: rows affected
TaskRepository-->>MainActivity: returns
MainActivity->>MainActivity: notifyItemRemoved()
Chapter 6: Day 5 — Testing and Debugging
the trainer's golden rule: never assume it works until you've killed the app and restarted it.
Testing Checklist
- Add several tasks, force-stop the app, reopen it — tasks must reload from the database
- Delete a task, verify it disappears from the list and is gone after a restart
- Submit the dialog with an empty task name — the empty-check guard in
AddTaskDialogmust prevent insertion - Rotate the device — RecyclerView must restore scroll position correctly
Using Logcat
Open Logcat in Android Studio and filter by your package name. Drop strategic log lines during development:
Log.d("TodoApp", "Task added: " + taskName);
Log.d("TodoApp", "Task deleted at position: " + position);
Log.d("TodoApp", "Tasks loaded from DB: " + taskList.size());
Remove or demote these to Log.v before release — Logcat noise confuses future debugging sessions.
Chapter 7: Day 6 — UI Enhancements and Polishing
A stable app that looks rough never gets downloads. Day 6 is about earning trust through visual quality.
Input Validation
The AddTaskDialog already guards against empty input. Add a visible error state so the user knows why nothing happened:
addButton.setOnClickListener(v -> {
String taskName = taskNameEditText.getText().toString().trim();
if (taskName.isEmpty()) {
taskNameEditText.setError("Task name cannot be empty");
} else {
listener.onTaskAdd(taskName);
dismiss();
}
});
Snackbar Over Toast
Snackbars are dismissible, actionable, and follow Material Design guidelines. Replace any Toast calls with:
Snackbar.make(recyclerView, "Task deleted", Snackbar.LENGTH_SHORT)
.setAction("UNDO", v -> {
// restore task if you implement undo
})
.show();
Themes and Styles
Define your color palette in res/values/colors.xml and apply a Material theme in res/values/themes.xml:
<style name="Theme.TodoList" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/colorAccent</item>
</style>
Add descriptive icons to the delete button and FAB via vector drawables from the Material Icons library — go to File > New > Vector Asset in Android Studio to import them without adding external dependencies.
Chapter 8: Day 7 — Prepare for Launch
All code-complete. Time to ship.
Step 1 — Create the App Icon
- Design a 512×512 PNG icon (use Figma, Canva, or Android Studio's Image Asset Studio).
- Android Studio's Image Asset Studio (right-click
res > New > Image Asset) generates all requiredmipmap-*density variants automatically.
Step 2 — Generate a Signed APK or App Bundle
- In Android Studio, go to Build > Generate Signed Bundle / APK.
- Choose Android App Bundle (
.aab) — Google Play prefers bundles over APKs for smaller install sizes. - Create or select a keystore file. Back up the keystore — losing it means you can never update the app under the same listing.
- Choose the
releasebuild variant and click Finish.
Step 3 — Prepare the Store Listing
Gather these assets before opening Play Console:
| Asset | Requirement |
|---|---|
| App icon | 512×512 PNG |
| Feature graphic | 1024×500 PNG |
| Screenshots | Minimum 2, phone screenshots required |
| Short description | Up to 80 characters |
| Full description | Up to 4,000 characters |
| Privacy policy URL | Required if app collects any data |
Step 4 — Publish to Google Play Store
- Go to play.google.com/console and create a developer account (one-time $25 registration fee).
- Click Create app, fill in the app name, default language, and app/game toggle.
- Navigate to Production > Releases > Create new release.
- Upload the signed
.aabfile. - Complete all required sections in the Store presence and Policy dashboards — Play Console will show a checklist of everything blocking submission.
- Submit for review. First-time submissions typically take 1–3 days to review.
🧪 Try It Yourself
Task: Add an "Edit Task" feature to the To-Do List app.
- Add a second button (
edit_button) toitem_task.xmlalongside the existing delete button. - In
TaskAdapter, add anOnTaskEditListenerinterface withvoid onTaskEdit(int position, String currentName). - When the edit button is clicked, open an
AddTaskDialogpre-populated with the current task name. - On confirm, call
taskRepository.deleteTask(oldName)andtaskRepository.addTask(newTask)in sequence, then calltaskAdapter.notifyItemChanged(position).
Success criterion: You should be able to tap Edit on any task, change its name in the dialog, confirm, and see the updated name immediately in the list — and the new name should persist after killing and restarting the app.
Starter snippet for edit wiring in MainActivity:
taskAdapter.setOnTaskEditListener((position, currentName) -> {
AddTaskDialog dialog = new AddTaskDialog(this, newName -> {
Task oldTask = taskAdapter.getTasks().get(position);
taskRepository.deleteTask(oldTask.getName());
Task updated = new Task(newName);
taskRepository.addTask(updated);
taskAdapter.getTasks().set(position, updated);
taskAdapter.notifyItemChanged(position);
});
dialog.show();
});
🔍 Checkpoint Quiz
Q1. Why does TaskRepository call dbHelper.getWritableDatabase() for inserts and deletes but dbHelper.getReadableDatabase() for queries?
Q2. Given this snippet from TaskAdapter:
holder.deleteButton.setOnClickListener(v -> listener.onTaskDelete(position));
A bug appears where deleting the 3rd item in a 5-item list sometimes deletes the wrong item after previous deletions. What is the likely cause?
A) notifyItemRemoved was not called after taskList.remove()
B) getItemCount() returns a stale count
C) The position variable captured in the lambda is the adapter position at binding time, which can be stale after earlier items are removed
D) RecyclerView does not support item removal
Q3. You force-stop the app after adding three tasks, then reopen it. The list is empty. Which of these is the most likely root cause?
A) FloatingActionButton was not wired up correctly
B) taskRepository.getAllTasks() is not being called in onCreate, so the adapter starts with an empty in-memory list
C) SQLite databases are deleted when an app is force-stopped
D) RecyclerView does not render items loaded asynchronously
Q4. What is the key reason Google Play requires an .aab App Bundle rather than a plain .apk for new apps?
A1. getWritableDatabase() opens (or creates) the database in read-write mode and is required for any statement that modifies data. getReadableDatabase() opens it in read-only mode when possible, which is safer and avoids unnecessary write locks for SELECT queries. In practice on Android they often return the same object, but using the semantically correct call communicates intent and prevents accidental writes in query paths.
A2. C) — The position captured in the lambda is the value at the moment onBindViewHolder was called. After other items are deleted above it in the list, the actual index of that item changes but the lambda still holds the old value. The fix is to call holder.getAdapterPosition() inside the click handler instead of relying on the captured position.
A3. B) — SQLite databases survive force-stop; data is written to disk. The bug is that taskRepository.getAllTasks() either isn't called in onCreate, or its result is discarded and the adapter is initialized with a new empty ArrayList instead of the list returned from the repository.
A4. App Bundles let Google Play generate optimized, device-specific APKs (only the screen density, language, and ABI resources needed for that device), reducing download and install size by up to 15–35% compared to a universal APK that ships all variants to every device.
🪞 Recap
- A seven-day project plan covering UI scaffolding, RecyclerView + adapter, dialogs, SQLite persistence, testing, polish, and Play Store submission is achievable for a solo Android developer.
TaskAdapterdelegates deletion back toMainActivitythrough theOnTaskDeleteListenerinterface, keeping the adapter free of business logic.TaskDbHelperowns schema creation and upgrade;TaskRepositoryowns all SQL operations;MainActivityonly calls repository methods — a clean three-layer separation.- SQLite persistence via
TaskRepository.addTask,deleteTask, andgetAllTasksensures tasks survive app restarts and process death. - A signed App Bundle (
.aab), app icon, feature graphic, screenshots, and a privacy policy are the minimum requirements to submit a new app to Google Play Console.
📚 Further Reading
- Android RecyclerView docs — the canonical guide to adapters, ViewHolders, and layout managers
- Android SQLite docs — official reference for
SQLiteOpenHelper, cursors, andContentValues - Material Design Components for Android — FAB, Snackbar, and theme guidance straight from the source
- Google Play Console Help — Prepare your app — the official checklist for first-time submissions
- ⬅️ Previous: Operators on Collections
- ➡️ Next: Multithreading in Java