Files
english/.opencode/skills/mobile-development/references/mobile-android.md
2026-04-12 01:06:31 +07:00

14 KiB

Android Native Development

Complete guide to Android development with Kotlin and Jetpack Compose (2024-2025).

Kotlin 2.1 Overview

Key Features

  • Null safety: No more NullPointerExceptions
  • Coroutines: Structured concurrency
  • Extension functions: Extend classes without inheritance
  • Sealed classes: Type-safe state management
  • Data classes: Automatic equals/hashCode/toString

Modern Kotlin Patterns

Coroutines:

// Suspend function
suspend fun fetchUser(id: String): User {
    return withContext(Dispatchers.IO) {
        api.getUser(id)
    }
}

// Usage in ViewModel
viewModelScope.launch {
    try {
        val user = fetchUser("123")
        _uiState.update { it.copy(user = user) }
    } catch (e: Exception) {
        _uiState.update { it.copy(error = e.message) }
    }
}

Flow (Reactive streams):

class UserRepository {
    fun observeUsers(): Flow<List<User>> = flow {
        while (true) {
            emit(database.getUsers())
            delay(5000)  // Poll every 5 seconds
        }
    }.flowOn(Dispatchers.IO)
}

// Collect in ViewModel
init {
    viewModelScope.launch {
        repository.observeUsers().collect { users ->
            _uiState.update { it.copy(users = users) }
        }
    }
}

Sealed classes (Type-safe states):

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<User>) : UiState()
    data class Error(val message: String) : UiState()
}

// Pattern matching
when (uiState) {
    is UiState.Loading -> ShowLoader()
    is UiState.Success -> ShowData(uiState.data)
    is UiState.Error -> ShowError(uiState.message)
}

Jetpack Compose

Why Compose?

  • Declarative: Describe UI state, not imperative commands
  • 60% adoption: In top 1,000 apps (2024)
  • Less code: 40% reduction vs Views
  • Modern: Built for Kotlin and coroutines
  • Material 3: First-class support

Compose Basics

@Composable
fun UserListScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    Column(modifier = Modifier.fillMaxSize()) {
        when (val state = uiState) {
            is UiState.Loading -> {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }
            is UiState.Success -> {
                LazyColumn {
                    items(state.data) { user ->
                        UserItem(user)
                    }
                }
            }
            is UiState.Error -> {
                Text(
                    text = state.message,
                    color = MaterialTheme.colorScheme.error
                )
            }
        }
    }
}

@Composable
fun UserItem(user: User) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(
            text = user.name,
            style = MaterialTheme.typography.bodyLarge
        )
    }
}

Key Composables:

  • Column/Row/Box: Layout
  • LazyColumn/LazyRow: Recycler equivalent (virtualized)
  • Text/Image/Icon: Content
  • Button/TextField: Input
  • Card/Surface: Containers

Architecture Patterns

MVVM with Clean Architecture

// Domain Layer - Use Case
class GetUsersUseCase @Inject constructor(
    private val repository: UserRepository
) {
    operator fun invoke(): Flow<Result<List<User>>> =
        repository.getUsers()
}

// Data Layer - Repository
interface UserRepository {
    fun getUsers(): Flow<Result<List<User>>>
}

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository {
    override fun getUsers(): Flow<Result<List<User>>> = flow {
        // Local cache first
        val cachedUsers = dao.getUsers()
        emit(Result.success(cachedUsers))

        // Then fetch from network
        try {
            val networkUsers = api.getUsers()
            dao.insertUsers(networkUsers)
            emit(Result.success(networkUsers))
        } catch (e: Exception) {
            emit(Result.failure(e))
        }
    }.flowOn(Dispatchers.IO)
}

// Presentation Layer - ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {

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

    init {
        loadUsers()
    }

    private fun loadUsers() {
        viewModelScope.launch {
            getUsersUseCase().collect { result ->
                result.onSuccess { users ->
                    _uiState.update { it.copy(users = users, isLoading = false) }
                }.onFailure { error ->
                    _uiState.update { it.copy(error = error.message, isLoading = false) }
                }
            }
        }
    }
}

// UI State
data class UserUiState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = true,
    val error: String? = null
)

MVI (Model-View-Intent)

When to use:

  • Unidirectional data flow needed
  • Complex state management
  • Time-travel debugging
  • Predictable state updates
// State
data class UserScreenState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// Events (User intentions)
sealed class UserEvent {
    object LoadUsers : UserEvent()
    data class DeleteUser(val id: String) : UserEvent()
    object RetryLoad : UserEvent()
}

// ViewModel
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow(UserScreenState())
    val state: StateFlow<UserScreenState> = _state.asStateFlow()

    fun onEvent(event: UserEvent) {
        when (event) {
            is UserEvent.LoadUsers -> loadUsers()
            is UserEvent.DeleteUser -> deleteUser(event.id)
            is UserEvent.RetryLoad -> loadUsers()
        }
    }
}

Dependency Injection

Setup:

// App class
@HiltAndroidApp
class MyApplication : Application()

// Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity()

// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
    private val analytics: Analytics
) : ViewModel()

// Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}

Koin (Lightweight Alternative)

Setup:

// Module definition
val appModule = module {
    single { UserRepository(get()) }
    viewModel { UserViewModel(get()) }
}

// Application
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
            modules(appModule)
        }
    }
}

// Usage
class UserViewModel(
    private val repository: UserRepository
) : ViewModel()

Hilt vs Koin:

  • Hilt: Compile-time, type-safe, Google-backed, complex setup
  • Koin: Runtime, simple DSL, 50% faster setup, reflection-based

Performance Optimization

R8 Optimization

Automatic optimizations:

  • Code shrinking (remove unused)
  • Obfuscation (rename classes/methods)
  • Optimization (method inlining)
// build.gradle
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
        }
    }
}

Impact:

  • 10-20% app size reduction
  • 20% faster startup
  • Harder to reverse engineer

Baseline Profiles

Performance boost:

  • 10-20% faster startup
  • Reduced jank in critical paths
  • AOT compilation of hot code
// build.gradle
dependencies {
    implementation "androidx.profileinstaller:profileinstaller:1.3.1"
}

Compose Performance

1. Stability annotations:

// Mark stable classes
@Stable
data class User(val name: String, val age: Int)

// Immutable collections
@Immutable
data class UserList(val users: List<User>)

2. Avoid recomposition:

// ❌ Bad: Recomposes every render
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users) { user ->
            Text(user.name)  // Recreated every time
        }
    }
}

// ✅ Good: Use keys
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users, key = { it.id }) { user ->
            Text(user.name)
        }
    }
}

3. Remember expensive computations:

@Composable
fun ExpensiveList(items: List<Item>) {
    val sortedItems = remember(items) {
        items.sortedBy { it.priority }
    }

    LazyColumn {
        items(sortedItems) { item ->
            ItemCard(item)
        }
    }
}

Testing

Unit Testing (JUnit + MockK)

class UserViewModelTest {
    private lateinit var viewModel: UserViewModel
    private val mockRepository = mockk<UserRepository>()

    @Before
    fun setup() {
        viewModel = UserViewModel(mockRepository)
    }

    @Test
    fun `loadUsers should update state with users`() = runTest {
        // Given
        val users = listOf(User("1", "Test", "test@example.com"))
        coEvery { mockRepository.getUsers() } returns flowOf(Result.success(users))

        // When
        viewModel.loadUsers()

        // Then
        val state = viewModel.uiState.value
        assertEquals(users, state.users)
        assertFalse(state.isLoading)
    }
}

Compose Testing

class UserListScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun displayUsers() {
        val users = listOf(User("1", "John", "john@example.com"))

        composeTestRule.setContent {
            UserListScreen(
                users = users,
                onUserClick = {}
            )
        }

        composeTestRule.onNodeWithText("John").assertIsDisplayed()
    }
}

Instrumented Testing (Espresso)

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun loginFlow() {
        onView(withId(R.id.emailField))
            .perform(typeText("test@example.com"))

        onView(withId(R.id.passwordField))
            .perform(typeText("password123"))

        onView(withId(R.id.loginButton))
            .perform(click())

        onView(withText("Welcome"))
            .check(matches(isDisplayed()))
    }
}

Material Design 3

Theme Setup

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Material Components

// Cards
Card(
    modifier = Modifier.fillMaxWidth(),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
    Text("Content")
}

// FAB
FloatingActionButton(onClick = { /* Do something */ }) {
    Icon(Icons.Default.Add, contentDescription = "Add")
}

// Navigation
NavigationBar {
    items.forEach { item ->
        NavigationBarItem(
            icon = { Icon(item.icon, contentDescription = null) },
            label = { Text(item.label) },
            selected = selectedItem == item,
            onClick = { selectedItem = item }
        )
    }
}

Google Play Requirements (2024-2025)

SDK Requirements

  • Current: Target Android 14 (API 34)
  • Mandatory (Aug 31, 2025): Target Android 15 (API 35)

Privacy & Security

  • Privacy policy: Required for apps collecting data
  • Data safety: Form in Play Console
  • Permissions: Request only needed, justify dangerous permissions
  • Encryption: HTTPS for network, KeyStore for sensitive data

AAB (Android App Bundle)

android {
    bundle {
        density {
            enableSplit true
        }
        abi {
            enableSplit true
        }
        language {
            enableSplit true
        }
    }
}

Benefits:

  • 15-30% smaller downloads
  • Dynamic feature modules
  • Instant apps support

Common Pitfalls

  1. Main thread blocking: Use coroutines with Dispatchers.IO
  2. Memory leaks: Unregister listeners, cancel coroutines
  3. Configuration changes: Use ViewModel, avoid Activity references
  4. Large images: Use Coil/Glide for caching and resizing
  5. Forgetting permissions: Runtime permission requests
  6. Ignoring Android versions: Test on multiple API levels
  7. Not handling back press: OnBackPressedDispatcher
  8. Hardcoded strings: Use strings.xml for localization
  9. Not using Proguard/R8: Enable in release builds
  10. Ignoring battery: Use WorkManager for background tasks

Resources

Official:

Community: