3. Kotlin函数与面向对象:函数声明、默认参数、命名参数、高阶函数、Lambda表达式、类、对象、继承、接口、数据类、密封类

Kotlin 的函数与面向对象体系,是 Jetpack Compose 声明式 UI 的语法基石。Compose 中几乎每一个界面元素(如 Text、Button)都是通过函数调用来构建的,而状态管理、主题定制、业务逻辑封装则高度依赖类、接口、数据类与密封类的配合。本章将系统梳理 Kotlin 函数从基础声明到高阶抽象的演进路径,并深入剖析面向对象的核心机制,帮助你在 Compose 开发中写出更简洁、更安全、更易维护的代码。

Kotlin 函数与面向对象知识体系 函数体系 基础声明 默认参数 / 命名参数 高阶函数 Lambda 表达式 函数类型: (T) -> R | 内联函数 面向对象体系 类 / 对象 / 继承 接口 / 抽象类 数据类 data class 密封类 sealed class Compose 状态管理: mutableStateOf / ViewModel Compose 中的融合应用 @Composable 函数 = 函数 + Lambda + 状态类 密封类管理 UI 状态,数据类承载界面数据 典型 Compose 代码模式 fun MyScreen(viewModel: MyViewModel = viewModel()) { ... } sealed interface UiState { data class Success(val data: List<Item>) : UiState }

3.1 函数声明:从基础到表达式体

核心原理

Kotlin 函数使用 fun 关键字声明,基本语法为 fun 函数名(参数列表): 返回类型 { 函数体 }。与 Java 不同,Kotlin 支持表达式体函数——当函数体仅由单个表达式构成时,可以省略花括号和 return,直接使用 = 连接表达式。这一特性在 Compose 中极为常见,例如声明一个简单的 UI 组件:

// 传统块体函数
fun calculateArea(width: Int, height: Int): Int {
    return width * height
}

// 表达式体函数(类型可推断)
fun calculateArea(width: Int, height: Int) = width * height

// Compose 中的实际应用
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(text = "Hello, $name!", modifier = modifier)
}

判断方法与操作流程

选择块体还是表达式体,核心判断标准是函数体的复杂度。如果函数逻辑超过一个表达式(包含条件分支、循环、多步操作),必须使用块体;如果函数只是简单计算或返回一个值,优先使用表达式体。在 Compose 中,@Composable 函数通常使用块体,因为内部包含多个 UI 元素的组合调用。

案例拆解:Compose 中的函数设计

假设我们要构建一个用户信息展示组件,需要根据用户等级显示不同颜色。初始版本使用块体函数,代码冗长;重构后使用表达式体配合 when 表达式,代码更加简洁:

// 重构前
@Composable
fun UserBadge(level: Int) {
    val color = when (level) {
        1 -> Color.Gray
        2 -> Color.Blue
        3 -> Color.Gold
        else -> Color.Black
    }
    Box(modifier = Modifier.background(color).size(40.dp))
}

// 重构后:提取表达式体函数
fun badgeColor(level: Int): Color = when (level) {
    1 -> Color.Gray
    2 -> Color.Blue
    3 -> Color.Gold
    else -> Color.Black
}

@Composable
fun UserBadge(level: Int) {
    Box(modifier = Modifier.background(badgeColor(level)).size(40.dp))
}
经验提醒:在 Compose 中,将颜色计算、尺寸计算等纯逻辑提取为普通函数(非 Composable),有助于提升代码可测试性和复用性。Composable 函数应专注于 UI 组合,业务逻辑尽量下沉到普通函数或 ViewModel 中。

常见误区

  • 过度使用表达式体:当表达式体函数超过一行时,可读性反而下降,建议保持简洁。
  • 忽略 Unit 返回类型:无返回值的函数应显式声明 : Unit 或省略,但不要写成 = Unit。
  • Composable 函数命名:必须以大写字母开头,这是 Compose 编译器的强制要求。

3.2 默认参数与命名参数:提升 API 灵活性

核心原理

Kotlin 允许在函数声明时为参数指定默认值,调用时可以省略这些参数。命名参数则允许在调用时通过参数名指定值,不受声明顺序限制。两者结合,可以大幅减少函数重载的数量,让 API 设计更加清晰。在 Compose 中,几乎所有组件都使用了默认参数,例如 Modifier、color、fontSize 等都有默认值。

// 带默认参数的函数
fun createButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    backgroundColor: Color = Color.Blue
) { /* ... */ }

// 调用方式
createButton("点击", onClick = { /* 处理点击 */ })
createButton("提交", onClick = { /* 提交 */ }, enabled = false, backgroundColor = Color.Red)

操作流程与判断方法

设计默认参数时,应遵循以下原则:

  1. 将最常用的参数放在前面,带默认值的参数放在后面。
  2. 默认值应选择“安全”的初始值,避免空指针或异常。
  3. 在 Compose 中,modifier 参数通常放在第一个或第二个位置,并默认 Modifier。
  4. 命名参数在调用时能显著提升代码可读性,尤其是参数较多时。

案例拆解:Compose 组件参数设计

以自定义一个 NetworkImage 组件为例,展示默认参数与命名参数的实际应用:

@Composable
fun NetworkImage(
    url: String,
    modifier: Modifier = Modifier,
    placeholder: Painter? = null,
    error: Painter? = null,
    contentScale: ContentScale = ContentScale.Fit,
    crossfade: Boolean = true
) {
    // 实际实现使用 Coil 或 Glide 加载图片
}

// 调用时使用命名参数,清晰表达意图
NetworkImage(
    url = "https://example.com/image.jpg",
    modifier = Modifier.size(100.dp).clip(CircleShape),
    contentScale = ContentScale.Crop,
    crossfade = false
)
风险提示:在 Compose 中,如果默认参数是对象类型(如 Painter?),应确保默认值不会导致不必要的对象创建。对于 Modifier,默认值 Modifier 是单例,不会产生额外开销。但自定义默认对象时需谨慎,避免每次重组都创建新实例。

常见误区

  • 默认参数顺序错误:带默认值的参数不能位于无默认值参数之前(除非使用命名参数调用)。
  • 重载冲突:在 Java 互操作时,默认参数不会自动生成重载方法,需使用 @JvmOverloads 注解。
  • 命名参数与位置参数混用:一旦使用命名参数,后续参数也必须使用命名参数(Kotlin 1.4+ 允许位置参数在命名参数之后,但建议保持一致性)。

3.3 高阶函数与 Lambda 表达式:函数式编程的核心

核心原理

高阶函数是指以函数作为参数或返回值的函数。Lambda 表达式则是匿名函数的简洁写法。Kotlin 的函数类型语法为 (参数类型) -> 返回类型。在 Compose 中,高阶函数无处不在:onClick 回调、LaunchedEffect 中的挂起函数、remember 的计算逻辑等。

// 高阶函数:接收函数参数
fun  List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (item in this) {
        if (predicate(item)) result.add(item)
    }
    return result
}

// Lambda 表达式调用
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = numbers.customFilter { it % 2 == 0 }

// Compose 中的高阶函数应用
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {  // Lambda 作为参数
        Text("点击次数: $count")
    }
}

操作流程与判断方法

编写高阶函数时,需要明确以下几点:

  1. 确定函数类型的参数签名,包括参数个数和返回类型。
  2. 考虑是否使用 inline 修饰符——当函数参数是 Lambda 且被频繁调用时,内联可以消除 Lambda 对象创建的开销。
  3. 在 Compose 中,@Composable 函数不能直接作为普通高阶函数的参数,需要使用 @Composable 标记的 Lambda。

案例拆解:Compose 中的事件回调设计

假设我们需要设计一个可复用的列表组件,支持自定义点击行为。通过高阶函数,我们可以将事件处理逻辑完全交给调用方:

@Composable
fun <T> ItemList(
    items: List<T>,
    modifier: Modifier = Modifier,
    onItemClick: (T) -> Unit = {},
    itemContent: @Composable (T) -> Unit
) {
    LazyColumn(modifier = modifier) {
        items(items) { item ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onItemClick(item) }
            ) {
                itemContent(item)
            }
        }
    }
}

// 使用示例
ItemList(
    items = userList,
    onItemClick = { user -> navigateToProfile(user.id) },
    itemContent = { user -> UserRow(user) }
)
性能要点:在 Compose 中,Lambda 捕获外部变量时会产生额外开销。如果 Lambda 不捕获任何变量,可以将其声明为顶层函数或使用 remember 缓存。对于高频重组场景,使用 rememberUpdatedState 或 rememberCoroutineScope 来优化回调的稳定性。

常见误区

  • 滥用高阶函数:过度嵌套 Lambda 会降低代码可读性,建议适当提取具名函数。
  • 忽略内联优化:对于性能敏感的高阶函数,不使用 inline 可能导致额外的对象分配。
  • Composable Lambda 误用:普通 Lambda 不能调用 Composable 函数,必须使用 @Composable 注解的 Lambda 类型。

3.4 类、对象与继承:构建 UI 组件的骨架

核心原理

Kotlin 的类声明使用 class 关键字,支持主构造函数和次构造函数。对象(object)用于创建单例。继承使用 : 符号,默认情况下类和方法都是 final 的,需要显式使用 open 关键字才能被继承和重写。在 Compose 中,类主要用于 ViewModel、状态容器和业务逻辑封装。

// 类与继承
open class BaseViewModel {
    open fun loadData() { /* 默认实现 */ }
}

class UserViewModel : BaseViewModel() {
    override fun loadData() {
        // 具体实现
    }
}

// 单例对象
object AppConfig {
    val apiBaseUrl = "https://api.example.com"
    fun getTimeout(): Long = 30_000
}

// 伴生对象(类似静态成员)
class NetworkClient {
    companion object {
        const val TAG = "NetworkClient"
        fun create(): NetworkClient = NetworkClient()
    }
}

判断方法与操作流程

在 Compose 项目中,选择类还是函数取决于职责:

  • UI 组件:优先使用 Composable 函数,而非自定义 View 类。
  • 业务逻辑:使用 ViewModel 类,配合 StateFlow 或 mutableStateOf 管理状态。
  • 工具类:使用单例对象(object)或顶层函数。
  • 数据模型:使用数据类(见 3.6 节)。

案例拆解:ViewModel 与 Compose 的协作

以下是一个典型的 ViewModel 类设计,展示类、继承和对象在 Compose 项目中的实际应用:

// 基类:封装通用状态处理
open class BaseViewModel : ViewModel() {
    protected val _isLoading = mutableStateOf(false)
    val isLoading: State<Boolean> = _isLoading

    protected fun showLoading() { _isLoading.value = true }
    protected fun hideLoading() { _isLoading.value = false }
}

// 具体业务 ViewModel
class ProductViewModel : BaseViewModel() {
    private val _products = mutableStateOf<List<Product>>(emptyList())
    val products: State<List<Product>> = _products

    fun fetchProducts() {
        showLoading()
        viewModelScope.launch {
            try {
                val result = repository.getProducts()
                _products.value = result
            } finally {
                hideLoading()
            }
        }
    }
}

// 在 Composable 中使用
@Composable
fun ProductScreen(viewModel: ProductViewModel = viewModel()) {
    val products by viewModel.products
    val isLoading by viewModel.isLoading

    if (isLoading) {
        CircularProgressIndicator()
    } else {
        ProductList(products)
    }
}
风险提示:在 ViewModel 中直接暴露 mutableStateOf 可能导致状态被外部修改。建议使用 State 只读接口暴露状态,修改操作封装在 ViewModel 内部方法中。

常见误区

  • 过度使用继承:Compose 推荐组合优于继承,尽量使用函数参数和 Lambda 实现代码复用。
  • 单例滥用:将可变状态放入单例对象会导致全局状态污染,应使用依赖注入或 ViewModel。
  • 忽略 open 关键字:在 Kotlin 中,类和方法默认 final,需要继承时必须显式声明 open。

3.5 接口与抽象类:定义行为契约

核心原理

接口(interface)定义了一组行为契约,Kotlin 接口支持默认方法实现和属性声明。抽象类(abstract class)可以包含抽象成员和具体实现,适合有共享状态的场景。在 Compose 中,接口常用于定义仓库(Repository)层、状态管理策略等。

// 接口定义
interface ProductRepository {
    val cacheSize: Int  // 接口属性
    suspend fun getProducts(): List<Product>
    suspend fun getProductById(id: String): Product?

    // 默认方法实现
    fun logAccess() {
        println("Repository accessed at ${System.currentTimeMillis()}")
    }
}

// 实现接口
class NetworkProductRepository(
    private val api: ProductApi
) : ProductRepository {
    override val cacheSize: Int = 100

    override suspend fun getProducts(): List<Product> {
        return api.fetchProducts()
    }

    override suspend fun getProductById(id: String): Product? {
        return api.fetchProduct(id)
    }
}

// 抽象类示例
abstract class BaseRepository {
    abstract val tag: String
    protected fun logError(e: Exception) {
        Log.e(tag, "Error: ${e.message}")
    }
}

判断方法与操作流程

选择接口还是抽象类,可以参考以下标准:

维度接口抽象类
多继承支持(一个类可实现多个接口)不支持(单继承)
状态存储不能存储实例状态(属性需实现)可以存储状态
构造器无构造器有构造器
访问控制成员默认 public支持 protected 等
适用场景行为契约、多态共享代码、模板方法

案例拆解:依赖注入中的接口应用

在 Compose 项目中,接口常用于解耦数据层与 UI 层。以下是一个完整的依赖注入示例:

// 定义接口
interface AuthRepository {
    suspend fun login(username: String, password: String): Result<User>
    suspend fun logout()
}

// 实现类
class FirebaseAuthRepository : AuthRepository {
    override suspend fun login(username: String, password: String): Result<User> {
        return try {
            val user = firebaseAuth.signInWithEmailAndPassword(username, password)
            Result.success(user.toDomain())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun logout() {
        firebaseAuth.signOut()
    }
}

// 在 ViewModel 中注入接口
class LoginViewModel(
    private val authRepository: AuthRepository
) : ViewModel() {
    fun login(username: String, password: String) {
        viewModelScope.launch {
            authRepository.login(username, password)
                .onSuccess { user -> /* 更新 UI 状态 */ }
                .onFailure { error -> /* 显示错误 */ }
        }
    }
}
最佳实践:在 Compose 项目中,使用接口定义 Repository 层,配合 Hilt 或 Koin 进行依赖注入,可以轻松替换实现(如从 Firebase 切换到 Mock 数据),极大提升测试性和可维护性。

常见误区

  • 接口设计过于庞大:遵循接口隔离原则,将大接口拆分为多个小接口。
  • 抽象类滥用:如果不需要共享状态,优先使用接口。
  • 忽略默认方法:Kotlin 接口的默认方法可以减少实现类的重复代码。

3.6 数据类与密封类:状态管理的利器

核心原理

数据类(data class)自动生成 equals()、hashCode()、toString()、copy() 和 componentN() 方法,非常适合承载不可变数据。密封类(sealed class)用于表示受限的类层次结构,所有子类必须嵌套在密封类内部或同一文件中。在 Compose 中,密封类常用于管理 UI 状态(加载中、成功、失败),数据类用于承载界面数据。

// 数据类
data class User(
    val id: String,
    val name: String,
    val email: String,
    val avatarUrl: String? = null
)

// 使用 copy 方法
val updatedUser = user.copy(name = "新名字")

// 密封类管理 UI 状态
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String, val code: Int? = null) : UiState<Nothing>()
}

// 在 ViewModel 中使用
class ProfileViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState<User>>(UiState.Loading)
    val uiState: State<UiState<User>> = _uiState

    fun loadProfile() {
        _uiState.value = UiState.Loading
        viewModelScope.launch {
            _uiState.value = try {
                UiState.Success(repository.getProfile())
            } catch (e: Exception) {
                UiState.Error(e.message ?: "未知错误")
            }
        }
    }
}

操作流程与判断方法

使用数据类和密封类的关键决策点:

  1. 数据类:当类主要承载数据且需要比较、复制时使用。注意数据类不能是抽象类、开放类、密封类或内部类。
  2. 密封类:当状态或操作有固定且有限的类型集合时使用。在 when 表达式中,密封类可以穷举所有分支,无需 else。
  3. 密封接口:Kotlin 1.5+ 支持 sealed interface,适合更灵活的状态设计。

案例拆解:完整的 UI 状态管理

以下是一个结合数据类和密封类的完整案例,展示如何在 Compose 中优雅地处理异步状态:

// 定义数据模型
data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val author: String,
    val publishedAt: Long
)

// 定义 UI 状态
sealed class ArticleUiState {
    object Loading : ArticleUiState()
    data class Success(val articles: List<Article>) : ArticleUiState()
    data class Error(val message: String, val retryAction: (() -> Unit)? = null) : ArticleUiState()
}

// ViewModel
class ArticleViewModel : ViewModel() {
    private val _uiState = mutableStateOf<ArticleUiState>(ArticleUiState.Loading)
    val uiState: State<ArticleUiState> = _uiState

    fun loadArticles() {
        _uiState.value = ArticleUiState.Loading
        viewModelScope.launch {
            _uiState.value = try {
                val articles = repository.getArticles()
                ArticleUiState.Success(articles)
            } catch (e: IOException) {
                ArticleUiState.Error("网络连接失败", retryAction = { loadArticles() })
            } catch (e: Exception) {
                ArticleUiState.Error("加载失败: ${e.message}")
            }
        }
    }
}

// Composable 中的 when 穷举
@Composable
fun ArticleScreen(viewModel: ArticleViewModel = viewModel()) {
    val uiState by viewModel.uiState

    when (val state = uiState) {
        is ArticleUiState.Loading -> {
            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                CircularProgressIndicator()
            }
        }
        is ArticleUiState.Success -> {
            LazyColumn {
                items(state.articles) { article ->
                    ArticleCard(article)
                }
            }
        }
        is ArticleUiState.Error -> {
            ErrorView(
                message = state.message,
                onRetry = { state.retryAction?.invoke() }
            )
        }
    }
}
设计要点:密封类 + when 穷举是 Compose 状态管理的黄金组合。它强制开发者处理所有可能的状态分支,避免遗漏。同时,数据类的 copy() 方法可以方便地更新部分字段,保持不可变性。

常见误区

  • 数据类属性过多:超过 5-6 个属性时,考虑拆分或使用嵌套数据类。
  • 密封类子类分散:Kotlin 1.5 之前,子类必须嵌套在密封类内部;1.5+ 允许同文件不同类,但建议保持嵌套以提升可读性。
  • 在数据类中使用可变属性:数据类应保持不可变性(val),避免使用 var,否则 copy() 和 equals() 的行为可能不符合预期。
  • 忽略解构声明:数据类的 componentN() 方法支持解构,如 val (id, name) = user,但过度使用会降低代码可读性。

3.7 综合实战:构建一个完整的 Compose 功能模块

实战场景

我们将综合运用本章所有知识点,构建一个简单的“待办事项”功能模块。该模块包含:数据模型(数据类)、状态管理(密封类)、业务逻辑(ViewModel + 接口)、UI 组件(Composable 函数 + 高阶函数)。

实现步骤

// 1. 数据模型
data class TodoItem(
    val id: Long,
    val title: String,
    val isCompleted: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)
// 2. 仓库接口
interface TodoRepository {
    suspend fun getTodos(): List<TodoItem>
    suspend fun addTodo(title: String): TodoItem
    suspend fun toggleTodo(id: Long)
}
// 3. 实现类
class LocalTodoRepository : TodoRepository {
    override suspend fun getTodos(): List<TodoItem> = todoDao.getAll()
    override suspend fun addTodo(title: String): TodoItem = todoDao.insert(TodoItem(title = title))
    override suspend fun toggleTodo(id: Long) = todoDao.toggle(id)
}
// 4. UI 状态
sealed class TodoUiState {
    object Loading : TodoUiState()
    data class Success(val todos: List<TodoItem>) : TodoUiState()
    data class Error(val message: String) : TodoUiState()
}
// 5. ViewModel
class TodoViewModel(
    private val repository: TodoRepository
) : ViewModel() {
    private val _uiState = mutableStateOf<TodoUiState>(TodoUiState.Loading)
    val uiState: State<TodoUiState> = _uiState
    fun loadTodos() {
        _uiState.value = TodoUiState.Loading
        viewModelScope.la