这篇文章不是那种”推荐XX个库”的清单文,而是一个踩过无数坑的老Android开发者,把血泪教训写成给你的”别走我走过的路”指南。
先说个真事儿
我带过三个实习生,第一个上来就用RxJava + Retrofit + Dagger2搭了个项目,两周没跑起来,天天问我”为什么compile失败”。第二个知道用Kotlin + Coroutines,结果选了个三年前就不更新的网络库,线上Crash满天飞。第三个……直接来找我,说”学长,能不能帮我审一下项目,我怕又踩坑”。
所以我决定写这个——不是推荐清单,是避坑指南。每个坑我都会说清楚:这是什么坑、为什么坑、怎么绕过去、正确的姿势是什么。
先给结论:选库的原则只有一个——活跃度+社区+版本匹配度。
一、依赖管理:新手最容易踩的”版本地狱”
1.1 为什么你的项目会”编译失败”
Android项目最常见的编译错误不是代码写错了,而是依赖版本冲突。
举个例子:
// build.gradle (Module: app)
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.9'
// 你以为没事,结果某个库依赖了okhttp 3.x
implementation 'com.xing:android-highlights:2.3.0'
}
android-highlights 内部依赖了 okhttp:3.14.9,Gradle解析依赖树时,会优先选择版本号最高的那个(4.9.3),但某些类在4.x和3.x之间接口变了,运行时就Crash。
1.2 怎么避免:版本锁定 + 依赖排除
方案一:在根build.gradle里统一版本
// build.gradle (Project)
ext {
okhttpVersion = '4.9.3'
gsonVersion = '2.10.1' // 注意:2.8.9有已知安全漏洞,升级到2.10.1
coroutinesVersion = '1.7.3'
}
// 然后在子模块里引用
dependencies {
implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okhttpVersion"
}
方案二:用exclude排除冲突依赖
dependencies {
implementation('com.xing:android-highlights:2.3.0') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
}
方案三:用Android Studio的依赖分析工具
在Android Studio里,菜单 → File → Project Structure → Dependencies,可以看到所有依赖的树形结构,红色标记的就是冲突。
或者在Terminal里运行:
./gradlew app:dependencies
会打印完整的依赖树,手动搜索okhttp就能找到冲突点。
二、网络库:别再碰Retrofit 1.x了
2.1 Retrofit 2.x的正确姿势
很多教程还在写Retrofit 1.x的语法,你照着做一定会报错。
Retrofit 1.x(已过时):
RestAdapter adapter = new RestAdapter.Builder()
.setEndpoint("https://api.example.com")
.build();
UserService service = adapter.create(UserService.class);
Retrofit 2.x(当前标准):
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
UserService service = retrofit.create(UserService.class);
注意:baseUrl必须以/结尾,否则相对路径拼接会出错。
2.2 OkHttp拦截器:日志打印的正确姿势
新手常犯的错误:直接打印Response,结果日志里全是乱码。
// 错误做法:直接打印body
String body = response.body().string();
Log.d("TAG", body);
// 正确做法:用OkHttp的HttpLoggingInterceptor
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.build();
这个拦截器会自动格式化JSON,打印请求头和响应头,调试时非常有用。
2.3 缓存策略:别让用户等
网络请求不加缓存,用户体验极差。
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(cacheDir, 10 * 1024 * 1024)) // 10MB缓存
.addInterceptor(chain -> {
Request request = chain.request();
// 有网络时用网络,无网络时用缓存(最多1分钟)
if (!isNetworkAvailable(context)) {
request = request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=60")
.build();
}
Response response = chain.proceed(request);
// 有网络时设置缓存策略
if (isNetworkAvailable(context)) {
int maxAge = 60; // 缓存60秒
response.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
}
return response;
})
.build();
三、异步处理:Coroutines是未来,但要注意作用域
3.1 为什么别再用手线程+Handler了
// 过时做法:手动管理线程
new Thread(() -> {
// 网络请求
Handler(Looper.getMainLooper()).post(() -> {
// 更新UI
});
}).start();
问题:
- 忘记更新UI会Crash(Not on main thread)
- 内存泄漏(Thread持有了Activity引用)
- 代码嵌套深,可读性差
3.2 Coroutines的正确使用姿势
// ViewModel里定义协程作用域
private val viewModelScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
// 网络请求
fun loadUsers() {
viewModelScope.launch {
try {
val users = withContext(Dispatchers.IO) {
repository.fetchUsers()
}
_uiState.value = UiState.Success(users)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
// 组件销毁时自动取消所有协程
override fun onCleared() {
super.onCleared()
viewModelScope.cancel()
}
3.3 坑点: SupervisorJob vs Job
// 错误:用Job,一个协程失败会取消所有兄弟协程
val scope = CoroutineScope(Dispatchers.Main + Job())
// 正确:用SupervisorJob,各协程独立运行
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
SupervisorJob是ViewModel推荐的选择,因为网络请求、本地数据库读取、文件下载这几个任务应该独立,一个失败不应该影响其他的。
四、图片加载:Glide vs Coil,选哪个?
4.1 Glide:稳定但体积大
implementation 'com.github.bumptech.glide:glide:4.16.0'
kapt 'com.github.bumptech.glide:compiler:4.16.0'
优点:
- 社区成熟,文档齐全
- 支持动画、过渡效果
- 自动处理Lifecycle
缺点:
- 包体积增加约1MB
- 不支持Kotlin协程(需要额外库)
4.2 Coil:Kotlin首选,轻量级
implementation "io.coil-kt:coil:2.5.0"
implementation "io.coil-kt:coil-gif:2.5.0" // 如果需要GIF
优点:
- 专为Kotlin设计,支持协程
- 包体积增加仅200KB
- API简洁
缺点:
- 功能比Glide少(比如不支持Transition)
- 社区相对较小
4.3 如何选择?
| 场景 | 推荐 |
|---|---|
| 新项目,纯Kotlin | Coil |
| 老项目迁移,Java为主 | Glide |
| 需要复杂动画效果 | Glide |
| 包体积敏感(低端机) | Coil |
| 需要加载GIF/WebP | 两者都支持 |
Coil的使用示例:
// Activity/Fragment里
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
crossfade(true)
}
// ViewModel里(协程)
viewModelScope.launch {
val image = coilImageLoader.load(
ImageRequest.Builder(context)
.data("https://example.com/image.jpg")
.size(800, 800)
.build()
).drawable
}
五、数据库:Room是标准,但要注意迁移
5.1 为什么别再用手写SQLite了
// 过时做法:手写SQL
db.execSQL("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
Cursor cursor = db.rawQuery("SELECT * FROM users WHERE age > ?", new String[]{String.valueOf(18)});
问题:
- 表结构变更时要手动写迁移SQL
- 字段名写错编译不报错,运行时Crash
- 代码冗长,可读性差
5.2 Room的正确姿势
// 1. 定义实体
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int
)
// 2. 定义DAO
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersAboveAge(minAge: Int): List<User>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Delete
suspend fun delete(user: User)
}
// 3. 定义Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// 4. 单例获取
object DatabaseProvider {
val instance: AppDatabase by lazy {
Room.databaseBuilder(
MyApp.context,
AppDatabase::class.java,
"app_database"
).build()
}
}
5.3 数据库迁移:新手最大坑
当你修改了表结构(比如加了字段),Room会报错:
IllegalStateException: A migration from 1 to 2 was required but not found.
错误做法:删除数据库重新建
// 千万别这样!用户数据会丢!
Room.databaseBuilder(...)
.fallbackToDestructiveMigration() // 开发阶段可以用,上线绝对禁止
.build()
正确做法:写Migration
// 假设从version 1升级到version 2,加了email字段
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
}
}
// 在Database builder里添加
Room.databaseBuilder(...)
.addMigrations(MIGRATION_1_2)
.build()
如果迁移复杂(比如重命名表、移动数据),建议用Room.databaseBuilder的callback参数,在迁移前后执行自定义逻辑。
六、依赖注入:Dagger2 vs Hilt,怎么选?
6.1 为什么新手不建议直接上Dagger2
Dagger2配置复杂,光是@Module、@Component、@Provides就能让新手懵半天。而且错误信息不友好,”Could not find the generated …impl”这种错误排查起来很痛苦。
6.2 Hilt:Dagger2的Android简化版
// build.gradle (Project)
plugins {
id 'com.android.application'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
}
Activity里注入:
@HiltAndroidApp
class MyApp : Application() {}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// userRepository已经注入好了
}
}
ViewModel里注入:
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
// 不需要手动创建实例,Hilt自动注入
}
Module定义依赖:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}
6.3 Hilt vs Dagger2对比
| 特性 | Dagger2 | Hilt |
|---|---|---|
| 配置复杂度 | 高 | 低 |
| Android组件支持 | 需手动集成 | 内置 |
| 学习曲线 | 陡峭 | 平缓 |
| 灵活性 | 高 | 中 |
| 推荐场景 | 大型项目、自定义需求多 | 中大型Android项目 |
结论:新项目直接用Hilt,别犹豫。
七、状态管理:StateFlow vs LiveData,别再选错了
7.1 LiveData的坑:内存泄漏和生命周期
// 错误:在Activity里直接观察ViewModel的LiveData
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// 问题:如果ViewModel持有Activity引用,会内存泄漏
viewModel.data.observe(this) { data ->
// 更新UI
}
}
}
LiveData本身不会内存泄漏(因为它感知生命周期),但如果你把LiveData用在非Activity场景(比如后台服务),就会出问题。
7.2 StateFlow:现代Android推荐的方案
class MainViewModel : ViewModel() {
// 用StateFlow管理状态
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
}
// UI层观察
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
}
}
7.3 StateFlow vs LiveData对比
| 特性 | StateFlow | LiveData |
|---|---|---|
| 线程安全 | 是(协程) | 否(需手动切换) |
| 默认值 | 必须提供 | 不需要 |
| 背压处理 | 支持(BufferOverflow) | 不支持 |
| 生命周期感知 | 手动(repeatOnLifecycle) | 自动 |
| 推荐场景 | 新项目、复杂状态管理 | 简单UI状态更新 |
结论:新项目用StateFlow,老项目用LiveData也可以,但别混用。
八、日志库:别再用System.out.println了
8.1 Timber:Android标准日志库
implementation 'com.jakewharton.timber:timber:5.0.1'
基础用法:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
// 上线时只记录错误
Timber.plant(object : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, e: Throwable?) {
if (priority >= Log.WARN) {
CrashReporting.log(priority, tag, message, e)
}
}
})
}
}
}
// 使用
Timber.d("用户登录成功: %s", userId)
Timber.w("网络请求超时")
Timber.e(exception, "请求失败")
