Build a todo list app with RecyclerView and SQLite in Android
Android development remains a thriving space across Australia, with indie studios in Brisbane and Melbourne and weekend hackathons hosted by communities in Sydney and Perth. Whether you are a student at RMIT, a freelancer working from a Brunswick café, or a hobbyist squeezing in code between surf sessions on the Gold Coast, the demand for solid portfolio projects is real. A todo list application is one of the most respected beginner-to-intermediate builds because it touches on data persistence, list rendering, and user interaction in a way that recruiters immediately recognise.
This guide walks through constructing a fully functional task manager with RecyclerView for the list and SQLite for local storage. You will set up the schema, wire an adapter, handle taps and long presses, and let users pick a due date through a calendar dialog. By the end, you will have a project to drop into a CV or GitHub profile.
Setting up the Android Studio project
Open Android Studio and create a new project using the Empty Activity template. Pick Kotlin if you are comfortable, though the steps translate cleanly to Java. Choose a minimum SDK around API 24 so your build covers most handsets circulating in Australia, from entry-level Telstra-branded devices to newer Samsung and Pixel flagships. Add the Material Components dependency since you will rely on Material buttons and text fields later.
Inside your build.gradle file, confirm that the recyclerview, cardview, and material libraries are listed, then sync the project. Create three packages: data for the SQLite helper and model, adapter for the RecyclerView adapter, and ui for activities and fragments. Aussie devs tend to keep structure flat and readable rather than over-engineering early, so do not worry about MVVM frameworks like Hilt or Room until you have a working baseline.
Designing the SQLite database schema
A todo item needs a stable identifier, a title, a description, a completion flag, and a due date stored as a long timestamp. Defining this clearly up front prevents awkward migrations. In your data package, create a class called TaskContract holding column names such as _id, title, description, is_done, and due_date. Using a contract class is a habit picked up from official Google samples and keeps strings out of your queries.
Next, build a TaskDbHelper extending SQLiteOpenHelper. Set the database name to tasks.db and version to 1. Inside onCreate, write a single CREATE TABLE statement with all columns and set _id as an autoincrement primary key. Override onUpgrade to drop and recreate the table while prototyping. Local code reviewers in Sydney will flag missing upgrade handling in pull requests, so getting this right early saves rediscovery later.
Building the data model and repository layer
The model class is straightforward: a data class called Task with properties matching your schema plus helpers to convert between entity and content values. Keep the data class immutable and let the repository handle mutations. From running student workshops in Adelaide, teaching learners to separate the model from the persistence layer early stops them tying views directly to cursors.
Wrap your TaskDbHelper inside a TaskRepository singleton exposing insert, update, delete, and getAll methods that return lists or cursors. Returning a Cursor directly keeps things simple, but if you plan to scale, swap it for a List<Task> and convert manually. Either way, the repository keeps the rest of the application ignorant of how storage actually works.
Creating the RecyclerView adapter
The adapter is where the user experience lives, so it deserves care. Extend RecyclerView.Adapter and inflate a card layout showing the title, a short description preview, the due date formatted as DD/MM/YYYY, and a checkbox for completion. Use notifyDataSetChanged sparingly while prototyping and switch to DiffUtil once your dataset grows. Local meet-ups in Perth often run talks on DiffUtil because performance gains are visible immediately on lower-end devices.
Inside onBindViewHolder, bind the fields and wire two click listeners: a tap to open the edit screen and a long press to surface a delete confirmation. Australian users appreciate confirmation prompts before destructive actions, so do not skip it.
Implementing add, edit, and delete flows
For adding tasks, build a small dialog or a dedicated activity with two EditText fields and a save button. On save, build a ContentValues object, call repository.insert, and refresh the list. Editing reuses the same layout but pre-fills the fields from the tapped task and calls update. Deletion uses the task id and shows a Snackbar with an undo action.
Wire your activity layout with a RecyclerView anchored to the parent, a FloatingActionButton in the bottom-right corner, and an empty-state view that appears when the database returns zero rows. The empty state might read "No tasks yet — chuck one in when you get a sec" to feel approachable. Use LinearLayoutManager for a vertical list, or StaggeredGridLayoutManager for a two-column look on tablets.
Adding a due date with a DatePickerDialog
A todo list feels half-finished until users can attach a due date. Wire a click listener on the due date field and launch a DatePickerDialog configured for the Australian locale so dates display as DD/MM/YYYY. If you want a refresher on hooking the dialog up to an EditText, the date picker guide walks through the exact steps. Convert the picked year, month, and day into a Calendar instance, then store the time in milliseconds in your database.
Format rendered dates with SimpleDateFormat using the same locale, or use a library like ThreeTenABP for LocalDate support. Australia spans three time zones, so storing timestamps in UTC avoids confusion across state borders. That consistency matters when the same user opens the app on a tablet at work and a phone on the train.
Testing on real devices and polishing the build
Run the app on an emulator first to confirm flows, then push it to a physical handset. Australians rely on Telstra and Optus networks, so test on both 4G and flaky café Wi-Fi to make sure nothing breaks under poor connectivity. Check rotation behaviour and verify that completed tasks stay completed after the activity is recreated.
Before publishing or sharing, run lint, format with the built-in formatter, and tighten hardcoded strings into strings.xml so the app is ready for localisation. If you release on the Play Store, price any premium tier in Australian dollars and remember GST applies to in-app purchases. A short readme with screenshots on GitHub catches the eye of hiring managers scanning candidates between flat whites in Surry Hills.
Storage options at a glance
| Approach | Best for | Trade-offs |
|---|---|---|
| Raw SQLite with SQLiteOpenHelper | Learning fundamentals, small apps | More boilerplate, manual migrations |
| Room persistence library | Production apps, complex queries | Annotation processing, learning curve |
| SharedPreferences | Tiny key-value flags, not lists | Not designed for relational data |
| External file storage | Large blobs or export files | Slow to query, no relational joins |
Recommended practices for a reliable todo app
- Stick to a single source of truth in your repository and avoid passing cursors between activities.
- Use DiffUtil before the list feels laggy, not after, since performance debt compounds fast.
- Store dates in UTC and format them at the presentation layer.
- Provide an undo action for destructive operations using Snackbar.
- Keep destructive confirms visible but unobtrusive, following Material guidelines.
- Localise strings and respect right-to-left layouts even if your first audience is Australian.
- Write at least one instrumented test around the database helper to catch schema mistakes early.