Jetpack Compose Tutorial #25: Publishing Your App to Google Play

This is it — the final tutorial. You built a complete task manager app with Jetpack Compose, Room, Hilt, Navigation, MVI, animations, and adaptive layouts. Now let’s put it in the hands of real users. Before You Publish — Checklist Make sure your app is ready: App works — test every feature on a real device No crashes — check Logcat for errors Dark mode works — test both themes Different screen sizes — test on phone and tablet Keyboard handling — forms work with keyboard visible Proguard/R8 — release build compiles and runs App icon — custom icon (not the default green Android) App name — set in strings.xml Step 1: Generate a Signing Key Every app on Google Play must be signed. This proves the app comes from you. ...

March 26, 2026 · 6 min

Jetpack Compose Tutorial #24: Navigation, Animations, and Polish

The task manager works. You can add tasks, complete them, delete them, search, and filter. But it doesn’t feel polished. Screens change instantly. Deleting a task is jarring. There’s no feedback when you complete something. This tutorial adds the polish that makes the difference between a homework project and a real app. What We Add Feature What It Does Navigation transitions Screens slide in/out smoothly Swipe to delete Swipe a task left to delete it Animated task completion Checkbox animates, strikethrough fades in Animated list changes Tasks slide in/out when added or removed Empty state animations Gentle fade-in when list is empty Dark mode Follows system theme Snackbar with undo “Task deleted” with undo option Navigation Transitions By default, screens appear instantly. Add slide transitions: ...

March 26, 2026 · 5 min

Jetpack Compose Tutorial #23: Building the UI Layer — Screens and ViewModels

In the previous tutorial, we built the data layer — Room entities, DAO, repository, and use cases. Now we connect it to the UI. This tutorial builds the three main screens of our task manager app: the task list, the add task form, and the settings screen. What We Build UI Layer: ├── TaskListScreen + TaskListViewModel (MVI) │ ├── Search bar │ ├── Filter chips (All, Active, Completed) │ ├── Task cards with priority badge + category │ └── Swipe to delete ├── AddTaskScreen + AddTaskViewModel │ ├── Title + description fields │ ├── Category chips │ ├── Priority chips │ └── Save button with validation └── SettingsScreen + SettingsViewModel ├── Dark mode toggle └── Delete completed tasks Task List Screen — State and Intents State // ui/screens/tasklist/TaskListState.kt // Everything the task list screen needs to display data class TaskListState( val tasks: List<Task> = emptyList(), val filter: TaskFilter = TaskFilter.ALL, val searchQuery: String = "", val isLoading: Boolean = true, val error: String? = null, val taskCount: Int = 0, val completedCount: Int = 0 ) Intents // ui/screens/tasklist/TaskListIntent.kt // Every action the user can take on the task list 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 data object DeleteCompleted : TaskListIntent data object Retry : TaskListIntent } ViewModel // ui/screens/tasklist/TaskListViewModel.kt @HiltViewModel class TaskListViewModel @Inject constructor( private val getTasks: GetTasksUseCase, private val toggleTask: ToggleTaskUseCase, private val deleteTask: DeleteTaskUseCase, private val repository: TaskRepository ) : ViewModel() { private val _state = MutableStateFlow(TaskListState()) val state: StateFlow<TaskListState> = _state.asStateFlow() private var searchJob: Job? = null init { observeTasks() observeCounts() } fun onIntent(intent: TaskListIntent) { when (intent) { is TaskListIntent.Search -> { _state.update { it.copy(searchQuery = intent.query) } observeTasks() } is TaskListIntent.ChangeFilter -> { _state.update { it.copy(filter = intent.filter) } observeTasks() } is TaskListIntent.ToggleTask -> { viewModelScope.launch { toggleTask(intent.taskId) } } is TaskListIntent.DeleteTask -> { viewModelScope.launch { deleteTask(intent.taskId) } } is TaskListIntent.DeleteCompleted -> { viewModelScope.launch { repository.deleteCompletedTasks() } } is TaskListIntent.Retry -> { _state.update { it.copy(error = null) } observeTasks() } } } private fun observeTasks() { searchJob?.cancel() searchJob = viewModelScope.launch { _state.update { it.copy(isLoading = true) } try { getTasks( filter = _state.value.filter, searchQuery = _state.value.searchQuery ).collect { tasks -> _state.update { it.copy(tasks = tasks, isLoading = false, error = null) } } } catch (e: Exception) { _state.update { it.copy(error = e.message, isLoading = false) } } } } private fun observeCounts() { viewModelScope.launch { repository.getTaskCount().collect { count -> _state.update { it.copy(taskCount = count) } } } viewModelScope.launch { repository.getCompletedCount().collect { count -> _state.update { it.copy(completedCount = count) } } } } } The ViewModel pattern: ...

March 26, 2026 · 9 min

Jetpack Compose Tutorial #22: Building the Data Layer — Room + Repository Pattern

In the previous tutorial, we planned our task manager app. Now we build the foundation — the data layer. The data layer is everything below the UI: the database, the repository, and the use cases. Get this right, and the UI layer writes itself. What We Build in This Tutorial data/ ├── local/ │ ├── TaskEntity.kt ← Database table definition │ ├── TaskDao.kt ← Database operations │ └── AppDatabase.kt ← Room database ├── mapper/ │ └── TaskMapper.kt ← Convert Entity ↔ Domain Model ├── repository/ │ └── TaskRepositoryImpl.kt ← Repository implementation │ domain/ ├── model/ │ ├── Task.kt ← Clean domain model │ ├── Category.kt ← Enum │ └── Priority.kt ← Enum ├── repository/ │ └── TaskRepository.kt ← Interface (contract) └── usecase/ ├── GetTasksUseCase.kt ├── AddTaskUseCase.kt ├── ToggleTaskUseCase.kt └── DeleteTaskUseCase.kt Step 1: Domain Models The domain layer is pure Kotlin — no Android dependencies, no Room annotations. This is the “truth” of your app. ...

March 25, 2026 · 11 min

Jetpack Compose Tutorial #21: Planning the App — A Task Manager with Compose

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. ...

March 25, 2026 · 8 min

Jetpack Compose Tutorial #20: Adaptive Layouts — Phones, Tablets, and Foldables

Your app looks great on a phone. But open it on a tablet and there is a giant empty space. Open it on a foldable and the layout breaks. In 2026, adaptive layouts are not optional. Google Play requires apps to support different screen sizes. And with foldables, tablets, and ChromeOS — your app will run on screens from 4 inches to 15 inches. This tutorial teaches you how to build one codebase that adapts to every screen. ...

March 25, 2026 · 10 min

Jetpack Compose Tutorial #19: Permissions and Camera

Your app needs the camera. Or the user’s location. Or access to files. On Android, you can’t just use these — you need to ask permission first. In the old View system, permissions required complex boilerplate with onRequestPermissionsResult. In Compose, it’s much simpler — a few lines with rememberLauncherForActivityResult or the Accompanist library. Two Ways to Handle Permissions Approach Library Best For Activity Result API Built-in (no extra dependency) Simple, single permission Accompanist Permissions accompanist-permissions Multiple permissions, complex flows Both work well. Activity Result API is simpler. Accompanist gives more control. ...

March 25, 2026 · 6 min

Jetpack Compose UI Testing: performScrollTo, performScrollToNode & Gestures

Your app works when you manually tap through it. But does it work after your next code change? And the one after that? Manual testing doesn’t scale. You can’t tap through 50 screens after every change. That is what automated tests are for — and Compose makes testing surprisingly easy. Why Test Compose UI? Three reasons: Catch bugs before users do — tests run in seconds, not minutes of manual tapping Refactor with confidence — change code, run tests, know nothing broke Document behavior — tests show WHAT the UI should do Setup Add the testing dependencies to app/build.gradle.kts: ...

March 24, 2026 · 9 min

Jetpack Compose Tutorial #17: Performance — Making Your App Fast

Your app works. But it stutters when scrolling. The screen freezes for a split second when you type. Animations aren’t smooth. The problem isn’t Compose — it’s recomposition. Compose redraws parts of your UI when state changes. If it redraws too much, too often, your app feels slow. This tutorial will teach you why Compose gets slow and how to fix it. How Recomposition Works When state changes, Compose doesn’t redraw the entire screen. It redraws only the Composables that read the changed state. This is called recomposition. ...

March 24, 2026 · 9 min

Jetpack Compose Tutorial #16: Custom Layouts and Canvas — Drawing Your Own Components

Sometimes Column, Row, and Box are not enough. You need a circular progress bar. A custom chart. A drawing canvas. A shape that doesn’t exist in Material Design. That is when you use Canvas — Compose’s drawing API that lets you draw anything pixel by pixel. What is Canvas? Canvas is a Composable that gives you a blank area to draw on. You can draw shapes, lines, arcs, text — anything. ...

March 24, 2026 · 7 min