Native Android apps with Kotlin & Jetpack.
2 topics
Kotlin is the official language for Android development, endorsed by Google since 2017. Start with: val (immutable) vs var (mutable) variables, null safety (the ? operator, safe calls ?., Elvis operator ?:, and non-null assertions !!), data classes (automatic equals, hashCode, copy, toString), string templates, when expressions (powerful switch replacement), ranges and progressions, extension functions (adding methods to existing classes without inheritance), lambda expressions and higher-order functions, collections (List, Map, Set with functional operations like map, filter, groupBy), sealed classes (restricted class hierarchies), and object declarations (singletons). Kotlin's coroutines provide elegant asynchronous programming — they are essential for Android development. Kotlin is fully interoperable with Java, so you can use existing Java libraries seamlessly.
2 resources
Coroutines are Kotlin's solution for asynchronous programming — they let you write non-blocking code that looks sequential. Key concepts: suspend functions (can be paused and resumed), CoroutineScope (lifecycle-aware container for coroutines), Dispatchers (Main for UI, IO for network/disk, Default for CPU-intensive), launch (fire-and-forget), async/await (return a result), structured concurrency (child coroutines are canceled when parent is canceled), and exception handling with CoroutineExceptionHandler. Flow is Kotlin's reactive stream API — it emits values asynchronously over time. StateFlow (holds a current value, like LiveData) and SharedFlow (broadcasts events) are used extensively in modern Android architecture. In Android, use viewModelScope in ViewModels and lifecycleScope in Activities/Fragments to automatically cancel coroutines when the lifecycle ends.
Jetpack Compose is Android's modern declarative UI toolkit — it replaces XML layouts with Kotlin code. Instead of imperatively modifying views, you describe what the UI should look like for a given state, and Compose handles updates efficiently through recomposition. Key concepts: @Composable functions (the building blocks), state management (remember, mutableStateOf), state hoisting (lifting state to parent composables), Modifier (chaining layout, drawing, and interaction behaviors), Column/Row/Box (layout composables), LazyColumn/LazyRow (RecyclerView replacement for scrollable lists), Material 3 components (Button, Card, TextField, TopAppBar), theming (MaterialTheme with custom colors, typography, shapes), and navigation (NavHost, NavController). Compose offers live previews (@Preview), a simplified development cycle, and much less boilerplate than the old View system.
Beyond basics, master: side effects (LaunchedEffect for coroutines in composition, DisposableEffect for cleanup, SideEffect for non-suspend effects), CompositionLocal for providing values down the tree (like React Context), custom layouts (Layout composable for complex positioning), animations (animateContentSize, AnimatedVisibility, Crossfade, and Animatable for fine-grained control), gestures (pointerInput for drag, swipe, multi-touch), interoperability with Views (AndroidView for embedding legacy views, ComposeView for Compose in fragments), and performance optimization (derivedStateOf to avoid unnecessary recompositions, key for list stability, Modifier.drawBehind for custom drawing). Use the Layout Inspector and Compose compiler metrics to identify recomposition issues.
Google recommends a layered architecture: UI Layer (Compose UI + ViewModel), Domain Layer (optional — use cases for complex business logic), and Data Layer (repositories + data sources). The ViewModel holds UI state as StateFlow, survives configuration changes (screen rotation), and communicates with the data layer. The Repository pattern abstracts data sources — the ViewModel does not know if data comes from a network API, local database, or cache. Dependency injection with Hilt (built on Dagger) wires everything together — @HiltViewModel, @Inject, @Module, @Provides. This architecture enables: testability (mock repositories in ViewModel tests), separation of concerns, and predictable state management. Unidirectional data flow (UDF) means state flows down from ViewModel to UI, and events flow up from UI to ViewModel.
Most Android apps need network data and local persistence. Retrofit is the standard HTTP client — it uses annotations to define API endpoints, Kotlin serialization or Moshi for JSON parsing, and supports coroutines with suspend functions. OkHttp (Retrofit's underlying client) handles interceptors for logging, authentication headers, and caching. For local storage: Room is the SQLite abstraction — define entities (tables), DAOs (queries), and the database class, all with compile-time SQL verification and Flow support for reactive queries. DataStore replaces SharedPreferences for key-value storage (Preferences DataStore) and typed data (Proto DataStore). For offline-first apps, implement a caching strategy: load from cache immediately, fetch from network, update cache, and show the latest data. WorkManager handles guaranteed background work (syncing, uploads) that survives app restarts.
Android testing follows three layers. Unit tests (JUnit, MockK) run on the JVM without Android — test ViewModels, repositories, and business logic by mocking dependencies. Integration tests verify components work together — test Room databases with in-memory databases, test Retrofit with MockWebServer. UI tests (Compose testing with createComposeRule, or Espresso for View-based UI) verify the user interface by interacting with composables and asserting on the resulting state. End-to-end tests with UI Automator test across app boundaries. Write tests using the AAA pattern (Arrange, Act, Assert). Use Turbine to test Kotlin Flows in ViewModels. Aim for high unit test coverage on business logic, integration tests for data layer, and UI tests for critical user journeys. Run tests in CI with GitHub Actions or Firebase Test Lab for real device testing.
Launching on the Google Play Store involves several steps. App signing: generate an upload key, enroll in Play App Signing (Google manages your signing key for security). Build variants: use build types (debug, release) and product flavors (free, pro) for different configurations. ProGuard/R8 minifies and obfuscates your release build. Create a compelling store listing: app icon, screenshots, feature graphic, description, and privacy policy. Testing tracks: internal testing (fast review), closed testing (beta users), and open testing (public beta) before production release. Implement in-app updates (flexible or immediate) and in-app review prompts. Monitor crashes with Firebase Crashlytics, track analytics with Firebase Analytics, and respond to user reviews. Use Android App Bundle (.aab) instead of APK for smaller downloads via Dynamic Delivery.