Kotlin从语法到实战:协程、Flow、Compose与SQL Server全解析
2026/9/8 4:18:48 网站建设 项目流程

Kotlin 这门语言,前几年我还在观望,觉得“不过又是一个 JVM 语言”,真正用起来才发现自己错得离谱。身边搞 Android 的朋友早就从 Java 切到了 Kotlin,Google 也把 Kotlin 捧成了官方一等公民。最近有读者私信问我,说学 Kotlin 到底应该从哪里下手,是直接看语法、还是边做项目边学?还有人问 Compose、Flow 这些进阶概念怎么串起来,以及 Kotlin 能不能直接连 SQL Server 做后端数据库操作。

老实说,这些问题我当年也纠结过。Kotlin 的生态现在已经非常庞大,它能写 Android 客户端,能写服务端,能搞多平台,甚至通过 Kotlin/Native 和 C 互操作时还能生成头文件。但不管怎么用,地基始终是那套简洁、安全、函数式的核心语法。这篇文章我打算做一次比较完整的梳理,从 Kotlin 语法基础讲到协程和 Flow 的原理,再落到 Compose 实战和 SQL Server 连接方案,最后整理一份面试高频考点的速查表。不管你是刚接触 Kotlin 的新手,还是准备跳槽想在面试里不露怯的进阶开发者,这篇应该都能帮到你。

1. Kotlin 语法核心:它到底比 Java 好在哪里

很多人问我的第一句话是:Kotlin 语法真的有网上吹得那么香吗?我的回答是,香不香要看你有没有掌握它的设计思路。Kotlin 不是 Java 的“语法糖合集”,它是在类型系统、空安全、函数式编程这几个维度上重新思考了 JVM 开发。我们一条一条说。

1.1 空安全不是噱头,是从根上解决 NPE

接触 Kotlin 的人几乎都会先被“问号和感叹号”搞晕。为什么定义变量有时候要写?,有时候又要写!!?其实这套设计背后非常简单:Kotlin 在编译期就把“可能为空”和“不可能为空”的类型给区分开了。

var name: String = "Kotlin" // 非空类型,编译器保证不为 null var nickname: String? = null // 可空类型,使用前必须处理空的情况

Java 里你写String name = null;,编译器完全不拦你,等运行到name.length()那一刻才知道出事了。Kotlin 的思路是把这个检查提到编译期:你如果声明了非空类型,那就不能赋 null;如果声明可空类型,访问属性时编译器会强制你处理。比如我最常用的一段代码:

fun getUserName(user: User?): String { return user?.name ?: "匿名用户" }

这里?.是安全调用,只有user不为 null 才会访问name?:是 Elvis 操作符,左侧结果为 null 时用右侧兜底。这行代码放在 Java 里至少要写一个 if 判断,在 Kotlin 里一行搞定,而且空安全的意图非常明确。

!!这个非空断言操作符要谨慎使用。它在编译期告诉编译器“我保证这不会是 null”,运行时空指针照样崩。我在生产代码里几乎只用它来打破 Java 互操作时的平台类型限制,比如某个 Java 方法返回了可能为 null 的对象,但我确定业务上不会为 null。其他地方能用?.?:就绝不用!!,这是 Kotlin 社区踩过无数坑后的共识。

1.2 数据类与扩展函数:效率翻倍的两个“法宝”

Java 开发中最痛苦的事情之一就是写 POJO 类。定义几个字段,然后用 IDE 生成 getter、setter、toString、equals、hashCode,代码几百行,真正有用的业务逻辑没几行。Kotlin 的数据类一行解决:

data class User( val id: Int, val name: String, val email: String )

编译器会自动生成toString()equals()hashCode()copy()componentN()。注意data classequals只比较主构造函数中声明的属性,如果业务里需要额外字段参与比较,可以把它们写在类体里,但要清楚它们不会进入对象比较逻辑。这个特性在列表去重、对象对比、MVVM 架构中传递状态时特别好用。配合解构声明,还能写出这样的代码:

val (id, name, email) = user

扩展函数是 Kotlin 另一个让我上头的特性。它允许你给一个已经存在的类添加新函数,而不需要继承或修改原类。本质上,扩展函数在编译后就是一个静态方法,第一个参数是接收者对象,所以它不能访问接收者类的 private 成员,也不会被子类多态重写。

fun String.isValidMobile(): Boolean = matches(Regex("^1[3-9]\\d{9}$")) fun String.isValidEmail(): Boolean = isNotEmpty() && contains("@") && contains(".")

然后在业务代码里直接这样调用:

if (phoneInput.isValidMobile()) { ... }

这种写法让代码读起来像自然语言,可读性提升一大截。不过扩展函数也别滥用。你在自己的类里加扩展没问题,但如果你给大家共用的工具类或者第三方库的类到处加扩展,团队成员可能会疯掉——因为他们看代码的时候根本不知道这些方法从哪里“冒”出来的。合理的做法是把扩展函数按业务模块分文件管理,命名要体现用途,团队约定好目录规则。

1.3 object 与 companion object:Kotlin 里的“静态”新姿势

Java 里写单例要双重检查锁、静态内部类,面试八股文背了一堆。Kotlin 直接用object关键字声明单例:

object AppConfig { val appName: String = "MyKotlinApp" fun log(message: String) { println("[App] $message") } }

object声明的对象是懒加载的,在第一次访问时初始化,而且线程安全由 JVM 的类加载机制保证,不需要你写任何同步代码。这在全局配置、工具类、缓存管理器这类场景中非常实用。

companion object则用来模拟 Java 的 static 成员:

class UserRepository private constructor() { companion object { const val TABLE_NAME = "users" fun create(): UserRepository = UserRepository() } }

调用方式是UserRepository.create(),看起来像静态方法,但它实际上是一个伴生对象的方法。有一点很多人忽略:const val只允许修饰基本类型和 String,编译期会直接把值内联到调用处;而val在伴生对象里是运行时才访问的属性。这在做 SDK 版本判断、常量定义时会有细微差异,面试时被问到别答错。

Kotlin 的基础语法还有很多细节值得展开,比如sealed classinline classby委托、lateinitby lazy的区别,但这些在前面几节已经基本覆盖了日常开发的主干。接下来我们进入更进阶的领域:协程和 Flow。

2. 协程与 Flow:从会用一点点懂原理

如果你学 Kotlin 只停在语法层面,那可能只发挥了一半功力。协程和 Flow 才是 Kotlin 在并发和异步领域最硬核的武器。面试十有八九会问,实际项目里逃不掉,Compose 开发更是和它们深度绑定。这部分我尽量讲透原理,不说废话。

2.1 协程把异步代码“伪装”成同步代码

先看一个经典痛点。假设你要先请求用户信息,再根据用户信息请求订单列表,Java 里用回调写出来是这样的:

api.getUser(new Callback<User>() { @Override public void onSuccess(User user) { api.getOrders(user.id, new Callback<List<Order>>() { @Override public void onSuccess(List<Order> orders) { ... } }); } });

这种回调地狱让人头皮发麻,一旦嵌套到三层以上,代码基本没法维护。Kotlin 协程解决这个问题的方式非常优雅:

suspend fun loadOrders(): List<Order> { val user = api.getUser() // 挂起,不阻塞线程 return api.getOrders(user.id) // 挂起,不阻塞线程 }

suspend函数可以被挂起,挂起的含义是:当协程遇到耗时操作时,它会主动让出当前线程,等数据准备好再恢复执行。这看起来像同步代码,但底层是非阻塞的。

这里要强调一个概念:协程不是线程,而是运行在线程之上的“轻量级任务”。它可以把几十万个协程调度到少量线程上执行,而创建几十万个 Java 线程早就 OOM 了。我们用launch来启动一个协程:

viewModelScope.launch { val orders = loadOrders() updateUI(orders) }

默认情况下,launch会继承外层作用域的调度器。如果在 ViewModel 中,viewModelScope默认使用Dispatchers.Main,网络请求在挂起函数内部可以切换到 IO 线程,网络回来后再切回主线程更新 UI,整个切换过程由协程框架自动完成。

2.2 suspend 的底层本质:状态机

很多人面试被问“suspend 函数是怎么实现的”,答不上来就背书说“它只是一个标记”。其实 suspend 函数的实现原理是一个编译期状态机。

Kotlin 编译器会把每个挂起点(即每个调用 suspend 函数的地方)变成一个状态,整个函数体编译成一个匿名类,内部维护一个状态字段。当挂起发生时,函数保存当前状态和局部变量;恢复时,根据状态字段跳到对应分支继续执行。为了验证这一点,你可以把一个 suspend 函数反编译成字节码看,会发现它多了一个Continuation参数,而这个参数就是用来保存和恢复执行上下文的。

所以挂起的代价非常小,它不涉及线程切换,只是保存几个局部变量和状态数字,这也是协程能大量创建的底气所在。

2.3 Flow 的原理:冷流、热流,到底怎么选

Flow 是用来处理异步数据流的。协程解决了“异步执行一次”的问题,Flow 解决的是“异步产生多个值”的问题。比如监听数据库变化、监听传感器数据、处理分页加载,这些场景用 Flow 非常顺手。

先看 Flow 的冷热之分,这是面试高频雷区。

冷流(Cold Flow)是指每次有收集者 collect 时,生产者的代码才会执行。普通的flow { }构建器创建的就是冷流:

val coldFlow = flow { emit(1) emit(2) emit(3) } coldFlow.collect { println(it) } // 这时才执行 flow 块,打印 1、2、3

每次 collect,flow 块都会重新执行,生产者和消费者是一一对应的。

热流(Hot Flow)则是在创建时就独立于收集者运行,多个收集者共享同一份数据。StateFlowSharedFlow是常用的热流代表:

  • StateFlow:总是保存一个当前值,新订阅者立刻收到当前值,并且只在值变化时通知收集者。它天然适合表示 UI 状态,因为 UI 状态就需要“当前值 + 变化通知”。
  • SharedFlow:没有当前值概念,更像一个广播通道,多个订阅者都能收到事件,适合一次性事件流,比如 Toast 提示、导航事件。

实际开发中最常见的坑是:把一次性事件用StateFlow来传。比如登录失败后弹一个 Toast,如果用StateFlow,事件的消费方处理完事件后状态还是那个值,旋转屏幕后又会重新弹一次 Toast。这种场景应该用SharedFlow并配置replay = 0,让事件不重放。

Flow 还有一个重要问题是背压。所谓背压,就是生产者产生数据的速度比消费者处理数据的速度快,如果不做处理,数据会积压。简单的处理方式有三种:

操作符作用
buffer()为生产者设置独立缓冲区,生产者和消费者可以并发运行,缓冲满时挂起生产者
conflate()缓冲区只保留最新值,消费者处理不过来时丢弃旧值
collectLatest {}新值到达时取消上一个值的处理,专注于最新值

我处理实时数据流时最常用collectLatest,比如搜索关键词联想、高频的位置更新,这种场景只关心最新结果,旧结果被打断也无所谓。

2.4 用 Flow 串联登录接口的真实场景

纸上谈兵没用,我们直接看一个登录场景。用户输入用户名密码,点击登录,请求接口,最后展示结果。如果用 Flow 写,大概是这样:

sealed interface LoginUiState { object Idle : LoginUiState object Loading : LoginUiState data class Success(val token: String) : LoginUiState data class Error(val message: String) : LoginUiState } class LoginViewModel : ViewModel() { private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Idle) val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow() fun login(username: String, password: String) { viewModelScope.launch { _uiState.value = LoginUiState.Loading try { val token = repository.login(username, password) // suspend 函数 _uiState.value = LoginUiState.Success(token) } catch (e: Exception) { _uiState.value = LoginUiState.Error(e.message ?: "登录失败") } } } }

这段代码把 UI 状态收敛到了一个StateFlow中,ViewModel 通过_uiState对外暴露只读的uiState,UI 层收集它来渲染界面。这就是现在 Jetpack 架构里非常主流的状态管理模式。状态、事件、数据流的关系清晰了,后面接 Compose 也会顺理成章。

3. Compose 实战:从登录注册看声明式 UI 的架构

讲完协程和 Flow,终于可以进入 Compose 了。Compose 是 Android 官方的现代 UI 工具包,完全基于 Kotlin,摆脱了 XML 布局。作为一个做过传统 View 系统开发的 Android 开发者,我第一次用 Compose 的感受是:UI 居然能这样写。

3.1 Compose 的核心理念:状态驱动 UI

传统 View 系统里你想更新界面,流程是这样的:findViewById找到控件,然后调用控件的setTextsetVisibility等方法去修改。界面是手动的,状态和 UI 是两张皮,容易忘记同步。

Compose 完全不同,它是声明式 UI,你只需要描述 UI 在某个状态长什么样。状态变了,UI 自动重组:

@Composable fun LoginScreen(viewModel: LoginViewModel) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (val state = uiState) { is LoginUiState.Idle -> { /* 初始界面 */ } is LoginUiState.Loading -> { CircularProgressIndicator() } is LoginUiState.Success -> { Text("登录成功,token: ${state.token}") } is LoginUiState.Error -> { Text("出错:${state.message}") } } }

你看,UI 就是一个状态函数。状态变了,Compose 会重新执行相应区域的组合函数,自动生成新的 UI。collectAsStateWithLifecycle()会把 Flow 转换成 Compose 的状态,并且在界面进入后台时自动停止收集,避免内存泄漏和资源浪费。

这里有一个关键机制叫重组(Recomposition)。Compose 并不是每次状态变化都从头执行整个界面的组合函数,它只重组依赖了变化状态的那个部分。比如上面代码中,只有uiState变了,围绕when表达式的区域会重组,而界面上其他不依赖它的 Text、Button 不会受影响。这种细粒度的更新,是 Compose 性能优秀的基础。

3.2 登录注册 Demo 的分层架构

如果我们拿到一个“Compose 登录注册操作数据库”的开源代码,很容易被一堆文件搞晕。我来帮你拆解这种 Demo 的典型架构,它一般分三层:

  • UI 层:由@Composable函数组成,只负责状态渲染和事件回调。
  • ViewModel 层:持有 UI 状态(通常用StateFlow),处理业务逻辑,调用仓库层。
  • 数据层:Repository + DAO,封装数据库操作或者网络请求。

举个例子,注册功能中的用户名查重。UI 层拿到用户输入,调用 ViewModel 的register()方法;ViewModel 转发给 Repository;Repository 调用 DAO 查询数据库中的用户表。如果用户名已存在,返回错误状态,UI 层显示提示:

class UserRepository(private val userDao: UserDao) { suspend fun register(user: User): Result<Unit> = runCatching { val exists = userDao.findByUsername(user.name) != null if (exists) throw IllegalArgumentException("用户名已被注册") userDao.insert(user) } }

这种分层的好处是职责单一。DAO 只关心数据库读写,Repository 负责业务规则(比如用户名查重),ViewModel 负责状态转换和线程调度。UI 层根本不关心数据从哪里来,它只关系状态是什么。面试时如果被问到项目架构,照着这个思路讲,面试官大概率会认可。

3.3 remember 与 rememberSaveable:Compose 状态的两个关键函数

写 Compose 时最容易犯错的地方就是状态丢失。rememberrememberSaveable长得像,但作用范围完全不同。

  • remember只在当前组合(composition)中记住值。如果 Activity 被系统回收重建,整个组合销毁,remember 里的状态就没了。
  • rememberSaveable会把状态保存到 Bundle 中,Activity 重建后自动恢复,适合保存输入框内容、选中项这类 UI 状态。
@Composable fun LoginInput() { var username by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } OutlinedTextField( value = username, onValueChange = { username = it }, label = { Text("用户名") } ) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("密码") }, visualTransformation = PasswordVisualTransformation() ) }

用户旋转一下手机,输入框内容还在,这全靠rememberSaveable。在处理复杂对象时,你要确保对象能放进 Bundle(实现 Parcelable 或 Serializable),否则就得自定义Saver。这也是 Compose 面试题里经常考察的点。

3.4 Compose 操作数据库的跨层协作

如果我们把 Flow、Room 和 Compose 放在一起,一个完整的登录注册 Demo 数据流是这样的:

  1. 用户在 TextField 输入内容,onValueChange更新 Compose 状态。
  2. 点击按钮,调用 ViewModel 的方法。
  3. ViewModel 在协程中调用 Repository 的 suspend 函数。
  4. Repository 操作 Room 数据库(或调用远程 API)。
  5. 操作结果返回后,Service 更新StateFlow
  6. Composable 收集StateFlow,自动重组 UI。

这个链路非常清晰,也是目前 Android 官方推荐的架构模式。如果你手头有“Compose 登录注册操作数据库”的源码项目,按这个思路去看,很快就能理清每个文件的作用。

4. Kotlin 连接 SQL Server:从 JDBC 到封装

聊完 Android 端,再谈谈 Kotlin 在服务端或者工具类场景中如何连接 SQL Server。不少读者问过“Kotlin 的数据库连接 SqlServer”,其实这里的关键不是 Kotlin 本身,而是 JDBC。Kotlin/JVM 完全可以复用 Java 生态中的所有 JDBC 驱动。

4.1 最小的 JDBC 连接示例

首先你需要引入微软的 JDBC 驱动依赖。在 Gradle 中加:

dependencies { implementation("com.microsoft.sqlserver:mssql-jdbc:12.6.1.jre11") }

然后写一个最基础的连接查询:

import java.sql.DriverManager fun main() { val url = "jdbc:sqlserver://localhost:1433;databaseName=test;encrypt=true;trustServerCertificate=true" val user = "sa" val password = "your_password" DriverManager.getConnection(url, user, password).use { connection -> connection.createStatement().use { statement -> val rs = statement.executeQuery("SELECT id, name FROM users") while (rs.next()) { println("${rs.getInt("id")}: ${rs.getString("name")}") } } } }

注意use是 Kotlin 标准库提供的扩展函数,它可以在代码块执行完毕后自动关闭资源,相当于 Java 的 try-with-resources。JDBC 连接是资源密集型对象,用完必须释放,否则过一段时间连接数会耗尽。

encrypt=true;trustServerCertificate=true这两个参数是 SQL Server 新版驱动的默认要求,生产环境如果启用了 SSL 证书,要配置证书信任链,本地测试可以直接 trustServerCertificate 临时跳过证书校验。

4.2 封装一个简洁的查询工具

每次写 JDBC 样板代码实在太痛苦了。我一般会封装一个小工具类,重点解决三个问题:连接管理、参数绑定、结果集转对象。下面是一个简化版本:

object JdbcUtils { private const val URL = "jdbc:sqlserver://localhost:1433;databaseName=test;encrypt=true;trustServerCertificate=true" private const val USER = "sa" private const val PASSWORD = "your_password" fun <T> query(sql: String, mapper: (ResultSet) -> T): List<T> { DriverManager.getConnection(URL, USER, PASSWORD).use { conn -> conn.createStatement().use { stmt -> stmt.executeQuery(sql).use { rs -> val list = mutableListOf<T>() while (rs.next()) list.add(mapper(rs)) return list } } } } fun <T> query(sql: String, params: List<Any?>, mapper: (ResultSet) -> T): List<T> { DriverManager.getConnection(URL, USER, PASSWORD).use { conn -> conn.prepareStatement(sql).use { pstmt -> params.forEachIndexed { index, param -> pstmt.setObject(index + 1, param) } pstmt.executeQuery().use { rs -> val list = mutableListOf<T>() while (rs.next()) list.add(mapper(rs)) return list } } } } }

使用起来就很清爽:

val users = JdbcUtils.query("SELECT id, name FROM users WHERE age > ?", listOf(18)) { User(it.getInt("id"), it.getString("name")) }

这里我用了prepareStatement做参数化查询,强烈建议所有带用户输入的 SQL 都用这种方式,能有效防止 SQL 注入。拼字符串的方式虽然省事,但在任何生产环境都是大忌。

如果业务量上来,每次都新建连接显然不是好方案,这时候你需要连接池。HikariCP 是目前比较成熟的连接池选择,配置如下:

val config = HikariConfig().apply { jdbcUrl = "jdbc:sqlserver://localhost:1433;databaseName=test;encrypt=true;trustServerCertificate=true" username = "sa" password = "your_password" maximumPoolSize = 10 minimumIdle = 2 connectionTimeout = 30_000 } val dataSource = HikariDataSource(config)

有了连接池,就把DriverManager.getConnection替换成dataSource.connection,其他代码几乎不用改。连接池可以复用连接,避免频繁建立 TCP 连接的开销。

4.3 Android 直连 SQL Server 的劝退与替代方案

有人问能不能在 Android 上直接连接 SQL Server。技术上是可以的,用 JDBC 驱动就能连,但实际开发我强烈不建议这么干。原因有几个:

  • Android App 直接暴露数据库账号密码,反编译后立刻泄露。
  • 手机网络环境不稳定,数据库连接频繁断开,体验很差。
  • SQL Server 通常部署在内网,需要公网暴露,攻击面太大。
  • Android 主线程不能执行网络操作,还要处理协程调度,复杂度很高。

更合理的方案是:写一个后端服务(Kotlin 写服务端就是很好选择),通过 REST API 暴露数据;Android 端只关心网络请求和 UI 展示。架构清晰,数据安全可控,也不违背 KISS 原则。

真正用 Kotlin 连接 SQL Server 的场景,更多出现在服务端、本地工具、ETL 任务等 JVM 环境中。所以学 Kotlin 的时候,数据库连接这块可以参考 JDBC 方案,但要清楚它适合用在哪儿、不适合用在哪儿。

5. Kotlin 面试高频考点速查

最后一部分,我结合自己的面试和被面试经验,整理了 Kotlin 相关的高频考点。这个速查表不是一个一个背答案,而是给出了回答的切入点和思路。

5.1 语法底层面试题

高阶函数和 inline 有什么关系、有什么好处?

高阶函数就是把函数作为参数或返回值的函数。Kotlin 中 lambda 会被编译成 Function 接口的匿名内部类,每次调用都会创建对象,带来性能开销。用inline修饰函数后,编译器会把函数体以及 lambda 表达式内联到调用处,省去对象创建和函数调用开销。最常见的例子是letapplyrun这些标准库函数,它们都声明为 inline。面试时如果能说出“锁内联消除”、“reified 具体化类型参数”这些点,会加分。

by lazy 和 lateinit var 的区别?

by lazy是属性委托,只有第一次访问才初始化,默认线程安全,适合 val 属性;lateinit var是 var 的延迟初始化,适合依赖注入场景,比如@Inject lateinit var repository: UserRepository。使用 lateinit 要注意:在未初始化前访问会抛UninitializedPropertyAccessException,可以调用::repository.isInitialized判断。

object、companion object、sealed class 的使用场景?

object 适合单例;companion object 适合伴随类的静态成员;sealed class 适合受限的类层次结构,比如 UI 状态、网络回调中的 Success/Error/Loading。sealed class 的 when 表达式可以是穷举的,不需要 else 分支,这在状态管理中特别有用。

5.2 协程与 Flow 面试题

协程和线程的区别?

协程是线程之上的轻量级调度单元,可挂起可恢复,不阻塞线程;线程是操作系统调度的最小单位,切换有成本。协程适合大量并发但每个并发任务很轻的场景;线程适合 CPU 密集型任务。

Flow 是冷流还是热流?哪种场景选哪个?

普通flow{}是冷流,每次 collect 都会重新执行 producer。StateFlowSharedFlow是热流(注意这里的说法,严格说 StateFlow 是热流数据载体,但它本身基于协程实现),适合共享状态和事件广播。UI 状态用StateFlow,一次性事件用SharedFlow(replay = 0)

Flow 的背压怎么处理?

buffer()conflate()collectLatest()。面试时最好结合实际场景解释,比如搜索框输入联想用collectLatest,因为它需要丢弃过期结果。

5.3 Compose 面试题

Compose 重新组合的原理是什么?

Compose 使用可观察状态跟踪;状态变化后,组合作用域内依赖该状态的函数会被标记为 invalid;在重组阶段,仅重组这些函数,跳过参数未变化的函数。@Stable@Immutable注解可以帮助 Compose 做跳过优化,提升性能。

remember 和 rememberSaveable 的区别?

前者在组合销毁后丢失,后者通过 SavedState 保存,可以在 Activity 重建后恢复。复杂对象需要自定义 Saver 才能保存在 Bundle 中。

StateFlow 和 Compose 怎么配合?

通过collectAsStateWithLifecycle()将 Flow 转为 State,并在生命周期感知的情况下收集数据。后台时不收集,前台时恢复收集,避免无谓的资源消耗。

5.4 综合项目面试策略

如果面试官问“讲一个你做过的最复杂的 Kotlin 项目”,不要只说功能点,要按架构讲。先讲整体分层(UI / ViewModel / Repository),再讲数据流(事件如何从 UI 到数据库再回来),然后讲技术亮点(用了 Flow 做状态管理、Compose 实现无 XML UI、协程处理并发),最后讲踩过的坑(比如 UI 线程卡顿、Flow 背压处理、数据库连接池配置)。这样回答逻辑清晰,也容易把话题引向自己熟悉的方向。

一个比较冷门但容易出彩的加分项是提一下 Kotlin/Native。如果你了解 Kotlin 在 JS 之外通过 cinterop 和 C 语言互操作,可以讲讲 Kotlin 如何生成头文件供 C/C++ 调用,以及 Kotlin Multiplatform 如何共享业务逻辑。不是每个面试者都能说到这一层,作为一个亮点抛出来,面试官的印象分会高不少。

写在最后

我学 Kotlin 的过程其实不太顺利。一开始抱着“Java 够用了”的心态,看完语法就搁置了;后来工作需要写了几个 Compose 项目,才真正体会到处处的设计巧思。从一个 Java 老手的视角来看,Kotlin 最打动我的不是语法简洁,而是把那些容易犯错的“约定”变成了“编译期强制”。

如果你想学 Kotlin,我的建议是从一个小项目开始,比如做一个 Compose 写登录注册加 Room 数据库的 Demo。语法不懂就查,架构不熟就抄,等整个链路跑通了,Kotlin 的核心语法、协程、Flow、状态管理这些概念自然就串起来了。如果是为了面试,那这篇文章第五节的内容至少过两遍,再写一个拿得出手的实战项目,心里就有底了。

最后还是那句话:Kotlin 是一款不断增长的语言,官方对 Compose、Kotlin Multiplatform 和协程的投入都在加速。现在开始学,刚刚好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询