You have learned every piece: layouts, state, navigation, ViewModel, MVI, Room, Hilt, animations, testing, and adaptive layouts. Now it is time to use them all together.

In the next five tutorials, we will build a complete task manager app from scratch. Not a toy demo — a real app with real architecture that you could publish to Google Play.

This tutorial is about planning — deciding what to build, how to organize the code, and what dependencies to use. No code yet. Just decisions.

What We Are Building

A task manager app with these features:

Core Features

  • Create tasks with title, description, and due date
  • Mark tasks as complete/incomplete
  • Delete tasks (swipe to delete)
  • Search tasks by title
  • Filter tasks (all, active, completed)

Organization

  • Categories (work, personal, shopping, health)
  • Priority levels (low, medium, high)
  • Sort by date, priority, or name

UX

  • Bottom navigation (Tasks, Add, Settings)
  • Dark mode toggle
  • Empty state screens
  • Loading and error states
  • Animations on add/delete/complete

Data

  • Local database (Room) — works offline
  • Preferences (DataStore) — stores settings

Architecture: Clean Architecture + MVI

We will use a simplified clean architecture:

┌─────────────────────────────────────────────┐
│                    UI Layer                  │
│                                             │
│  Screens (Composables)                      │
│       ↕ observes state                      │
│  ViewModels (MVI: State + Intent)           │
└────────────────┬────────────────────────────┘
                 │ calls
┌────────────────▼────────────────────────────┐
│                 Domain Layer                 │
│                                             │
│  Use Cases (one per business action)        │
│  Models (Task, Category, Priority)          │
└────────────────┬────────────────────────────┘
                 │ calls
┌────────────────▼────────────────────────────┐
│                  Data Layer                  │
│                                             │
│  Repository (single source of truth)        │
│  Room Database (DAO, Entity)                │
│  DataStore (settings/preferences)           │
└─────────────────────────────────────────────┘

Why This Architecture?

LayerPurposeDepends On
UIShow screens, handle user inputDomain (ViewModels call use cases)
DomainBusiness logic, rulesNothing (pure Kotlin)
DataDatabase, API, storageDomain (implements interfaces)

Each layer depends only on the layer below it. The UI doesn’t know about Room. The Domain doesn’t know about Compose. This makes the code testable and maintainable.

Why MVI (Not MVVM)?

We covered MVI in Tutorial #10. Quick recap:

// One state per screen — predictable and debuggable
data class TaskListState(
    val tasks: List<Task> = emptyList(),
    val filter: TaskFilter = TaskFilter.ALL,
    val searchQuery: String = "",
    val isLoading: Boolean = true,
    val error: String? = null
)

// Intents describe what the user wants
sealed interface TaskListIntent {
    data class Search(val query: String) : TaskListIntent
    data class ChangeFilter(val filter: TaskFilter) : TaskListIntent
    data class ToggleTask(val taskId: Long) : TaskListIntent
    data class DeleteTask(val taskId: Long) : TaskListIntent
}

One state object. Clear intents. No scattered LiveData or MutableState variables.

Project Structure

app/src/main/java/com/kemalcodes/taskmanager/
├── di/                          ← Hilt modules
│   └── AppModule.kt
├── data/                        ← Data layer
│   ├── local/
│   │   ├── TaskEntity.kt        ← Room entity
│   │   ├── TaskDao.kt           ← Room DAO
│   │   └── AppDatabase.kt       ← Room database
│   ├── preferences/
│   │   └── SettingsDataStore.kt  ← DataStore for settings
│   └── repository/
│       └── TaskRepositoryImpl.kt ← Repository implementation
├── domain/                      ← Domain layer
│   ├── model/
│   │   ├── Task.kt              ← Domain model (not Entity)
│   │   ├── Category.kt          ← Enum
│   │   └── Priority.kt          ← Enum
│   ├── repository/
│   │   └── TaskRepository.kt    ← Interface (not implementation)
│   └── usecase/
│       ├── GetTasksUseCase.kt
│       ├── AddTaskUseCase.kt
│       ├── ToggleTaskUseCase.kt
│       └── DeleteTaskUseCase.kt
├── ui/                          ← UI layer
│   ├── navigation/
│   │   ├── AppNavigation.kt
│   │   └── Routes.kt
│   ├── screens/
│   │   ├── tasklist/
│   │   │   ├── TaskListScreen.kt
│   │   │   ├── TaskListState.kt
│   │   │   ├── TaskListIntent.kt
│   │   │   └── TaskListViewModel.kt
│   │   ├── addtask/
│   │   │   ├── AddTaskScreen.kt
│   │   │   └── AddTaskViewModel.kt
│   │   └── settings/
│   │       ├── SettingsScreen.kt
│   │       └── SettingsViewModel.kt
│   ├── components/              ← Reusable components
│   │   ├── TaskCard.kt
│   │   ├── CategoryChip.kt
│   │   ├── PriorityBadge.kt
│   │   └── EmptyState.kt
│   └── theme/
│       ├── Color.kt
│       ├── Theme.kt
│       └── Type.kt
├── App.kt                       ← @HiltAndroidApp
└── MainActivity.kt              ← @AndroidEntryPoint

Why Separate Domain Models from Entities?

// Room Entity — tied to database schema
@Entity(tableName = "tasks")
data class TaskEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val description: String,
    val category: String,         // Stored as String in DB
    val priority: Int,            // Stored as Int in DB
    val isCompleted: Boolean,
    val dueDate: Long?,
    val createdAt: Long,
    val updatedAt: Long
)

// Domain Model — clean, type-safe, no database annotations
data class Task(
    val id: Long = 0,
    val title: String,
    val description: String = "",
    val category: Category = Category.PERSONAL,
    val priority: Priority = Priority.MEDIUM,
    val isCompleted: Boolean = false,
    val dueDate: LocalDate? = null,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    val updatedAt: LocalDateTime = LocalDateTime.now()
)

The domain model uses proper types (Category enum, LocalDate). The entity uses database-compatible types (String, Long). The repository converts between them.

Data Models

// domain/model/Category.kt
enum class Category(val displayName: String) {
    WORK("Work"),
    PERSONAL("Personal"),
    SHOPPING("Shopping"),
    HEALTH("Health")
}

// domain/model/Priority.kt
enum class Priority(val level: Int, val displayName: String) {
    LOW(0, "Low"),
    MEDIUM(1, "Medium"),
    HIGH(2, "High")
}

// domain/model/TaskFilter.kt
enum class TaskFilter {
    ALL, ACTIVE, COMPLETED
}

Dependencies

# gradle/libs.versions.toml
[versions]
compose-bom = "2024.09.00"
room = "2.7.1"
hilt = "2.56.2"
navigation = "2.9.7"
datastore = "1.2.1"
ksp = "2.1.21-2.0.5"
kotlin = "2.1.21"

[libraries]
# Compose (via BOM)
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }

# Room
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }

# Hilt
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt-navigation = { module = "androidx.hilt:hilt-navigation-compose", version = "1.2.0" }

# Navigation
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }

# DataStore
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }

Screens Overview

Screen 1: Task List

┌──────────────────────────┐
│ Tasks              🔍    │  ← Title + search toggle
├──────────────────────────┤
│ All | Active | Completed │  ← Filter chips
├──────────────────────────┤
│ ┌──────────────────────┐ │
│ │ 🔴 Buy groceries     │ │  ← Task card with priority
│ │    Shopping · Due Mon │ │     badge, category, due date
│ │              ☐       │ │     Checkbox to complete
│ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ 🟡 Review PR #42     │ │
│ │    Work · Due Today   │ │
│ │              ☑       │ │  ← Completed task
│ └──────────────────────┘ │
│                          │
│ 15 tasks · 3 completed   │  ← Footer stats
├──────────────────────────┤
│ 🏠 Tasks  ➕ Add  ⚙ Set │  ← Bottom navigation
└──────────────────────────┘

Screen 2: Add Task

┌──────────────────────────┐
│ ← New Task               │  ← Back + title
├──────────────────────────┤
│                          │
│ Title                    │  ← Text field
│ [________________________]│
│                          │
│ Description              │  ← Text field (multiline)
│ [________________________]│
│ [________________________]│
│                          │
│ Category                 │
│ [Work] [Personal] ...    │  ← Chips
│                          │
│ Priority                 │
│ [Low] [Medium] [High]    │  ← Chips
│                          │
│ Due Date                 │
│ [Select date        📅]  │  ← Date picker
│                          │
│ [     Save Task     ]    │  ← Button (disabled if empty)
└──────────────────────────┘

Screen 3: Settings

┌──────────────────────────┐
│ Settings                 │
├──────────────────────────┤
│                          │
│ Dark Mode          [🔘]  │  ← Toggle
│                          │
│ Default Category         │
│ [Personal        ▼]     │  ← Dropdown
│                          │
│ Default Priority         │
│ [Medium          ▼]     │  ← Dropdown
│                          │
│ ─────────────────────    │
│                          │
│ Completed Tasks: 42     │  ← Stats
│ Total Tasks: 128        │
│                          │
│ [Delete Completed Tasks] │  ← Danger button
│                          │
├──────────────────────────┤
│ 🏠 Tasks  ➕ Add  ⚙ Set │
└──────────────────────────┘

What We Will Build in Each Tutorial

TutorialWhat We BuildTutorials Used
#21 (this one)Planning + project setup
#22Data layer (Room + Repository)#13 Room, #14 Hilt
#23UI layer (Screens + ViewModels)#5 State, #10 MVI, #9 ViewModel
#24Navigation + Animations + Polish#8 Navigation, #15 Animations
#25Testing + Publishing#18 Testing

Each tutorial builds on the previous one. By #25, you will have a complete, publishable app.

Setting Up the Project

Step 1: Create a New Project

In Android Studio:

  1. File → New → New Project
  2. Choose Empty Activity
  3. Name: TaskManager
  4. Package: com.kemalcodes.taskmanager
  5. Minimum SDK: API 24
  6. Click Finish

Step 2: Add Dependencies

Update your build.gradle.kts files with all the dependencies listed above. (We covered each dependency’s setup in the individual tutorials — Room in #13, Hilt in #14, Navigation in #8, DataStore in KMP #8.)

Step 3: Create the Folder Structure

Create all the packages listed in the Project Structure section above. Having the structure ready before writing code keeps everything organized.

Step 4: Create the Application Class

// App.kt
@HiltAndroidApp
class App : Application()

Step 5: Update AndroidManifest.xml

<application
    android:name=".App"
    ...>
    <activity
        android:name=".MainActivity"
        android:exported="true"
        ...>
    </activity>
</application>

Step 6: Create MainActivity

// MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TaskManagerTheme {
                AppNavigation()
            }
        }
    }
}

The Rules We Follow

As we build this app, we will follow these rules:

  1. One state per screen — every screen has a single State data class
  2. Intents for user actions — sealed interface for every screen
  3. Repository pattern — all data goes through the repository
  4. Hilt for everything — no manual dependency creation
  5. Flow for reactive data — Room emits, ViewModel collects, UI observes
  6. Composables don’t know about ViewModels — use callbacks
  7. Tests for every ViewModel — at minimum

Source Code

The complete working code for this tutorial series is on GitHub:

View source code on GitHub →

What’s Next?

In the next tutorial, we will build the Data Layer — Room database, entities, DAOs, repository, and use cases. The foundation that everything else builds on.

See you there.