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框架 |
六、新手入门建议:别贪多,先扎实基础
很多刚入门的朋友一上来就堆各种框架,结果项目跑不起来还找不到原因。我的建议是:
- 先搞懂基础:Java/Kotlin基础语法、Android四大组件、Layout布局——这些是地基,地基不牢地动山摇。
- 学一个网络库就够了:Retrofit + OkHttp的组合是业界标准,学会了走遍天下都不怕。
- MVVM不是银弹:简单的App用MVC也能写得很清楚,MVVM适合中大型项目。
- 自己动手写一遍:看别人写的代码永远不如自己写一遍印象深刻。上面给的代码你可以直接复制到Android Studio里跑起来看效果。
- 调试技巧:学会用Logcat看日志、用Android Studio的Profiler看内存、用Network Inspector看网络请求——这三样是排查问题的基本功。
最后说句心里话:Android开发这行,技术更新确实快,但核心思想从来没变过——把复杂的逻辑拆分成简单的模块,让每个模块只做一件事,然后把它们有机地组合在一起。不管你是刚入门的大学生,还是想转行的程序员,只要按照这个思路一步步来,总能写出优雅又稳定的代码。
加油吧,未来的Android开发者!🚀
