This tutorial builds a working Android To-Do List app from scratch: set up the project, lay out a RecyclerView-based UI, define a Task data model, and wire up a TaskAdapter to display and check off tasks. From there, it layers on real functionality in the order you’d actually want to build it — a dialog for typing custom task names, swipe-to-delete with ItemTouchHelper, permanent storage with Room, and an empty-state message for when the list is blank — before finally running the app. It closes with common mistakes to avoid (overusing notifyDataSetChanged(), querying Room on the main thread, skipping the empty state) and ideas for extending the app further, like due dates, categories, and cloud sync.
- 1 Integrated Development: Using Java and the Android Studio, the blog gives a detailed process of developing an Advance To-Do List App focusing more on the user interface and creating data structures, and app interaction.
- 2 Core Components: It enforces important aspects of Android Development such as the use of RecyclerView for lists, the use of data model classes for handling properties of tasks, and the use of RecyclerView Adapters which help to efficiently connect the data to be displayed and the UI, thus improving the feature of the application.
- 3 Practical Implementation: Thus, reading this tutorial, developers will be able to set up the development environment, create adequate interface, work with data of tasks, and implement interactivities to create a fully functional, user-friendly Android application.
Welcome to the exciting world of Android app development! In this tutorial, we’ll guide you through the process of creating a more sophisticated Android app using Java and Android Studio. By the end of this journey, you’ll have built a fully functional “To-Do List” app, complete with user interface design, data management, and interactive features and leant about RecyclerView Adapter, data models, and task management.
Prerequisites
Before we begin, ensure you have the following installed on your machine:
- Java Development Kit (JDK)
- Android Studio
Now, let’s get started!
Step 1: Set Up Your Development Environment
Open Android Studio and create a new project named “ToDoListApp.” Follow the wizard to configure your project settings, such as package name and save location.
Step 2: Design Your User Interface
Open the res/layout/activity_main.xml file and design the layout for your To-Do List app. We’ll use a RecyclerView to display a list of tasks. Add the following code to your XML layout:
<!-- previous layout code -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/addTaskButton"/>
<Button
android:id="@+id/addTaskButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Task"
android:layout_alignParentBottom="true"/>
</RelativeLayout>Step 3: Set Up Your Data Model
In Android development, a Data Model represents the structure and attributes of the data your application manages. In our To-Do List app example, we’ve created a Task class to serve as our data model.
public class Task {
private String title;
private boolean completed;
public Task(String title) {
this.title = title;
this.completed = false;
}
// Getters and setters...
}- Attributes (
titleandcompleted): These represent the properties of a task. Thetitleholds the description of the task, andcompletedis a flag indicating whether the task has been completed. - Constructor: Initializes a new
Taskobject with a title. By default, thecompletedflag is set tofalse. - Getters and Setters: Methods allowing other parts of the program to access and modify the attributes of the
Taskclass.
The Data Model, in this case, the Task class, serves as the backbone of your application’s data structure, encapsulating the properties and behavior of the data entities your app will manipulate.
Step 4: Create a RecyclerView Adapter
A RecyclerView Adapter is a crucial component in Android development, acting as a bridge between the data source and the RecyclerView that displays this data on the user interface.
Here’s the breakdown of the TaskAdapter class:
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.ViewHolder> {
private List<Task> tasks;
public TaskAdapter(List<Task> tasks) {
this.tasks = tasks;
}
// ... other methods
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView titleTextView;
public CheckBox completedCheckBox;
public ViewHolder(View view) {
super(view);
titleTextView = view.findViewById(R.id.titleTextView);
completedCheckBox = view.findViewById(R.id.completedCheckBox);
}
}
}TaskAdapterClass: ExtendsRecyclerView.Adapterand parametrizes it with a nested class calledViewHolder.- Constructor: Takes a
List<Task>as a parameter, initializing the adapter with the data source (the list of tasks). onCreateViewHolder: Called when theRecyclerViewneeds a newViewHolderto represent an item. It inflates the layout for each task item.onBindViewHolder: Called to bind the data to a specificViewHolder. It updates the contents of theViewHolderto reflect the item at a given position in the data source.getItemCount: Returns the total number of items in the data source.ViewHolderClass: Represents the individual items in theRecyclerView. It holds references to the views within each item, such astitleTextViewandcompletedCheckBox.
By implementing a RecyclerView Adapter, you facilitate the efficient management of large data sets and enable smooth scrolling and updates in your app’s UI. It’s an essential component for any Android app that displays lists of data.
Step 5: Implement Task Management
Add a database or use a simple in-memory list to manage your tasks. For simplicity, we’ll use a List<Task> in the MainActivity:
// previous MainActivity code
public class MainActivity extends AppCompatActivity {
private List<Task> tasks;
private TaskAdapter taskAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tasks = new ArrayList<>();
taskAdapter = new TaskAdapter(tasks);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(taskAdapter);
Button addTaskButton = findViewById(R.id.addTaskButton);
addTaskButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tasks.add(new Task("New Task"));
taskAdapter.notifyDataSetChanged();
}
});
}
}
Step 6: Enhance User Interaction
Now, let’s make our to-do list interactive. Update the TaskAdapter to handle task completion:
// Inside TaskAdapter class
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Task task = tasks.get(position);
holder.titleTextView.setText(task.getTitle());
holder.completedCheckBox.setChecked(task.isCompleted());
holder.completedCheckBox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
task.setCompleted(holder.completedCheckBox.isChecked());
}
});
}Step 7: Run Your App
Connect your device or use an emulator to run your To-Do List app. Click the green “Run” button in Android Studio, and your app should launch on the selected device. Add and complete tasks to see the updates in real-time.
Congratulations! You’ve successfully built a To-Do List app, and this experience provides a solid foundation for more advanced Android development. Experiment with additional features, explore the Android documentation, and consider integrating persistence or cloud services to take your app to the next level.
Step 8: Let Users Type Their Own Task Name
So far, tapping “Add Task” just inserts a hardcoded “New Task” string, which isn’t very useful in practice. Let’s replace that with a simple dialog that asks the user what they actually want to add.
java
addTaskButton.setOnClickListener(view -> {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("New Task");
final EditText input = new EditText(MainActivity.this);
builder.setView(input);
builder.setPositiveButton("Add", (dialog, which) -> {
String taskTitle = input.getText().toString().trim();
if (!taskTitle.isEmpty()) {
tasks.add(new Task(taskTitle));
taskAdapter.notifyItemInserted(tasks.size() - 1);
}
});
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.show();
});This is a good moment to point out the trim() and empty check before adding the task — it’s a tiny detail, but without it, users can accidentally add a bunch of blank entries just by tapping “Add” without typing anything, which is a surprisingly common source of “why are there empty rows in my list” bug reports.
Step 9: Let Users Swipe to Delete a Task
Right now our app can add tasks and mark them complete, but there’s no way to get rid of one once it’s done. A common pattern in to-do apps is letting the user swipe a task off the screen to remove it, and Android gives you most of this behavior for free through ItemTouchHelper.
Attach it to your RecyclerView right after you set up the adapter in MainActivity:
ItemTouchHelper.SimpleCallback swipeCallback = new ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false; // we're not supporting drag-to-reorder here
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
tasks.remove(position);
taskAdapter.notifyItemRemoved(position);
}
};
new ItemTouchHelper(swipeCallback).attachToRecyclerView(recyclerView);
onSwiped fires once the user has dragged an item far enough to trigger the gesture, and getAdapterPosition() tells you exactly which task they let go of. Notice we’re calling notifyItemRemoved(position) here rather than notifyDataSetChanged() — it’s a small change, but it also gives you the nice built-in “item sliding away” animation instead of the list just abruptly redrawing itself.
Step 10: Let Users Type Their Own Task Name
So far, tapping “Add Task” just inserts a hardcoded “New Task” string, which isn’t very useful in practice. Let’s replace that with a simple dialog that asks the user what they actually want to add.
addTaskButton.setOnClickListener(view -> {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("New Task");
final EditText input = new EditText(MainActivity.this);
builder.setView(input);
builder.setPositiveButton("Add", (dialog, which) -> {
String taskTitle = input.getText().toString().trim();
if (!taskTitle.isEmpty()) {
tasks.add(new Task(taskTitle));
taskAdapter.notifyItemInserted(tasks.size() - 1);
}
});
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.show();
});
This is a good moment to point out the trim() and empty check before adding the task — it’s a tiny detail, but without it, users can accidentally add a bunch of blank entries just by tapping “Add” without typing anything, which is a surprisingly common source of “why are there empty rows in my list” bug reports.
Step 11: Let Users Edit an Existing Task
Deleting and re-adding a task just to fix a typo is annoying, so it’s worth letting users tap a task to edit its title directly, reusing the same dialog pattern from Step 7. Add a click listener on the row itself inside onBindViewHolder:
java
holder.titleTextView.setOnClickListener(v -> {
AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
builder.setTitle("Edit Task");
final EditText input = new EditText(v.getContext());
input.setText(task.getTitle());
builder.setView(input);
builder.setPositiveButton("Save", (dialog, which) -> {
String updatedTitle = input.getText().toString().trim();
if (!updatedTitle.isEmpty()) {
task.setTitle(updatedTitle);
notifyItemChanged(holder.getAdapterPosition());
}
});
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.show();
});The key difference from the “add” dialog is that we pre-fill the EditText with task.getTitle() so the user sees what’s already there instead of a blank field, and we call notifyItemChanged() rather than notifyItemInserted() since we’re updating a row that already exists, not adding a new one. If you’ve wired up Room by this point, remember to also call taskDao.update(task) inside the positive button handler so the edit actually persists — otherwise it’ll look saved until the app restarts and reverts to the old title.
Step 12: Save Tasks Permanently with Room
Everything we’ve built so far lives in memory, which means closing the app throws away the entire list. For a real to-do app, tasks need to survive app restarts, and the standard way to do that in modern Android development is Room, Google’s abstraction layer over SQLite.
First, turn Task into a Room entity:
@Entity(tableName = "tasks")
public class Task {
@PrimaryKey(autoGenerate = true)
public int id;
public String title;
public boolean completed;
public Task(String title) {
this.title = title;
this.completed = false;
}
}
Then define a DAO describing the operations you need:
@Dao
public interface TaskDao {
@Insert
void insert(Task task);
@Update
void update(Task task);
@Delete
void delete(Task task);
@Query("SELECT * FROM tasks")
List<Task> getAllTasks();
}
And a small database class that ties it together:
@Database(entities = {Task.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract TaskDao taskDao();
}
From here, instead of adding directly to an in-memory List<Task>, your button click and swipe-to-delete handlers call taskDao.insert() and taskDao.delete(), and you load the saved list back into the adapter in onCreate() using taskDao.getAllTasks(). One thing worth flagging: database calls should run off the main thread, so wrap them in something like an ExecutorService or Room’s own coroutine/RxJava support rather than calling them directly inside a click listener.
Step 13: Show an Empty State When There Are No Tasks
Right now, if a user opens the app before adding anything, they just see a blank screen — which works, but it’s not obvious whether the app loaded correctly or something’s broken. A quick fix is a text view that only shows up when the task list is empty.
First, add it to activity_main.xml, sitting behind the RecyclerView:
xml
<TextView
android:id="@+id/emptyStateText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="No tasks yet — add one to get started"
android:textSize="16sp"
android:visibility="gone"/>Then in MainActivity, check the list size any time it changes and toggle visibility accordingly:
java
private TextView emptyStateText;
private void updateEmptyState() {
emptyStateText.setVisibility(tasks.isEmpty() ? View.VISIBLE : View.GONE);
}Call updateEmptyState() right after onCreate() finishes loading tasks, and again anywhere the list changes — after adding a task, after a swipe-to-delete, and after tasks load in from Room. It’s a small addition, but it’s the difference between an app that looks finished and one that looks like it might be broken the first time someone opens it.
Step 14: Show a Task Counter
Once a list grows past a handful of items, it’s genuinely useful to see progress at a glance — how many tasks are done versus how many are left — without scrolling through and counting checkboxes yourself. This only takes a single TextView and a bit of arithmetic.
Add the view to activity_main.xml, above the RecyclerView:
xml
<TextView
android:id="@+id/taskCounterText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:textSize="14sp"/>Then in MainActivity, tally completed tasks and update the label alongside the empty-state check:
java
private TextView taskCounterText;
private void updateTaskCounter() {
int completedCount = 0;
for (Task task : tasks) {
if (task.isCompleted()) completedCount++;
}
taskCounterText.setText(completedCount + " of " + tasks.size() + " tasks completed");
}Call updateTaskCounter() in the same places you call updateEmptyState() — after onCreate() loads the list, after adding or deleting a task, and after a checkbox is toggled in onBindViewHolder. Since it depends on the same events as the empty-state check, it’s worth grouping both calls into a single helper method so you don’t end up updating one and forgetting the other.
Common Mistakes to Avoid
A few issues tend to show up again and again in first-time to-do list apps, and it’s worth knowing about them before you run into them yourself.
Calling notifyDataSetChanged() after every single change is one of the most common ones — it works, but it forces the RecyclerView to redraw the entire list instead of just the row that actually changed, which gets noticeably slower as your task list grows. The more targeted methods — notifyItemInserted(), notifyItemRemoved(), notifyItemChanged() — cost almost nothing extra to use and keep your list feeling snappy.
Another common trap is doing database work on the main thread. Room will actually throw a runtime exception if you try to query it directly inside your UI code, specifically to stop you from freezing the app while it waits on disk I/O.
Finally, a lot of beginners forget to handle the empty state — what does the screen look like before any tasks have been added? A blank RecyclerView isn’t a bug, but it can look like one to a user who isn’t sure whether the app loaded correctly. A simple “No tasks yet — add one to get started” text view that’s only visible when the list is empty goes a long way toward making the app feel finished.
Where to Go From Here
At this point you’ve got a to-do list app that adds, completes, deletes, and remembers tasks between launches, which honestly covers most of what people actually expect from an app like this. From here, there are a few natural directions to take it further.
Due dates and reminders are the obvious next step — pairing your Task model with Android’s AlarmManager or WorkManager lets you notify users when something’s overdue. Categories or tags let users organize tasks into groups like “Work” and “Personal,” which usually just means adding another field to your entity and a filter on top of your query. And if you want the list to sync across a phone and a tablet, swapping Room’s local storage for a cloud backend like Firebase Firestore keeps the same UI layer largely untouched — you’re really just changing where getAllTasks() pulls its data from.
None of these are required to call the app “done,” but they’re a good way to keep practicing the same patterns — data models, adapters, and background work — on slightly harder problems.
Conclusion
Building your first Android app can be an exciting and rewarding experience. In this tutorial, we’ve covered the essential steps—from setting up your development environment and creating a simple user interface to writing functional code and testing your app on an emulator or physical device. By following these steps, you’ve gained a foundational understanding of Android development and how the various components of an app work together.
The world of Android development is vast, and there’s always more to discover. Keep coding, stay curious, and don’t be afraid to ask questions or seek out resources as you continue to grow as an Android developer. Good luck on your journey, and happy coding!
What you built here also isn’t just a to-do list — the same combination of a data model, a RecyclerView adapter, and a bit of local persistence shows up in a huge share of real Android apps, from note-taking tools to habit trackers to shopping lists. Once these pieces feel familiar, swapping Task for a different kind of item and adjusting the UI around it is a much smaller jump than starting from scratch, so this project is a solid template to revisit the next time you have an app idea worth building.
