GitHub下载量超100万 Android开源项目精选 从UI框架到网络库MVVM架构完整实战源码

先说个有趣的发现——2026年很多人用手机刷视频、点外卖、聊天,但有多少人想过这些App是怎么”长”出来的?今天咱们就扒开Android开发的神秘面纱,看看那些让开发者直呼”真香”的开源神器们。


一、图片加载界的”三剑客”,你选谁?

Glide:Google亲儿子,快就一个字

Glide是Google官方维护的图片加载库,从2015年发布到现在,Star数早就破了万。它的最大特点是——。不是那种吹牛的快,是真能感受到流畅度的快。

// 基本用法:加载一张网络图片
Glide.with(context)
     .load("https://example.com/image.jpg")
     .placeholder(R.drawable.loading)      // 加载中显示的图
     .error(R.drawable.error)              // 加载失败显示的图
     .into(imageView);

// 进阶用法:设置圆形头像
Glide.with(context)
     .load("https://example.com/avatar.jpg")
     .circleCrop()                         // 自动裁剪成圆形
     .apply(RequestOptions.circleCropTransform())
     .into(profileImageView);

// 加载GIF动图,只需一行代码,连你小学妹发的动图都能放
Glide.with(context)
     .asGif()                              // 告诉Glide这是一个GIF
     .load("https://example.com/cool.gif")
     .into(gifImageView);

Glide最厉害的地方是它会自动根据图片尺寸调整采样率。比如你要在100x100的ImageView里显示一张4000x4000的原图,Glide不会傻乎乎地把整张图加载进内存,而是自动裁成合适的大小。这就好比你去水果店买西瓜,店员不会把整个西瓜摊摆到你面前,而是切好合适的一块递给你——既省空间又新鲜。

Picasso:简洁到让人感动

如果说Glide是全能选手,Picasso就是极简主义者。Square公司出品,语法极其简洁:

Picasso.get()
    .load("https://example.com/photo.jpg")
    .resize(200, 200)                     // 缩放到指定尺寸
    .centerCrop()                         // 居中裁剪
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .into(imageView);

注意一个细节:Picasso用的是单例模式,Picasso.get() 返回一个全局共享的实例。这意味着所有请求共用同一个线程池和缓存,节省资源的同时也让代码更干净。

Fresco:Facebook的”内存管理大师”

Fresco是Facebook专门为Android设计的图片库,它有一个其他库都没有的杀手锏——内存管理

// 在XML中声明
<com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/my_image_view"
    android:layout_width="200dp"
    android:layout_height="200dp"
    fresco:placeholderImage="@drawable/my_placeholder"
    fresco:failureImage="@drawable/my_error"
    fresco:roundAsCircle="true"
    fresco:roundedCornerRadius="4dp" />

// 在代码中加载
SimpleDraweeView draweeView = findViewById(R.id.my_image_view);
draweeView.setImageURI(Uri.parse("https://example.com/photo.jpg"));

Fresco把图片数据存在一个专门叫”Underdrawables”的内存区域,跟Android的常规内存池完全隔离。这就好比你在学校有一个专属储物柜,别人(其他App或系统进程)绝对碰不到你的东西。所以当App内存紧张被系统杀死时,Fresco管理的图片不会跟着一起消失——重新打开App时图片还在。


二、网络请求的王者:Retrofit + OkHttp

Retrofit:让网络请求变成”写代码的艺术”

Retrofit是Square公司开发的HTTP客户端库,2013年发布,至今仍是Android网络请求的”标配”。它的核心思想是——把网络请求变成接口调用

先说说配置,现在主流的做法是用Kotlin协程或者RxJava2/3。这里用Kotlin展示最优雅的方式:

// 1. 定义API接口 —— 像写函数签名一样简单
interface GitHubApi {
    
    @GET("users/{username}")
    suspend fun getUser(@Path("username") username: String): User
    
    @GET("users/{username}/repos")
    suspend fun getUserRepos(
        @Path("username") username: String,
        @Query("sort") sort: String = "stars",  // 可选参数有默认值
        @Query("per_page") perPage: Int = 30    // 每页数量
    ): List<Repo>
    
    @POST("repos/{owner}/{repo}/issues")
    suspend fun createIssue(
        @Path("owner") owner: String,
        @Path("repo") repo: String,
        @Body issue: IssueBody
    ): Issue
}

// 2. 定义数据模型 —— 用Kotlin数据类,简洁又安全
data class User(
    val login: String,
    val avatarUrl: String,
    val name: String?,
    val followers: Int,
    val following: Int
)

data class Repo(
    val name: String,
    val fullName: String,
    val description: String?,
    val stargazersCount: Int,
    val language: String?,
    val fork: Boolean
)

data class IssueBody(
    val title: String,
    val body: String
)
// 3. 创建Retrofit实例 —— 整个App只需创建一次
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.github.com/")      // 基础URL
    .addConverterFactory(GsonConverterFactory.create())  // 用Gson解析JSON
    .build()

val api = retrofit.create(GitHubApi::class.java)
// 4. 在ViewModel中调用 —— 用suspend函数,协程自动处理线程切换
class GitHubViewModel : ViewModel() {
    
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    private val _repos = MutableLiveData<List<Repo>>()
    val repos: LiveData<List<Repo>> = _repos
    
    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading
    
    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error
    
    // 这是关键:suspend函数 + 协程作用域 = 自动从子线程切回主线程
    fun fetchUser(username: String) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            try {
                val result = api.getUser(username)
                _user.value = result
                fetchRepos(username)  // 拿到用户信息后再请求其仓库
            } catch (e: Exception) {
                _error.value = e.message ?: "未知错误"
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    private suspend fun fetchRepos(username: String) {
        val result = api.getUserRepos(username)
        _repos.value = result
    }
}
// 5. 在Activity/Fragment中绑定 —— observe会自动处理生命周期
class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: GitHubViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        viewModel = ViewModelProvider(this)[GitHubViewModel::class.java]
        
        // 监听用户信息
        viewModel.user.observe(this) { user ->
            binding.tvUsername.text = user.login
            Glide.with(this).load(user.avatarUrl).into(binding.ivAvatar)
        }
        
        // 监听仓库列表
        viewModel.repos.observe(this) { repos ->
            binding.recyclerView.adapter = RepoAdapter(repos)
        }
        
        // 监听加载状态
        viewModel.isLoading.observe(this) { loading ->
            binding.progressBar.isVisible = loading
        }
        
        // 监听错误
        viewModel.error.observe(this) { errorMsg ->
            Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
        }
        
        // 绑定按钮点击事件
        binding.btnSearch.setOnClickListener {
            val username = binding.etUsername.text.toString().trim()
            if (username.isNotEmpty()) {
                viewModel.fetchUser(username)
            }
        }
    }
}

OkHttp:Retrofit的”底层引擎”

你可能会问:Retrofit不是已经能发请求了吗,为什么还要OkHttp?这就像问——跑车有发动机了,为什么还要有内燃机?因为Retrofit是高层API,OkHttp是底层引擎

// OkHttp直接使用的场景 —— 比如需要下载大文件或处理流式数据
object OkHttpHelper {
    
    // 创建一个配置好的OkHttpClient —— 全局单例,复用连接
    private val client = OkHttpClient.Builder()
        .connectTimeout(30, TimeUnit.SECONDS)       // 连接超时
        .readTimeout(30, TimeUnit.SECONDS)          // 读取超时
        .writeTimeout(30, TimeUnit.SECONDS)         // 写入超时
        .addInterceptor(LoggingInterceptor())       // 添加日志拦截器
        .addInterceptor(CacheInterceptor())         // 添加缓存拦截器
        .cache(Cache(File(App.context.cacheDir, "http_cache"), 10 * 1024 * 1024)) // 10MB缓存
        .build()
    
    // 发送GET请求
    fun GET(url: String, callback: (String) -> Unit) {
        val request = Request.Builder()
            .url(url)
            .header("Accept", "application/json")
            .build()
        
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback.invoke("请求失败: ${e.message}")
            }
            
            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (it.isSuccessful) {
                        callback.invoke(it.body?.string() ?: "")
                    } else {
                        callback.invoke("HTTP错误: ${it.code}")
                    }
                }
            }
        })
    }
    
    // 上传文件
    fun uploadFile(filePath: String, callback: (Boolean, String) -> Unit) {
        val file = File(filePath)
        val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("file", file.name,
                RequestBody.create(
                    MediaType.parse("application/octet-stream"), 
                    file
                )
            )
            .build()
        
        val request = Request.Builder()
            .url("https://example.com/upload")
            .post(requestBody)
            .build()
        
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback.invoke(false, e.message ?: "上传失败")
            }
            
            override fun onResponse(call: Call, response: Response) {
                callback.invoke(response.isSuccessful, 
                    response.body?.string() ?: "上传成功")
            }
        })
    }
}

拦截器:给网络请求装”望远镜”和”后视镜”

拦截器是OkHttp/Retrofit最强大的功能之一,它让你能在请求发出前和响应返回后做任意操作:

// 日志拦截器 —— 像行车记录仪一样记录所有网络请求
class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        
        // 请求前打印日志
        Log.d("HTTP", ">>> ${request.method} ${request.url}")
        request.headers.forEach { (name, value) ->
            Log.d("HTTP", "    $name: $value")
        }
        
        val startTime = System.currentTimeMillis()
        val response = chain.proceed(request)
        val endTime = System.currentTimeMillis()
        
        // 响应后打印日志
        Log.d("HTTP", "<<< ${response.code} ${response.message} (${endTime - startTime}ms)")
        
        return response
    }
}

// 认证拦截器 —— 自动给所有请求加上Token
class AuthInterceptor(private val tokenProvider: () -> String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val token = tokenProvider()
        
        val authenticatedRequest = originalRequest.newBuilder()
            .header("Authorization", "Bearer $token")
            .header("Content-Type", "application/json")
            .build()
        
        return chain.proceed(authenticatedRequest)
    }
}

// 缓存拦截器 —— 没网也能看缓存的数据
class CacheInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        
        // 有网络时用网络数据,无网络时用缓存
        val response = if (isNetworkAvailable()) {
            chain.proceed(request)
        } else {
            request.newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .build()
                .let { chain.proceed(it) }
        }
        
        // 给响应头加上缓存策略
        return response.newBuilder()
            .header("Cache-Control", "public, max-age=60")
            .removeHeader("Pragma")
            .build()
    }
    
    private fun isNetworkAvailable(): Boolean {
        val connectivityManager = 
            MyApp.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetworkInfo
        return network != null && network.isConnected
    }
}

三、UI组件库:别再重复造轮子了

Material Design Components (MDC):Google的”官方皮肤”

// build.gradle (app)
dependencies {
    implementation 'com.google.android.material:material:1.12.0'
}
<!-- 使用MaterialButton,自动适配主题颜色 -->
<com.google.android.material.button.MaterialButton
    android:id="@+id/btnPrimary"
    android:layout_width="match_parent"
    android:layout_height="56dp"
    android:text="主要操作"
    app:cornerRadius="28dp"           <!-- 圆角 -->
    app:icon="@drawable/ic_favorite"   <!-- 左侧图标 -->
    app:iconGravity="textStart"
    style="@style/Widget.MaterialComponents.Button.Compact" />  <!-- 紧凑样式 -->

<!-- 使用CardView实现卡片布局 -->
<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="12dp"
    app:cardElevation="4dp"
    app:strokeWidth="1dp"
    app:strokeColor="#E0E0E0">
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">
        
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:scaleType="centerCrop"
            android:src="@drawable/sample" />
            
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="卡片标题"
            android:textSize="18sp"
            android:textStyle="bold"
            android:marginTop="12dp" />
            
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="这是卡片的内容描述,可以放一段简短的文字说明。"
            android:textSize="14sp"
            android:textColor="#666666"
            android:marginTop="8dp" />
            
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

<!-- Bottom Sheet —— 从底部弹出的面板 -->
<com.google.android.material.bottomsheet.BottomSheetDialogFragment
    android:id="@+id/bottomSheet"
    android:name="com.example.BottomSheetFragment" />

Coil:Kotlin协程时代的图片加载新星

Coil是近几年崛起的图片加载库,完全用Kotlin编写,天然支持协程和Flow,跟Jetpack全家桶配合得天衣无缝:

// 依赖
// implementation("io.coil-kt:coil:2.6.0")
// implementation("io.coil-kt:coil-gif:2.6.0")

// 在ViewModel中加载图片 —— 直接返回ImageLoader需要的数据
class ImageViewModel : ViewModel() {
    
    private val _imageState = MutableStateFlow<ImageState>(ImageState.Loading)
    val imageState: StateFlow<ImageState> = _imageState
    
    fun loadImage(url: String) {
        viewModelScope.launch {
            try {
                val imageLoader = ImageLoader.Builder(App.context)
                    .components {
                        add(GifDecoder.Factory())  // 支持GIF
                    }
                    .build()
                
                val result = imageLoader.execute(
                    ImageRequest.Builder(App.context)
                        .data(url)
                        .size(Size(800, 600))           // 指定目标尺寸
                        .crossfade(true)                 // 淡入动画
                        .listener(
                            onError = { e -> 
                                _imageState.value = ImageState.Error(e.errorBody) 
                            },
                            onSuccess = { result -> 
                                _imageState.value = ImageState.Success(result) 
                            }
                        )
                        .build()
                )
            } catch (e: Exception) {
                _imageState.value = ImageState.Error(e.message)
            }
        }
    }
}

sealed class ImageState {
    object Loading : ImageState()
    data class Success(val result: ImageResult) : ImageState()
    data class Error(val message: String?) : ImageState()
}

四、MVVM架构:让代码”有纪律”地运行

架构分层:把责任分清楚

很多初学者写代码喜欢把所有逻辑都堆在Activity里,就像一个公司里所有员工都听老板一个人指挥——老板累死,员工闲死,公司还乱成一团。MVVM的核心思想是把职责分开

┌─────────────────────────────────────────────┐
│              View (Activity/Fragment)        │  ← 只管显示,不碰数据
│  绑定UI元素 + 监听用户操作 + 观察ViewModel   │
├─────────────────────────────────────────────┤
│           ViewModel (业务逻辑层)              │  ← 只管业务逻辑,不碰UI
│  持有LiveData/StateFlow + 处理业务逻辑        │
├─────────────────────────────────────────────┤
│         Repository (数据管理层)              │  ← 统一数据入口
│  决定数据来源(网络 or 本地缓存)             │
├─────────────────────────────────────────────┤
│  DataSource (网络 + 数据库 + 共享偏好)        │  ← 具体数据实现
└─────────────────────────────────────────────┘

完整实战:打造一个新闻App的架构骨架

// ===== 第一层:数据模型 =====
data class NewsArticle(
    val id: String,
    val title: String,
    val summary: String,
    val content: String,
    val author: String,
    val publishTime: Long,
    val coverImageUrl: String,
    val category: String,
    val tags: List<String>
)

data class ApiResponse<T>(
    val code: Int,
    val message: String,
    val data: T?
)

// ===== 第二层:数据源 =====
interface NewsDataSource {
    suspend fun getLatestNews(page: Int, pageSize: Int): ApiResponse<List<NewsArticle>>
    suspend fun getNewsDetail(articleId: String): ApiResponse<NewsArticle>
    suspend fun searchNews(keyword: String, page: Int): ApiResponse<List<NewsArticle>>
    suspend fun getCategoryList(): ApiResponse<List<String>>
}

// 网络数据源实现
class NewsRemoteDataSource(
    private val api: NewsApi
) : NewsDataSource {
    
    override suspend fun getLatestNews(page: Int, pageSize: Int): ApiResponse<List<NewsArticle>> {
        return api.getLatestNews(page = page, size = pageSize)
    }
    
    override suspend fun getNewsDetail(articleId: String): ApiResponse<NewsArticle> {
        return api.getArticleDetail(articleId = articleId)
    }
    
    override suspend fun searchNews(keyword: String, page: Int): ApiResponse<List<NewsArticle>> {
        return api.searchNews(keyword = keyword, page = page)
    }
    
    override suspend fun getCategoryList(): ApiResponse<List<String>> {
        return api.getCategoryList()
    }
}

// 本地缓存数据源(用Room数据库)
class NewsLocalDataSource(
    private val newsDao: NewsDao
) : NewsDataSource {
    
    // 先查缓存,缓存没有再返回空 —— 让Repository决定要不要请求网络
    override suspend fun getLatestNews(page: Int, pageSize: Int): ApiResponse<List<NewsArticle>> {
        val cached = newsDao.getNewsByPage(page, pageSize)
        if (cached.isNotEmpty()) {
            return ApiResponse(200, "success", cached)
        }
        return ApiResponse(404, "no cache", emptyList())
    }
    
    // ... 其他方法类似
}

// ===== 第三层:Repository(数据管理层)=====
class NewsRepository(
    private val remoteDataSource: NewsRemoteDataSource,
    private val localDataSource: NewsLocalDataSource,
    private val newsDao: NewsDao
) {
    
    // 带缓存策略的获取新闻列表
    suspend fun getLatestNews(page: Int, pageSize: Int): Flow<Result<List<NewsArticle>>> = flow {
        emit(Result_loading)  // 先发出加载状态
        
        try {
            // 1. 先尝试从网络获取
            val response = remoteDataSource.getLatestNews(page, pageSize)
            if (response.code == 200 && response.data != null) {
                // 2. 同时写入本地缓存
                newsDao.insertAll(response.data)
                emit(Result.success(response.data))
                return@flow
            }
        } catch (e: Exception) {
            // 3. 网络失败,尝试从本地读取
            try {
                val cached = localDataSource.getLatestNews(page, pageSize)
                if (cached.code == 200 && cached.data != null) {
                    emit(Result.success(cached.data))
                    return@flow
                }
            } catch (localError: Exception) {
                emit(Result.failure(localError))
                return@flow
            }
        }
        emit(Result.failure(Exception("获取新闻失败")))
    }
    
    // 搜索新闻
    suspend fun searchNews(keyword: String, page: Int): Flow<Result<List<NewsArticle>>> = flow {
        val response = remoteDataSource.searchNews(keyword, page)
        if (response.code == 200) {
            emit(Result.success(response.data ?: emptyList()))
        } else {
            emit(Result.failure(Exception(response.message)))
        }
    }
}

// ===== 第四层:ViewModel(业务逻辑层)=====
class NewsViewModel(
    private val repository: NewsRepository
) : ViewModel() {
    
    // 使用StateFlow —— 比LiveData更现代,支持更多操作
    private val _newsList = MutableStateFlow<Resource<List<NewsArticle>>>(Resource.Loading)
    val newsList: StateFlow<Resource<List<NewsArticle>>> = _newsList
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading
    
    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error
    
    private val _selectedCategory = MutableStateFlow("全部")
    val selectedCategory: StateFlow<String> = _selectedCategory
    
    // 获取新闻列表(支持分页)
    fun loadNews(page: Int = 1) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null
            
            repository.getLatestNews(page, 20)
                .catch { e ->
                    _error.value = e.message
                    _isLoading.value = false
                }
                .collect { result ->
                    when (result) {
                        is Result.Success -> {
                            _newsList.value = Resource.Success(result.data)
                            _isLoading.value = false
                        }
                        is Result.Failure -> {
                            _error.value = result.exception.message
                            _isLoading.value = false
                        }
                        is Result.Loading -> {
                            _newsList.value = Resource.Loading
                        }
                    }
                }
        }
    }
    
    // 搜索
    fun search(keyword: String) {
        viewModelScope.launch {
            repository.searchNews(keyword, 1)
                .collect { result ->
                    when (result) {
                        is Result.Success -> _newsList.value = Resource.Success(result.data)
                        is Result.Failure -> _error.value = result.exception.message
                        else -> {}
                    }
                }
        }
    }
    
    // 切换分类
    fun changeCategory(category: String) {
        _selectedCategory.value = category
        loadNews()
    }
    
    companion object {
        // 工厂方法 —— ViewModelProvider需要用
        fun provideFactory(repository: NewsRepository): ViewModelProvider.Factory = 
            object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    return NewsViewModel(repository) as T
                }
            }
    }
}

// 通用的结果包装类
sealed class Resource<out T> {
    data object Loading : Resource<Nothing>()
    data class Success<out T>(val data: T) : Resource<T>()
    data class Error(val message: String) : Resource<Nothing>()
}
// ===== 第五层:DI注入(Koin)=====
// 依赖注入让代码更灵活,方便测试

val appModule = module {
    // Retrofit实例 —— 单例
    single { 
        Retrofit.Builder()
            .baseUrl("https://api.news.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(NewsApi::class.java)
    }
    
    // 数据源 —— 单例
    single { NewsRemoteDataSource(get()) }
    single { NewsLocalDataSource(get()) }
    
    // Repository —— 单例
    single { NewsRepository(get(), get(), get()) }
    
    // ViewModel —— 用工厂创建,传入Repository
    viewModel { NewsViewModel(get()) }
}

// 在Application中初始化
class NewsApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@NewsApp)
            modules(appModule)
        }
    }
}
// ===== 第六层:View层(Fragment)=====
class NewsListFragment : Fragment() {
    
    private var _binding: FragmentNewsListBinding? = null
    private val binding get() = _binding!!
    
    private val viewModel: NewsViewModel by viewModels()  // Koin自动注入
    private val adapter = NewsAdapter()
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentNewsListBinding.inflate(inflater, container, false)
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        setupRecyclerView()
        setupSwipeRefresh()
        observeViewModel()
        
        // 初始加载
        viewModel.loadNews()
    }
    
    private fun setupRecyclerView() {
        binding.recyclerView.apply {
            adapter = this@NewsListFragment.adapter
            layoutManager = LinearLayoutManager(context)
            addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL))
            
            // 加载更多
            addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    val layoutManager = recyclerView.layoutManager as? LinearLayoutManager
                        ?: return
                    if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) {
                        // 到底部了,加载更多
                        val currentPage = (viewModel.newsList.value as? Resource.Success<*>)
                            ?.data?.size?.div(20)?.plus(1) ?: 1
                        viewModel.loadNews(currentPage)
                    }
                }
            })
        }
    }
    
    private fun setupSwipeRefresh() {
        binding.swipeRefresh.setOnRefreshListener {
            viewModel.loadNews(1)  // 刷新第一页
        }
    }
    
    private fun observeViewModel() {
        // 观察新闻列表 —— StateFlow的collectLatest会在Fragment销毁时自动取消
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.newsList.collect { resource ->
                    when (resource) {
                        is Resource.Loading -> {
                            binding.progressBar.isVisible = true
                        }
                        is Resource.Success -> {
                            binding.progressBar.isVisible = false
                            binding.swipeRefresh.isRefreshing = false
                            if (resource.data.isEmpty()) {
                                binding.tvEmpty.isVisible = true
                            } else {
                                binding.tvEmpty.isVisible = false
                                // 判断是刷新还是加载更多
                                val isFirstLoad = (viewModel.newsList.value as? Resource.Success<*>)
                                    ?.data?.isEmpty() == true
                                if (isFirstLoad) {
                                    adapter.submitList(resource.data)
                                } else {
                                    adapter.loadMore(resource.data)
                                }
                            }
                        }
                        is Resource.Error -> {
                            binding.progressBar.isVisible = false
                            binding.tvError.isVisible = true
                            binding.tvError.text = resource.message
                        }
                    }
                }
            }
        }
        
        // 观察错误信息 —— 用toast展示
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.error.collect { errorMsg ->
                    errorMsg?.let {
                        Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

适配器:RecyclerView的”灵魂伴侣”

class NewsAdapter : ListAdapter<NewsArticle, NewsAdapter.NewsViewHolder>(DiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
        val binding = ItemNewsBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
        return NewsViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    // 加载更多用的方法
    fun loadMore(newItems: List<NewsArticle>) {
        val currentList = currentList.toMutableList()
        currentList.addAll(newItems)
        submitList(currentList)
    }
    
    class NewsViewHolder(
        private val binding: ItemNewsBinding
    ) : RecyclerView.ViewHolder(binding.root) {
        
        fun bind(article: NewsArticle) {
            binding.apply {
                tvTitle.text = article.title
                tvSummary.text = article.summary
                tvAuthor.text = article.author
                tvTime.text = formatTime(article.publishTime)
                tvCategory.text = article.category
                
                Glide.with(itemView.context)
                    .load(article.coverImageUrl)
                    .placeholder(R.drawable.placeholder)
                    .error(R.drawable.error)
                    .into(ivCover)
                
                // 标签展示
                tvTags.text = article.tags.take(3).joinToString(" ")
            }
        }
        
        private fun formatTime(timestamp: Long): String {
            val date = Date(timestamp)
            val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
            return format.format(date)
        }
    }
    
    class DiffCallback : DiffUtil.ItemCallback<NewsArticle>() {
        override fun areItemsTheSame(oldItem: NewsArticle, newItem: NewsArticle) =
            oldItem.id == newItem.id
        
        override fun areContentsTheSame(oldItem: NewsArticle, newItem: NewsArticle) =
            oldItem == newItem
    }
}

五、这些项目的GitHub地址和Star数(2026年最新数据)

项目 GitHub地址 Star数 简介
Retrofit square/retrofit ~35,000 类型安全的HTTP客户端
OkHttp square/okhttp ~29,000 高效的HTTP客户端
Glide bumptech/glide ~31,000 图片加载库
Coil coil-kt/coil ~11,000 Kotlin协程图片加载
MDC Android material-components/material-components-android ~22,000 Material Design组件库
PagedList googlearchive/paging ~5,000 分页加载库
Koin InsertKoinIO/koin ~12,000 轻量级Kotlin DI框架

六、新手入门建议:别贪多,先扎实基础

很多刚入门的朋友一上来就堆各种框架,结果项目跑不起来还找不到原因。我的建议是:

  1. 先搞懂基础:Java/Kotlin基础语法、Android四大组件、Layout布局——这些是地基,地基不牢地动山摇。
  2. 学一个网络库就够了:Retrofit + OkHttp的组合是业界标准,学会了走遍天下都不怕。
  3. MVVM不是银弹:简单的App用MVC也能写得很清楚,MVVM适合中大型项目。
  4. 自己动手写一遍:看别人写的代码永远不如自己写一遍印象深刻。上面给的代码你可以直接复制到Android Studio里跑起来看效果。
  5. 调试技巧:学会用Logcat看日志、用Android Studio的Profiler看内存、用Network Inspector看网络请求——这三样是排查问题的基本功。

最后说句心里话:Android开发这行,技术更新确实快,但核心思想从来没变过——把复杂的逻辑拆分成简单的模块,让每个模块只做一件事,然后把它们有机地组合在一起。不管你是刚入门的大学生,还是想转行的程序员,只要按照这个思路一步步来,总能写出优雅又稳定的代码。

加油吧,未来的Android开发者!🚀