Android CLI — Build Android Apps 3x Faster With Any AI Agent

Android CLI: Build Android Apps 3x Faster With Any AI Agent

Google announced a new tool for Android developers: Android CLI. It works with any AI agent — Claude Code, Gemini CLI, Codex, or others. It makes AI-assisted Android development 3x faster and cuts LLM token usage by 70%. In this article I’ll explain what Android CLI is, what commands it has, and how to get started. What Is Android CLI? Android CLI is a command-line tool built for AI agents. ...

April 19, 2026 · 3 min

KMP Tutorial #20: Migrating an Existing Android App to KMP — Step-by-Step Guide

You have an existing Android app. It works. Users like it. Now the team wants an iOS version. Rewriting from scratch in Swift takes months. Kotlin Multiplatform lets you share the business logic and add iOS on top, without rewriting everything. In this final tutorial, we cover how to migrate an existing Android app to KMP. This is not a “rewrite from scratch” approach. It is a gradual migration — move one layer at a time, keep the Android app working at every step, and add iOS when the shared module is ready. ...

April 7, 2026 · 11 min

KMP Tutorial #18: Publishing Your KMP App — Android APK, iOS IPA, and CI/CD

Your notes app works on both Android and iOS. The data layer syncs, the UI layer has navigation and editing, and the shared ViewModel handles all business logic. Now it is time to publish. In this tutorial, you will learn how to build a release APK for Android, archive an IPA for iOS, set up GitHub Actions CI/CD, and prepare for the Play Store and App Store. What We Are Covering Android release build — signing, APK, AAB iOS release build — Xcode archive, IPA export GitHub Actions CI/CD — automated builds on every push Signing basics — keystores and provisioning profiles Store submission overview — Play Store and App Store requirements Android: Building a Release APK Debug vs Release So far, we have been building debug APKs with ./gradlew :composeApp:assembleDebug. Debug builds are not optimized and include debugging tools. For publishing, you need a release build. ...

April 6, 2026 · 9 min

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