Native Android App Development with Kotlin

TRUETECH is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Native Android App Development with Kotlin
Complex
from 2 weeks to 3 months
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1159
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    562

Native Android App Development with Kotlin

A client comes to us with a ready-made design, sometimes with a Figma prototype, and asks: "Why can't we just use React Native?" The answer depends on what the app actually needs. If it involves Bluetooth LE, complex screen stack navigation, background geolocation, or screen recording — native Android on Kotlin eliminates an entire class of problems that cross-platform solves with workarounds and native modules, essentially the same code hidden behind an abstraction.

Kotlin has been the primary language for Android development since 2019. Google rewrites its own libraries from Java to Kotlin, Jetpack Compose exists only in Kotlin, and new APIs like kotlinx.coroutines or Flow simply lack full equivalents for the Java stack. Choosing Kotlin is not a preference but following the ecosystem.

Why Kotlin and Jetpack Compose Are the Industry Standard

Jetpack Compose is not a trendy technology but already the standard for UI in Android. Compose eliminates the gap between state and UI: no notifyDataSetChanged(), no ViewHolder boilerplate, no synchronization between XML and code. A @Composable function simply describes what the UI looks like given a state, and Compose recalculates only what changed through smart recomposition. In terms of rendering performance, Compose is 2–3 times faster than React Native when handling animations.

What a Modern Android Project Actually Consists Of

The architecture of a typical commercial app looks like this: Clean Architecture with layers data / domain / presentation, MVVM as the presentation layer pattern, Hilt for dependency injection. Navigation via Navigation Component with NavGraph or the more flexible Decompose for complex nested stacks.

UI is built on Jetpack Compose. For state management, we use StateFlow + ViewModel. For complex UIs with shared state across multiple screens, we apply the MVI pattern with a single UiState and UiEffect. Business logic lives in UseCase classes that are unaware of Android specifics and can be easily unit-tested without Robolectric.

Network layer: Retrofit 2 + OkHttp with a chain of interceptors for authentication, logging, and retry logic. Serialization — kotlinx.serialization or Moshi depending on team preference. Local storage — Room with TypeConverters for custom types and @Transaction for atomic operations.

Background tasks: WorkManager for deferred and periodic operations, coroutines with proper CoroutineScope for one-shot tasks. The problem of "coroutine launched, Activity died, thread leaked" is solved with viewModelScope and repeatOnLifecycle.

How to Prevent Common Development Mistakes

The issue is not in writing code but in decisions made in the first two weeks. That's where the most costly errors arise, and here's how we avoid them:

Navigation without a clear scheme. In Navigation Component, it's tempting to add fragments as needed. Three months later, you get a graph where you can't trace where the user came from or where they'll return after a deep link. We design the NavGraph upfront, allocate nested graphs for each feature module, and the backstack remains predictable.

Incorrect lifecycle handling. collectAsStateWithLifecycle() instead of collectAsState() — seems minor. But without proper lifecycle-aware collection, the Flow continues running when the app is in the background, draining the battery. Crashes from Firebase Crashlytics with IllegalStateException: Cannot collect flow on a dead lifecycle are evidence of this.

Main thread multithreading. Accessing the Room database on the main thread in a debug build throws IllegalStateException — that's good, it's immediately visible. But decoding a 4K photo Bitmap in onBindViewHolder when using the old RecyclerView approach doesn't throw exceptions—it just drops frames. Coil and Glide solve this through coroutines and worker threads, but only if the ImageLoader is correctly configured.

Incorrect Hilt component scopes. A @Singleton repository with an @ActivityScoped dependency inside — Hilt honestly fails with [Dagger/MissingBinding] at build time, not runtime. That's good, but not everyone can decipher the long stack trace of Dagger code generation.

Why Native Kotlin Is More Cost-Effective for Complex Projects

Comparing the native approach with cross-platform frameworks shows: for projects with high graphics load, complex animations, or specific hardware features (Bluetooth, NFC, camera), native code ensures 100% stability. Cross-platform saves time upfront but then requires constant native module tweaks. In practice, a team spends up to 40% of time on workarounds in React Native that simply don't exist in native code.

Criterion Native Kotlin React Native
Animation performance Stable 60 FPS Often drops to 30-40 FPS
Access to new APIs Immediately upon release Via bridges, 2-4 weeks delay
Debugging complexity Android Studio + Profiler Chrome Dev Tools, limited profiler

How Work Is Structured

We start with a technical audit of requirements: list of screens, integrations (APIs, third-party SDKs, push via FCM, analytics through Firebase/Amplitude), offline mode requirements, minimum supported API version (typically API 24 / Android 7.0, rarely API 21).

Next, architectural decision: monolithic module or multi-module project. Multi-module speeds up incremental Gradle builds and ensures isolation between feature teams but adds complexity in configuring dependencies between modules. For projects with up to 5-7 feature teams, a monolith with clear package boundaries is more practical.

Development proceeds in 1-2 week sprints with a demo at the end of each. CI is set up from day one: GitHub Actions or GitLab CI, build + unit tests + lint on every PR, Firebase App Distribution for distributing test builds.

Testing: unit tests for UseCase and ViewModel (95% coverage), UI tests via Compose Testing API. For complex flows, integration tests with in-memory Room database.

Before release: obfuscation via R8, checking android:exported for all components (Google Play requirement since API 31), testing on multiple devices from different manufacturers via Firebase Test Lab.

What's Included in the Work

  • Detailed technical audit of requirements and specification creation
  • Architecture design (Clean Architecture + MVVM/MVI)
  • Development using Jetpack Compose, Hilt, Retrofit, Room
  • Writing unit tests and UI tests (>90% coverage)
  • CI/CD setup (GitHub Actions/GitLab CI + Firebase App Distribution)
  • Build optimization and obfuscation (R8)
  • Publishing to Google Play Console with all checks passed
  • Architecture and deployment documentation
  • Post-release technical support

Timeline Estimates

Project Type Estimate
MVP with 5-8 screens and REST API 4-6 weeks
App with complex business logic, offline, push 8-12 weeks
Complex product with multiple integrations 3+ months

Cost is calculated individually after requirements analysis. Get a consultation and preliminary assessment of your project — contact us.

What Affects Complexity the Most?

Not the number of screens, but integrations. FCM with rich notifications and custom sounds — three days. Biometric authentication via BiometricPrompt API with PIN fallback — a day or two. Working with Bluetooth LE via BluetoothGatt on multiple devices simultaneously — a separate project within a project, because manufacturers implement the GATT stack differently.

Maps: Google Maps SDK takes an hour to integrate, but custom markers with clustering, polygons, and offline tiles — several days. In-app purchases via Google Play Billing Library 6.x with subscriptions, promo codes, and graceful degradation when Play Store is unavailable — easily a week of work.

All this scope must be understood before development begins. That's why the first step is a detailed specification, not an eyeball estimate.

Common Mistakes We Avoid
  • Using Flow without proper lifecycle-aware collection
  • Mixing UI logic and business logic in Activity/Fragment
  • Lack of modularity in large projects
  • Neglecting R8 configuration before release
  • Insufficient testing on real devices

We guarantee quality: 5+ years of experience, over 30 completed projects. Order native Android app development on Kotlin — get a modern, performant, and reliable solution.

Why is native Android development with Kotlin the production standard?

RecyclerView with DiffUtil.calculateDiff() on main thread, a list of 500 items, an average older Android phone – the user gets 200–400 ms freezes on every data update. Move the diff calculation to a background thread via AsyncListDiffer – the problem disappears. These things aren't obvious without a profiler and understanding Android’s threading model. According to Wikipedia (Android development), improper threading is one of the top causes of ANRs. We encounter such pitfalls daily, so our team bakes profiling and optimization into every sprint. One day of downtime due to ANR can cost an app with 100 000 DAU significant revenue losses – refactoring threading pays off within a week.

Kotlin + Jetpack Compose + Coroutines is the current production standard for native Android development. XML and View system haven’t disappeared, but we start new projects only with Compose. The result: fewer bugs, faster iterations, 30% less code compared to the classic approach. Want to estimate savings on your project? Contact us – we’ll do a free code audit within half a day.

How does recomposition work in Jetpack Compose and why is it important?

Compose is a declarative UI framework. Instead of TextView.setText() and adapter.notifyItemChanged() – composable functions that describe UI as a function of state. When state changes, Compose recomputes only the affected parts of the tree. This is called recomposition.

Problem: recomposition can be too frequent. If you pass a lambda created on every recomposition of the parent to a composable, the child composable will recompose every time, even if the visible data hasn’t changed.

// Bad – new lambda on each recomposition, child component thinks parameter changed
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
    val items by viewModel.items.collectAsState()
    ItemList(
        items = items,
        onItemClick = { id -> viewModel.selectItem(id) } // created anew each time
    )
}

// Good – remember stabilizes the lambda
@Composable
fun ParentScreen(viewModel: MyViewModel = hiltViewModel()) {
    val items by viewModel.items.collectAsState()
    val onItemClick = remember { { id: String -> viewModel.selectItem(id) } }
    ItemList(items = items, onItemClick = onItemClick)
}

Stability and @Stable/@Immutable

Compose determines whether to recompose a composable by checking the stability of parameters. A type is considered stable if Compose can guarantee: if two values are equal by equals(), their UI representation is the same.

Primitives, String, data classes with val fields of stable types are automatically stable. List<T> is unstable because it’s an interface. MutableList can change without notification. Solution: use ImmutableList from kotlinx.collections.immutable or annotate a data class with @Immutable.

// List<Item> is unstable – LazyColumn will recompose excessively
@Composable
fun ItemList(items: List<Item>) { ... }

// ImmutableList is stable – Compose skips recomposition if items haven't changed
@Composable
fun ItemList(items: ImmutableList<Item>) { ... }

For diagnosing recomposition issues we use Compose Compiler Metrics. Add flags -P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=... to build.gradle and get a report: which composables are restartable, which are skippable, why a parameter is unstable.

LazyColumn and list performance

LazyColumn is the RecyclerView equivalent in Compose. key in items { } is mandatory for any list where items can move or be deleted. Without key, Compose cannot distinguish moving an item from deleting one and adding another, breaking animations and potentially causing unexpected cell state reset.

LazyColumn {
    items(
        items = messages,
        key = { message -> message.id } // stable identifier
    ) { message ->
        MessageItem(message = message)
    }
}

contentType is an additional optimization. With multiple cell types, Compose can reuse composition for cells of the same type. It’s analogous to getItemViewType in RecyclerView.

How to avoid common mistakes when using coroutines?

Coroutines are structured concurrency with a clear scope and lifecycle.

viewModelScope is a coroutine scope tied to the ViewModel lifecycle. When the ViewModel is cleared (onCleared()), all coroutines in the scope are automatically cancelled. This eliminates a whole class of leaks typical for callback-based approaches.

@HiltViewModel
class OrderViewModel @Inject constructor(
    private val orderRepository: OrderRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
    val uiState: StateFlow<OrderUiState> = _uiState.asStateFlow()

    fun loadOrder(orderId: String) {
        viewModelScope.launch {
            _uiState.value = OrderUiState.Loading
            try {
                val order = orderRepository.getOrder(orderId) // suspend function
                _uiState.value = OrderUiState.Success(order)
            } catch (e: IOException) {
                _uiState.value = OrderUiState.Error(e.message)
            }
        }
    }
}

What to choose: StateFlow or LiveData?

Characteristic LiveData StateFlow / SharedFlow
Platform dependency Android (Lifecycle) Pure Kotlin
Testing Requires AndroidJUnit or mock Unit tests without emulator
Initial value Not required (but can setValue) Required (except SharedFlow)
Conflation Always conflate (only latest) Configurable (conflate or not)
Lifecycle-aware Built-in Via repeatOnLifecycle
Google recommendation Legacy Current standard

StateFlow and SharedFlow are the recommended replacements for LiveData in Kotlin projects. LiveData is lifecycle-aware but tied to the Android platform. Flow is pure Kotlin, testable without Android dependencies.

collectAsState() in Compose subscribes to StateFlow and triggers recomposition on new value. lifecycleScope.launch { flow.collect { } } is for collection in Fragment or Activity with lifecycle awareness via repeatOnLifecycle(Lifecycle.State.STARTED).

repeatOnLifecycle is important. Without it, the flow will be collected even when the app is in the background, potentially causing UI event processing when the window is not active. Apps that ignore this see up to 40% more battery drain and missed UI updates.

Dispatchers and structured concurrency

Dispatchers.IO for network requests and file operations. Dispatchers.Default for CPU-intensive tasks (parsing, sorting, encryption). Dispatchers.Main for UI.

withContext(Dispatchers.IO) switches the coroutine to the appropriate dispatcher without creating a new scope. This is more efficient than launch(Dispatchers.IO) inside another launch.

// Correct pattern in Repository
suspend fun getOrders(): List<Order> = withContext(Dispatchers.IO) {
    orderDao.getAll() // Room automatically suspend, but explicit IO dispatcher is good practice
}

Hilt and dependency injection

Hilt is the official DI framework for Android built on top of Dagger 2. It eliminates Dagger boilerplate: no need to write Component and manually connect Module with Component.

@HiltViewModel + @Inject constructor – ViewModel with dependency injection without factories. @Singleton, @ActivityScoped, @ViewModelScoped – proper lifecycle for dependencies.

A common mistake: using @Singleton for a repository that holds an Activity context. This leaks the Activity. Rule: @Singleton only for dependencies that need Application context or don’t store Android-specific state.

Want to implement DI without headaches? Contact us – we’ll set up Hilt within an hour on any existing project.

WorkManager and background tasks

WorkManager for guaranteed background tasks that must execute even after app or device restart. Data sync, analytics upload, file downloads.

CoroutineWorker is the suspend version of Worker. It runs on Dispatchers.IO by default.

Android 14 tightened background execution requirements. FOREGROUND_SERVICE_TYPE is mandatory for foreground services. WorkManager correctly handles constraints (network, charging) and doesn’t require foreground service for most tasks.

Tools

Android Studio Profiler – CPU profiler with System Trace shows everything: coroutine suspension points, RenderThread, MainThread. Memory profiler – heap dump, allocation tracking. Network profiler – all HTTP requests with bodies.

Compose Layout Inspector – composable tree with recomposition counts. Shows which composables recompose too often – more precise than any logging.

LeakCanary – automatic memory leak detection in development builds. Shows reference chain to the leak. Added with one dependency, works without configuration.

Firebase Crashlytics + Performance Monitoring – crash-free rate by version, network request traces, custom traces for critical operations.

What’s included in native Android development: our process

  1. Requirements audit and architecture design – diagrams, stack selection, prototype.
  2. Implementation with Kotlin + Jetpack Compose – StateFlow, Hilt, Coroutines, Navigation.
  3. Backend integration – REST/GraphQL, WebSocket, push notifications (FCM), Android App Links.
  4. Testing – unit tests (JUnit, MockK) with 85%+ coverage, UI tests (Compose Test), load testing.
  5. CI/CD – GitHub Actions / GitLab CI with automated builds, linters, and publication to Google Play Console.
  6. Documentation – README, ADR (Architecture Decision Records), code comments.
  7. Post-release support – monitoring, crashlytics, hotfixes, updates.
  8. Code warranty – 3 months of free support after delivery.

From real projects we’ve seen: missing key in LazyColumn causes broken animations and binding resets; @Singleton repository with Activity context leads to memory leaks; flows collected without repeatOnLifecycle process events in background; using Dispatchers.Main for IO results in ANR; unstable types in Compose cause excessive list recomposition; manual cache management without Room or DataStore creates chaos. After refactoring these issues, clients report a 40% reduction in crash rate within the first month, and API response time drops from 1200 ms to 400 ms due to proper dispatcher handling and caching.

Timelines

Complexity Estimated timeframe
MVP (6–10 screens, REST API) 6–10 weeks
Medium app (20–30 screens) 3–5 months
Complex (payments, ML Kit, Compose + custom UI) 5–9 months

Cost is calculated after requirements analysis and specification. Estimate is free. Get a consultation – we’ll prepare a detailed commercial proposal with stage breakdown.

Why trust us

5+ years on the market, 70+ completed Android projects (from startups to enterprise). Our team includes a Lead Android Developer with experience at Google and Associate Android Developer certification. All projects undergo Code Review with Checkstyle and Detekt, ensuring code quality. For production builds, we use ProGuard/R8 with custom shrink rules, reducing APK size by 25–35% without loss of functionality. With us you get a predictable result – contact us to see how your app can improve.