这篇文章不是那种”推荐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();

问题:

  1. 忘记更新UI会Crash(Not on main thread)
  2. 内存泄漏(Thread持有了Activity引用)
  3. 代码嵌套深,可读性差

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)});

问题:

  1. 表结构变更时要手动写迁移SQL
  2. 字段名写错编译不报错,运行时Crash
  3. 代码冗长,可读性差

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, "请求失败")

8.2 为什么别直接用