Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Software Development

Android App Development with Kotlin: 2026 Best Practices

Jetpack Compose is the UI layer. Kotlin Multiplatform shares business logic. Here's how to build modern Android apps with the patterns and tools that actually matter in 2026.

🤖 Mobile Development February 19, 2026 13 min read

Android development in 2026 barely resembles the XML-layout, Java-first world of a few years ago. Jetpack Compose is now the default UI toolkit — Google's own apps run on it, and the library ecosystem has followed. Kotlin 2.1 brings compiler improvements and better multiplatform support. And the architecture story has finally settled into clear, well-documented patterns. Here's a practical guide to building Android apps the right way this year.

📋 Table of Contents

Jetpack Compose in Production

Jetpack Compose has crossed the adoption threshold. Google Maps, Play Store, Gmail — they all run Compose. The library is stable, performant (especially with the compiler improvements in Kotlin 2.1), and the component ecosystem covers almost every UI pattern you'll encounter.

Building a Production Screen

@Composable
fun OrdersScreen(
    viewModel: OrdersViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Orders") },
                actions = {
                    FilterChip(
                        selected = uiState.filter != OrderFilter.ALL,
                        onClick = { viewModel.toggleFilter() },
                        label = { Text(uiState.filter.label) }
                    )
                }
            )
        }
    ) { padding ->
        when (val state = uiState) {
            is OrdersUiState.Loading -> LoadingIndicator(Modifier.padding(padding))
            is OrdersUiState.Error -> ErrorMessage(
                message = state.message,
                onRetry = viewModel::retry,
                modifier = Modifier.padding(padding)
            )
            is OrdersUiState.Success -> OrdersList(
                orders = state.orders,
                onOrderClick = viewModel::onOrderSelected,
                modifier = Modifier.padding(padding)
            )
        }
    }
}

@Composable
private fun OrdersList(
    orders: List<Order>,
    onOrderClick: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier,
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(orders, key = { it.id }) { order ->
            OrderCard(
                order = order,
                onClick = { onOrderClick(order.id) },
                modifier = Modifier.animateItem()
            )
        }
    }
}

Compose Best Practices

Architecture: MVVM + Clean Architecture

Google's official guidance has converged on a layered architecture: UI layer (Compose + ViewModel), Domain layer (optional, for complex business logic), and Data layer (repositories + data sources). This isn't new, but it's now well-documented and consistently applied across Google's samples and codelabs.

// ViewModel — UI state management
@HiltViewModel
class OrdersViewModel @Inject constructor(
    private val getOrdersUseCase: GetOrdersUseCase
) : ViewModel() {

    private val filter = MutableStateFlow(OrderFilter.ALL)

    val uiState: StateFlow<OrdersUiState> = filter
        .flatMapLatest { f -> getOrdersUseCase(f) }
        .map { orders -> OrdersUiState.Success(orders) as OrdersUiState }
        .catch { e -> emit(OrdersUiState.Error(e.message ?: "Unknown error")) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrdersUiState.Loading)

    fun toggleFilter() {
        filter.update { current ->
            when (current) {
                OrderFilter.ALL -> OrderFilter.PENDING
                OrderFilter.PENDING -> OrderFilter.COMPLETED
                OrderFilter.COMPLETED -> OrderFilter.ALL
            }
        }
    }
}

// Use case — business logic
class GetOrdersUseCase @Inject constructor(
    private val orderRepository: OrderRepository
) {
    operator fun invoke(filter: OrderFilter): Flow<List<Order>> {
        return orderRepository.observeOrders()
            .map { orders ->
                when (filter) {
                    OrderFilter.ALL -> orders
                    OrderFilter.PENDING -> orders.filter { it.status == Status.PENDING }
                    OrderFilter.COMPLETED -> orders.filter { it.status == Status.COMPLETED }
                }
            }
    }
}

// Repository — data access abstraction
class OrderRepositoryImpl @Inject constructor(
    private val api: OrderApi,
    private val dao: OrderDao
) : OrderRepository {

    override fun observeOrders(): Flow<List<Order>> = dao.observeAll()
        .onStart { refreshFromNetwork() }

    private suspend fun refreshFromNetwork() {
        try {
            val remote = api.getOrders()
            dao.upsertAll(remote.map { it.toEntity() })
        } catch (e: IOException) {
            // Offline — use cached data
        }
    }
}
When to skip the domain layer: If your ViewModel just calls repository.getItems() and maps it to UI state, the use case is boilerplate. Add the domain layer when you have real business logic that spans multiple repositories or needs to be reused across ViewModels. Don't add it "just in case."

Dependency Injection with Hilt and Koin

Two DI frameworks dominate in 2026:

Factor Hilt Koin
TypeCompile-time (Dagger under the hood)Runtime service locator
Setup complexityMore setup, annotationsSimpler, Kotlin DSL
Error detectionCompile-time errorsRuntime errors
KMP supportAndroid onlyFull multiplatform
Google recommendedYes (official)No (but widely used)
Build time impactSlower (code generation)Minimal

Our recommendation: Use Hilt for Android-only projects — compile-time safety catches errors before they reach production. Use Koin if you're doing Kotlin Multiplatform and need DI in shared code.

Kotlin Multiplatform for Shared Logic

Kotlin Multiplatform (KMP) is stable and production-ready in 2026. The key insight: you share business logic (networking, data models, validation, state management) while keeping native UI on each platform. This is fundamentally different from React Native or Flutter, which share the UI layer too.

// Shared KMP module — used by Android (Compose) and iOS (SwiftUI)

// commonMain/src
class OrderRepository(
    private val api: OrderApi,
    private val database: OrderDatabase
) {
    fun getOrders(): Flow<List<Order>> = database.observeOrders()
        .onStart { syncFromServer() }

    suspend fun syncFromServer() {
        val remote = api.fetchOrders()
        database.upsert(remote)
    }
}

// Shared ViewModel (using KMP-friendly approach)
class OrdersSharedViewModel(
    private val repository: OrderRepository
) {
    val orders: StateFlow<List<Order>> = repository.getOrders()
        .stateIn(CoroutineScope(Dispatchers.Default), SharingStarted.Lazily, emptyList())
}

// Android consumes it with Compose
@Composable
fun OrdersScreen(viewModel: OrdersSharedViewModel = koinViewModel()) {
    val orders by viewModel.orders.collectAsStateWithLifecycle()
    // ... Compose UI
}

// iOS consumes it with SwiftUI
// struct OrdersView: View {
//     @StateObject var viewModel = OrdersSharedViewModel()
//     var body: some View { List(viewModel.orders) { ... } }
// }

KMP works well when your team has strong Kotlin skills and wants native UI on both platforms. The shared module handles networking (Ktor), serialization (kotlinx.serialization), database (SQLDelight), and business logic — typically 40-60% of the total codebase.

Networking and Data Layer

The networking stack for Android in 2026 has clear winners:

Layer Tool Why
HTTP clientRetrofit + OkHttp (Android) / Ktor (KMP)Retrofit is mature; Ktor for multiplatform
Serializationkotlinx.serializationKotlin-native, multiplatform, no reflection
Local databaseRoom (Android) / SQLDelight (KMP)Room is simpler; SQLDelight for shared code
Key-value storageDataStore (Proto or Preferences)Type-safe, coroutine-based, replaces SharedPreferences
Image loadingCoil 3Kotlin-first, Compose-native, multiplatform
Caching / offlineRoom + offline-first repository patternSingle source of truth from local DB
// Offline-first repository pattern with Room
@Dao
interface OrderDao {
    @Query("SELECT * FROM orders ORDER BY created_at DESC")
    fun observeAll(): Flow<List<OrderEntity>>

    @Upsert
    suspend fun upsertAll(orders: List<OrderEntity>)
}

class OrderRepositoryImpl @Inject constructor(
    private val api: OrderApi,
    private val dao: OrderDao,
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : OrderRepository {

    // Single source of truth: always read from local DB
    override fun observeOrders(): Flow<List<Order>> =
        dao.observeAll().map { entities -> entities.map { it.toDomain() } }

    // Sync from network — update local DB, observers get notified
    override suspend fun sync(): Result<Unit> = withContext(dispatcher) {
        runCatching {
            val response = api.getOrders()
            dao.upsertAll(response.map { it.toEntity() })
        }
    }
}

Performance Optimization

Compose performance has improved significantly with the strong-skipping compiler mode (now default). But there are still patterns to know:

Avoiding Unnecessary Recompositions

// ❌ Bad — lambda recreated every recomposition
@Composable
fun ParentScreen(viewModel: ParentViewModel) {
    val items by viewModel.items.collectAsStateWithLifecycle()

    LazyColumn {
        items(items) { item ->
            // This lambda is recreated on every recomposition of ParentScreen
            ItemCard(item = item, onClick = { viewModel.onItemClick(item.id) })
        }
    }
}

// ✅ Better — stable lambda reference
@Composable
fun ParentScreen(viewModel: ParentViewModel) {
    val items by viewModel.items.collectAsStateWithLifecycle()
    val onItemClick = viewModel::onItemClick  // stable reference

    LazyColumn {
        items(items, key = { it.id }) { item ->
            ItemCard(item = item, onClick = { onItemClick(item.id) })
        }
    }
}

Key Performance Tips

Testing Strategy

A practical testing pyramid for Android in 2026:

// Unit test — ViewModel logic
class OrdersViewModelTest {
    private val fakeRepository = FakeOrderRepository()
    private lateinit var viewModel: OrdersViewModel

    @Before
    fun setup() {
        viewModel = OrdersViewModel(GetOrdersUseCase(fakeRepository))
    }

    @Test
    fun `initial state is loading`() = runTest {
        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(OrdersUiState.Loading::class.java)
    }

    @Test
    fun `orders loaded successfully`() = runTest {
        fakeRepository.emit(listOf(Order(id = "1", total = 99.0)))
        val state = viewModel.uiState.first { it is OrdersUiState.Success }
        assertThat((state as OrdersUiState.Success).orders).hasSize(1)
    }
}

// Compose UI test
class OrdersScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `displays order list when loaded`() {
        val orders = listOf(Order(id = "1", title = "Order #1001", total = 49.99))

        composeTestRule.setContent {
            OrdersList(orders = orders, onOrderClick = {})
        }

        composeTestRule.onNodeWithText("Order #1001").assertIsDisplayed()
        composeTestRule.onNodeWithText("$49.99").assertIsDisplayed()
    }
}

Focus your testing effort on: ViewModel logic (unit tests), repository/data layer (unit tests with fakes), and critical user flows (Compose UI tests). Don't aim for 100% coverage — aim for confidence in the code paths that handle money, user data, and core business logic.

The 2026 Android Toolchain

Category Tool Notes
IDEAndroid Studio Ladybug+Compose Preview, Gemini assistance, Layout Inspector
Build systemGradle (Kotlin DSL) + Version CatalogsCentralized dependency management
NavigationNavigation Compose (type-safe)Kotlin Serialization for route args
Background workWorkManagerReliable background tasks with constraints
Crash reportingFirebase CrashlyticsFree, real-time, integrates with Play Console
CI/CDGitHub Actions / BitriseBuild, test, deploy to Play Console
Feature flagsFirebase Remote ConfigToggle features without app updates

Frequently Asked Questions

Should I still learn XML layouts in 2026?

Only for maintaining existing apps. All new Android development should use Jetpack Compose. Google's official samples, documentation, and new APIs are Compose-first. XML layouts are legacy.

Is Java still used for Android development?

In legacy codebases, yes. For new projects, Kotlin is the only sensible choice. Google made Kotlin the preferred language in 2019, and Jetpack Compose only works with Kotlin. Java interop means you can migrate gradually.

How does Android native compare to Flutter?

Native gives you the best performance, latest APIs, and Google's full Jetpack ecosystem. Flutter offers cross-platform from one codebase. For Android-only apps, native Kotlin is better. For both platforms, evaluate cross-platform options.

What's the minimum Android version to target?

API 26 (Android 8.0) as a minimum covers 95%+ of active devices. For Compose, you need API 21 minimum, but targeting API 26+ lets you use modern APIs without excessive compat layers.

Should I use Kotlin Multiplatform or Flutter for cross-platform?

KMP shares business logic while keeping native UI — ideal when you want Compose on Android and SwiftUI on iOS. Flutter shares everything including UI — faster development but less platform-native feel. Choose based on your team's skills and UI requirements.

🤖

Pillai Infotech LLP

We build Android apps with Kotlin and Jetpack Compose — from startup MVPs to enterprise-grade applications. Let's build your Android app.

Related Articles

Kotlin Multiplatform: Share Code Across Android, iOS, and Web → iOS App Development with Swift: What's New in 2026 → Cross-Platform Mobile Frameworks Compared →