嘿,我是Agnes。说实话,刚开始写这篇文章的时候,我甚至有点想吐槽——为什么Android混编能让人这么头秃?但你别说,这事儿一旦理顺了,那叫一个爽。
今天咱不整那些虚头巴脑的教材式开场,我就以一个过来人的身份,跟你聊聊Android开发里最让人又爱又恨的话题:Java和Kotlin混编。
一、为什么会有混编这个坑?
先说个真实场景。
2018年,Google在I/O大会上正式把Kotlin列为Android开发的一等公民。那会儿很多公司手里还攥着几万行Java代码,重写?成本太高。不重写?Android Studio又默认推荐Kotlin,Gradle配置稍微不对,编译直接报错。
这就是混编诞生的背景。
你现在的公司大概率是这样:
- 老模块全是Java,跑得好好的
- 新模块用Kotlin写,性能好、代码简洁
- 两个语言的项目得放在一起跑
混编不是你想不想用的问题,而是现实逼着你用的问题。
二、从HelloWorld开始:混编项目怎么建?
2.1 用Android Studio新建项目时的选择
打开Android Studio 2022+(现在都出Flamingo、Giraffe版本了),新建项目时你会看到这样一个界面:
□ Include Kotlin Support
□ Include C++ Support
重点来了: 如果你选了”Include Kotlin Support”,项目里会默认只有Kotlin代码,没有Java文件。如果你想混编,不要勾选这个选项,或者新建完项目后再手动添加Kotlin支持。
2.2 手动搭建混编环境的步骤
假设你已经有一个Java项目,现在要加Kotlin支持:
第一步:在项目级build.gradle里加Kotlin插件
// build.gradle (Project level)
buildscript {
ext.kotlin_version = '1.9.22' // 用最新的稳定版
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
第二步:在模块级build.gradle里应用插件
// build.gradle (Module level)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android' // Kotlin Android插件
id 'org.jetbrains.kotlin.kapt' // 用于注解处理器(Lombok、Dagger等)
}
android {
namespace 'com.example.mixedproject'
compileSdk 34
defaultConfig {
applicationId "com.example.mixedproject"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
}
第三步:创建第一个Kotlin文件
在Java包旁边新建一个Kotlin包(或者直接建在同一个包下),创建WelcomeActivity.kt:
package com.example.mixedproject
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class WelcomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_welcome)
// 直接操作View,Kotlin的视图绑定用起来真舒服
val textView: TextView = findViewById(R.id.tv_welcome)
textView.text = "Hello from Kotlin!"
}
}
第四步:在Java里调用Kotlin的Activity
这时候问题来了:Java怎么启动Kotlin的Activity?
答案是:直接调用,完全没区别。
// JavaActivity.java
package com.example.mixedproject;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class JavaActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java);
Button btnToKotlin = findViewById(R.id.btn_to_kotlin);
btnToKotlin.setOnClickListener(v -> {
// 直接跳转,Java调用Kotlin,丝滑
Intent intent = new Intent(this, WelcomeActivity.class);
startActivity(intent);
});
}
}
看到没?在Android层面,Java和Kotlin编译后都是.class字节码,JVM根本不管你是哪个写的。所以跨语言调用Activity、Fragment、ViewModel,跟同语言调用完全一样。
三、混编的核心配置:让Java和Kotlin和平共处
3.1 编译选项配置
这是最容易踩坑的地方。你在build.gradle里必须加这些:
android {
// ...其他配置...
compileOptions {
// Java 8支持,Kotlin很多特性依赖这个
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
// JVM目标版本,必须和compileOptions一致
jvmTarget = '1.8'
// 允许使用实验性API(比如suspend函数在旧版Android上的支持)
freeCompilerArgs += ['-Xallow-jvm-ir-dependencies']
}
}
为什么必须配这个?
因为Kotlin默认编译成Kotlin IR(中间表示),如果Java模块想用Kotlin的类,JVM必须能用标准Java字节码理解它。jvmTarget = '1.8'就是告诉Kotlin编译器:”别搞花里胡哨的,给我生成标准的Java 8字节码。”
3.2 混淆配置(ProGuard/R8)
如果你用了代码混淆,必须加这些规则:
# keep.gradle
# Kotlin反射支持
-keepattributes Signature, InnerClasses, BootstrapMethods
-keepclassmembernames class ** {
@kotlin.Metadata <fields>;
}
# 保留所有Kotlin类
-keep class ** implements kotlin.Metadata { *; }
# 保留数据类(如果有)
-keep class ** {
@kotlinx.serialization.* <fields>;
}
# 保留suspend函数(如果你用了协程)
-keepclassmembers,allowobfuscation class * {
public final <methods>;
}
不加这些会怎样?
混淆后,Kotlin的元数据(@kotlin.Metadata注解)会被删掉,导致反射失效、数据类序列化崩掉、协程的挂起点被误杀。你编译能过,但运行时直接Crash。
四、实战:混编项目的典型结构
一个好的混编项目,结构要清晰。我见过太多项目,Java和Kotlin文件乱炖在一起,后期维护简直是噩梦。
4.1 推荐的包结构
com.example.mixedproject/
├── java/ # Java代码根目录
│ ├── base/
│ │ ├── BaseActivity.java
│ │ └── BaseFragment.java
│ ├── ui/
│ │ ├── MainActivity.java # Java写的旧页面
│ │ └── settings/
│ │ └── SettingsActivity.java
│ └── network/
│ └── ApiService.java # 用Retrofit + Java
│
├── kotlin/ # Kotlin代码根目录
│ ├── base/
│ │ ├── BaseViewModel.kt
│ │ └── BaseRepository.kt
│ ├── ui/
│ │ ├── HomeActivity.kt # Kotlin写的新页面
│ │ └── profile/
│ │ ├── ProfileViewModel.kt
│ │ └── ProfileFragment.kt
│ └── network/
│ └── ApiServiceKt.kt # Kotlin版的网络层
│
└── shared/ # 共享代码,建议全部用Kotlin
├── constants/
├── utils/
└── extensions/
为什么要这样分?
Java代码尽量放在java/根目录,Kotlin代码放在kotlin/根目录。这样你在Android Studio里一眼就能看出哪些是老的、哪些是新的。
而且,新建的共享工具类、扩展函数、常量类,建议全部用Kotlin写。因为Kotlin的空安全、扩展函数、数据类在这些场景下优势巨大。
4.2 Java调用Kotlin:那些容易忽略的细节
场景1:Java调用Kotlin的数据类
// KotlinDataClass.kt
data class User(
val id: Int,
val name: String,
val email: String? = null // 注意:这是个可空类型
)
在Java里怎么用?
// JavaCaller.java
public class JavaCaller {
public void testKotlinDataClass() {
// 构造User对象
User user = new User(1, "张三", "zhangsan@example.com");
// 获取属性(Kotlin的数据类会自动生成getter)
int id = user.getId();
String name = user.getName();
String email = user.getEmail(); // 可能返回null!
// 如果用Kotlin的copy方法
User updatedUser = user.copy(name = "李四");
// 如果用Kotlin的copy + 默认参数
User noEmail = new User(2, "王五"); // email默认为null
}
}
关键点:
- Kotlin的数据类在Java里使用时,所有属性都变成getter方法
- 可空类型(
String?)在Java里就是普通的String,但返回值可能是null,你要自己判空 copy()方法在Java里也能调用,参数名也要带上
场景2:Java调用Kotlin的扩展函数
// StringExt.kt
fun String.capitalizeFirst(): String {
return this.isEmpty() ? this :
this[0].toString().uppercase() + this.substring(1)
}
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
在Java里调用:
// JavaExtCaller.java
public class JavaExtCaller {
public void testExtensions() {
String name = "hello";
// 扩展函数在Java里是静态方法调用
String capitalized = StringExtKt.capitalizeFirst(name);
boolean isValid = StringExtKt.isValidEmail("test@example.com");
System.out.println(capitalized); // Hello
System.out.println(isValid); // true
}
}
注意: 扩展函数在Java里调用时,要加Kt后缀(比如StringExtKt)。这是Kotlin编译器自动生成的。
场景3:Java调用Kotlin的协程
这是最容易出问题的地方!
// KotlinCoroutine.kt
class UserRepository {
// suspend函数在Java里会变成什么?
suspend fun getUser(id: Int): User {
return withContext(Dispatchers.IO) {
// 模拟网络请求
delay(1000L)
User(id, "张三", "zhangsan@example.com")
}
}
}
在Java里调用suspend函数:
// JavaCoroutineCaller.java
public class JavaCoroutineCaller {
public void testCoroutine() {
UserRepository repo = new UserRepository();
// 关键:必须在协程作用域内调用suspend函数
CoroutineScope scope = new CoroutineScopeKt.MainScope();
scope.launch(null, new CoroutineScopeKt.AnnotatedCoroutineStart(0), null, new Continuation<User>() {
@NonNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContextKt.getEmptyCoroutineContext();
}
@Override
public void resumeWith(@NotNull Object result) {
// 这里是协程恢复后的回调
if (result instanceof User) {
User user = (User) result;
System.out.println("User: " + user.getName());
} else if (result instanceof Throwable) {
((Throwable) result).printStackTrace();
}
}
});
}
}
天呐! 这段代码是不是看起来很恐怖?
别慌,我给你解释一下:
suspend函数在Java里调用时,必须用Continuation参数- 你不能用
launch或async这些Kotlin的扩展函数,因为它们也是Kotlin的代码 - 要用
CoroutineScopeKt和Continuation这些底层API
但是! 实际项目中,Java层通常不会直接调用Kotlin的协程。正确的做法是:
// 更好的方式:用一个普通的Java方法来封装协程调用
public class JavaCoroutineCaller {
public void fetchUser(int userId, Callback<User> callback) {
// 在Java里创建一个协程作用域
CoroutineScope scope = new CoroutineScopeKt.MainScope();
scope.launch(null, null, null, new Continuation<User>() {
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContextKt.getEmptyCoroutineContext();
}
@Override
public void resumeWith(@NotNull Object result) {
if (result instanceof User) {
// 在子线程回调,需要切回主线程
new Handler(Looper.getMainLooper()).post(() -> {
callback.onSuccess((User) result);
});
} else if (result instanceof Throwable) {
new Handler(Looper.getMainLooper()).post(() -> {
callback.onError((Throwable) result);
});
}
}
});
}
}
这样是不是清晰多了?
五、Kotlin调用Java:几乎无缝,但有几点要注意
Kotlin调用Java比反过来简单得多,因为Kotlin编译器会自动处理大部分兼容性问题。
5.1 Kotlin调用Java的普通方法
// JavaService.java
public class JavaService {
public String fetchData(String key) {
return "Data for " + key;
}
public List<String> getList() {
return Arrays.asList("Item1", "Item2", "Item3");
}
}
在Kotlin里调用:
// KotlinCaller.kt
class KotlinCaller {
fun testJavaCall() {
val service = JavaService()
// 直接调用,就像Kotlin自己的一样
val data = service.fetchData("user_123")
println(data) // Data for user_123
// List在Kotlin里会自动转换
val list: List<String> = service.getList()
list.forEach { println(it) }
}
}
5.2 Kotlin调用Java的可空类型
这是重点!Java里String可能是null,但Kotlin里String默认不可空。
// JavaNullable.java
public class JavaNullable {
public String findName(int id) {
// 可能返回null
return null;
}
}
在Kotlin里调用:
// KotlinNullableCaller.kt
class KotlinNullableCaller {
fun testNullable() {
val service = JavaNullable()
// Kotlin会自动把Java的String识别为String?
val name: String? = service.findName(1)
// 安全调用
println(name?.length) // null,因为findName返回null
// Elvis运算符
val safeName = name ?: "Unknown"
println(safeName) // Unknown
}
}
关键点: Kotlin会自动把Java的引用类型识别为可空类型,除非你用@NonNull注解标记。
5.3 Kotlin调用Java的静态方法
// JavaUtils.java
public class JavaUtils {
public static int calculate(int a, int b) {
return a + b;
}
public static String formatName(String firstName, String lastName) {
return "$firstName $lastName";
}
}
在Kotlin里调用:
// KotlinStaticCaller.kt
class KotlinStaticCaller {
fun testStatic() {
// 直接调用,不需要加类名
val result = calculate(10, 20)
println(result) // 30
val name = formatName("张", "三")
println(name) // 张 三
}
}
惊喜: Kotlin可以直接调用Java的静态方法,不需要写JavaUtils.calculate()!
六、常见报错及解决方案
这部分是精华,我按报错频率排序,每个都配上真实场景。
报错1:Kapt配置冲突
报错信息:
e: [kapt] Currently supported source roots: /home/user/project/app/src/main/java
e: /home/user/project/app/src/main/kotlin is not a valid source root
原因: kapt插件需要知道Kotlin源码目录,但配置不对。
解决方案:
在build.gradle里确保这样配置:
”`groovy android {
sourceSets {
main {
java {
srcDirs += 'src/main/java'
srcDirs += 'src/main/kotlin' // 关键:把Kotlin目录加进来
}
