mcpField
A
android-development

android-development

github.com/travisjneuman开发107171 次安装今天更新
代码仓库

关于 android-development

面向 Android 开发者的现代应用开发指南,涵盖从界面与架构到数据、测试和发布的实践,并涉及 Android 多种设备形态及 Kotlin Multiplatform。

主要功能

  1. 01Jetpack Compose 界面与状态管理
  2. 02现代 Android 架构与依赖注入
  3. 03Room、Retrofit 与仓储层
  4. 04Material Design 3 与平台适配
  5. 05测试、Play Store 发布及性能配置
  6. 06Kotlin Multiplatform 与 Compose Multiplatform

适用场景

  1. 01规划 Android 应用的分层架构
  2. 02实现 Compose 界面和导航
  3. 03构建本地及远程数据层
  4. 04准备测试与 Play Store 发布

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/travisjneuman/.claude --skill android-development

安装后在 Claude Code 中输入/android-development确认已加载。

详细介绍

Android 开发

构建现代 Android 应用的综合指南。

涵盖的平台

平台最低 SDK目标 SDK
Android 手机API 24(7.0)API 34(14)
Android 平板电脑API 24API 34
Android TVAPI 24API 34
Wear OSAPI 30API 34
Android AutoAPI 29API 34

Jetpack Compose(现代 UI)

基本结构
@Composable
fun MyApp() {
    MaterialTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            MainScreen()
        }
    }
}

@Composable
fun MainScreen() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Count: $count",
            style = MaterialTheme.typography.headlineLarge
        )

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}
状态管理
// Local state
var text by remember { mutableStateOf("") }

// State hoisting
@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    StatelessCounter(count = count, onIncrement = { count++ })
}

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

// ViewModel state
@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: ItemRepository
) : ViewModel() {

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

    fun loadItems() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val items = repository.getItems()
                _uiState.update { it.copy(items = items, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }
}

// Collecting in Compose
@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> LoadingIndicator()
        uiState.error != null -> ErrorMessage(uiState.error!!)
        else -> ItemList(uiState.items)
    }
}
导航
// Navigation setup
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { id ->
                    navController.navigate("detail/$id")
                }
            )
        }
        composable(
            route = "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.StringType })
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(itemId = itemId)
        }
    }
}

// Type-safe navigation (recommended)
@Serializable
data class DetailRoute(val itemId: String)

navController.navigate(DetailRoute(itemId = "123"))
列表
@Composable
fun ItemList(items: List<Item>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = items,
            key = { it.id }
        ) { item ->
            ItemCard(item = item)
        }
    }
}

// Pull to refresh
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RefreshableList(
    items: List<Item>,
    isRefreshing: Boolean,
    onRefresh: () -> Unit
) {
    val pullRefreshState = rememberPullToRefreshState()

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = onRefresh,
        state = pullRefreshState
    ) {
        LazyColumn { /* content */ }
    }
}

现代 Android 架构

清洁架构分层
app/
├── data/
│   ├── local/
│   │   ├── AppDatabase.kt
│   │   └── ItemDao.kt
│   ├── remote/
│   │   ├── ApiService.kt
│   │   └── ItemDto.kt
│   └── repository/
│       └── ItemRepositoryImpl.kt
├── domain/
│   ├── model/
│   │   └── Item.kt
│   ├── repository/
│   │   └── ItemRepository.kt
│   └── usecase/
│       └── GetItemsUseCase.kt
├── presentation/
│   ├── home/
│   │   ├── HomeScreen.kt
│   │   └── HomeViewModel.kt
│   └── navigation/
│       └── AppNavigation.kt
└── di/
    └── AppModule.kt
依赖注入(Hilt)
@HiltAndroidApp
class MyApplication : Application()

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app_database"
        ).build()
    }

    @Provides
    @Singleton
    fun provideApiService(): ApiService {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }

    @Provides
    @Singleton
    fun provideItemRepository(
        apiService: ApiService,
        database: AppDatabase
    ): ItemRepository {
        return ItemRepositoryImpl(apiService, database.itemDao())
    }
}

数据层

Room 数据库
@Entity(tableName = "items")
data class ItemEntity(
    @PrimaryKey val id: String,
    val name: String,
    val createdAt: Long
)

@Dao
interface ItemDao {
    @Query("SELECT * FROM items ORDER BY createdAt DESC")
    fun getItems(): Flow<List<ItemEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertItems(items: List<ItemEntity>)

    @Delete
    suspend fun deleteItem(item: ItemEntity)
}

@Database(entities = [ItemEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao
}
Retrofit API
interface ApiService {
    @GET("items")
    suspend fun getItems(): List<ItemDto>

    @POST("items")
    suspend fun createItem(@Body item: CreateItemRequest): ItemDto

    @DELETE("items/{id}")
    suspend fun deleteItem(@Path("id") id: String)
}

data class ItemDto(
    val id: String,
    val name: String,
    @SerializedName("created_at") val createdAt: String
)
仓库模式
interface ItemRepository {
    fun getItems(): Flow<List<Item>>
    suspend fun refreshItems()
    suspend fun deleteItem(id: String)
}

class ItemRepositoryImpl @Inject constructor(
    private val apiService: ApiService,
    private val itemDao: ItemDao
) : ItemRepository {

    override fun getItems(): Flow<List<Item>> {
        return itemDao.getItems().map { entities ->
            entities.map { it.toDomain() }
        }
    }

    override suspend fun refreshItems() {
        val items = apiService.getItems()
        itemDao.insertItems(items.map { it.toEntity() })
    }
}

Material Design 3

主题设置
@Composable
fun MyAppTheme(
    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
    )
}
常用组件
// Top App Bar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyTopBar(
    title: String,
    onBackClick: () -> Unit
) {
    TopAppBar(
        title = { Text(title) },
        navigationIcon = {
            IconButton(onClick = onBackClick) {
                Icon(Icons.Default.ArrowBack, contentDescription = "Back")
            }
        },
        actions = {
            IconButton(onClick = { /* menu */ }) {
                Icon(Icons.Default.MoreVert, contentDescription = "Menu")
            }
        }
    )
}

// Bottom Navigation
@Composable
fun MyBottomBar(
    selectedTab: Int,
    onTabSelected: (Int) -> Unit
) {
    NavigationBar {
        NavigationBarItem(
            icon = { Icon(Icons.Default.Home, contentDescription = null) },
            label = { Text("Home") },
            selected = selectedTab == 0,
            onClick = { onTabSelected(0) }
        )
        NavigationBarItem(
            icon = { Icon(Icons.Default.Settings, contentDescription = null) },
            label = { Text("Settings") },
            selected = selectedTab == 1,
            onClick = { onTabSelected(1) }
        )
    }
}

平台专属内容

Android TV
// Focus management
@Composable
fun TvButton(
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {
    var isFocused by remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .onFocusChanged { isFocused = it.isFocused }
            .focusable()
            .clickable(onClick = onClick)
            .background(
                if (isFocused) MaterialTheme.colorScheme.primary
                else MaterialTheme.colorScheme.surface
            )
    ) {
        content()
    }
}
Wear OS
@Composable
fun WearApp() {
    MaterialTheme {
        ScalingLazyColumn(
            modifier = Modifier.fillMaxSize(),
            anchorType = ScalingLazyListAnchorType.ItemCenter
        ) {
            item { TimeText() }
            item {
                Chip(
                    onClick = { },
                    label = { Text("Action") }
                )
            }
        }
    }
}

测试

单元测试
@Test
fun `getItems returns mapped domain objects`() = runTest {
    val repository = ItemRepositoryImpl(
        apiService = FakeApiService(),
        itemDao = FakeItemDao()
    )

    val items = repository.getItems().first()

    assertEquals(2, items.size)
    assertEquals("Item 1", items[0].name)
}
Compose UI 测试
@HiltAndroidTest
class MainScreenTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun displayItems_whenLoaded() {
        composeTestRule.onNodeWithText("Item 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Item 2").assertIsDisplayed()
    }

    @Test
    fun navigateToDetail_onItemClick() {
        composeTestRule.onNodeWithText("Item 1").performClick()
        composeTestRule.onNodeWithText("Item Details").assertIsDisplayed()
    }
}

Play Store 要求

必需项
  • 隐私政策
  • 应用图标(512x512)
  • 功能图片(1024x500)
  • 截图(至少 2 张)
  • 目标 API 34+
App Bundle
android {
    bundle {
        language {
            enableSplit = true
        }
        density {
            enableSplit = true
        }
        abi {
            enableSplit = true
        }
    }
}

最佳实践

应该:
  • 使用 Kotlin Coroutines 和 Flow
  • 遵循单向数据流
  • 为新 UI 使用 Jetpack Compose
  • 妥善处理生命周期
  • 支持多种屏幕尺寸
不应该:
  • 阻塞主线程
  • 泄漏上下文
  • 将尺寸硬编码
  • 忽略进程死亡
  • 跳过 ProGuard 规则

Kotlin Multiplatform(KMP)/ Compose Multiplatform(CMP)

使用 KMP 共享业务逻辑
// shared/src/commonMain/kotlin/UserRepository.kt
expect class PlatformContext

class UserRepository(private val api: ApiService, private val db: Database) {
    suspend fun getUsers(): List<User> {
        return try {
            val remote = api.fetchUsers()
            db.saveUsers(remote)
            remote
        } catch (e: Exception) {
            db.getCachedUsers()
        }
    }
}

// shared/src/androidMain/kotlin/PlatformContext.kt
actual class PlatformContext(val context: Context)

// shared/src/iosMain/kotlin/PlatformContext.kt
actual class PlatformContext
Compose Multiplatform(共享 UI)
// shared/src/commonMain/kotlin/App.kt
@Composable
fun App() {
    MaterialTheme {
        var users by remember { mutableStateOf<List<User>>(emptyList()) }
        val repository = remember { UserRepository() }

        LaunchedEffect(Unit) {
            users = repository.getUsers()
        }

        LazyColumn {
            items(users) { user ->
                UserCard(user)
            }
        }
    }
}

// Runs natively on Android, iOS, Desktop, and Web
KMP 项目结构
project/
├── shared/
│   └── src/
│       ├── commonMain/     # Shared Kotlin code
│       ├── androidMain/    # Android-specific
│       ├── iosMain/        # iOS-specific
│       └── desktopMain/    # Desktop-specific
├── androidApp/             # Android entry point
├── iosApp/                 # iOS entry point (Swift/SwiftUI)
└── desktopApp/             # Desktop entry point

Navigation 3(类型安全导航)

// Define routes as serializable data classes
@Serializable
data object Home

@Serializable
data class Detail(val itemId: String)

@Serializable
data class Settings(val section: String? = null)

// Navigation setup
@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(navController = navController, startDestination = Home) {
        composable<Home> {
            HomeScreen(onItemClick = { id ->
                navController.navigate(Detail(itemId = id))
            })
        }
        composable<Detail> { backStackEntry ->
            val detail: Detail = backStackEntry.toRoute()
            DetailScreen(itemId = detail.itemId)
        }
        composable<Settings> { backStackEntry ->
            val settings: Settings = backStackEntry.toRoute()
            SettingsScreen(section = settings.section)
        }
    }
}

API 35(Android 15)功能

功能描述
强制启用边到边显示应用必须妥善处理窗口内边距
预测性返回系统返回手势,并带有预览动画
私人空间由用户控制的隐藏应用配置文件
卫星连接通过卫星发送 SMS/MMS
应用归档自动归档未使用的应用,同时保留数据
Health Connect更新后的健康数据 API
// Edge-to-edge (required on API 35+)
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()

    setContent {
        Scaffold(
            modifier = Modifier.fillMaxSize(),
            contentWindowInsets = ScaffoldDefaults.contentWindowInsets,
        ) { innerPadding ->
            MainContent(modifier = Modifier.padding(innerPadding))
        }
    }
}

// Predictive back with Compose
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailScreen(onBack: () -> Unit) {
    BackHandler(onBack = onBack)
    // Content renders with predictive back animation automatically
}

使用基准配置文件提升启动性能

// baselineprofile/build.gradle.kts
dependencies {
    implementation("androidx.benchmark:benchmark-macro-junit4:1.3.0")
}

// BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generateBaselineProfile() {
        rule.collect(packageName = "com.example.app") {
            // Critical user journey
            pressHome()
            startActivityAndWait()
            device.findObject(By.text("Login")).click()
            device.wait(Until.hasObject(By.text("Dashboard")), 5000)
        }
    }
}
// app/build.gradle.kts
android {
    buildTypes {
        release {
            // Baseline profiles improve startup by 15-30%
            baselineProfile.automaticGenerationDuringBuild = true
        }
    }
}

Credential Manager API

// Modern sign-in replacing legacy APIs
val credentialManager = CredentialManager.create(context)

// Sign in with passkeys, passwords, or federated identity
suspend fun signIn() {
    val request = GetCredentialRequest(listOf(
        GetPasswordOption(),
        GetPublicKeyCredentialOption(requestJson = passkeyRequestJson),
        GetGoogleIdOption(serverClientId = WEB_CLIENT_ID),
    ))

    try {
        val result = credentialManager.getCredential(context, request)
        handleSignIn(result)
    } catch (e: GetCredentialException) {
        handleSignInError(e)
    }
}

// Create a passkey
suspend fun createPasskey() {
    val request = CreatePublicKeyCredentialRequest(
        requestJson = createPasskeyRequestJson
    )
    val result = credentialManager.createCredential(context, request)
    handlePasskeyCreation(result)
}

评论

支持 Markdown