2025最新|用MVVM架构打造你的漫威英雄App:从0到1全流程实战

2025最新|用MVVM架构打造你的漫威英雄App:从0到1全流程实战

【免费下载链接】MarvelHeroes ❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, Room, Repository, Koin) architecture. 【免费下载链接】MarvelHeroes 项目地址: https://gitcode.com/gh_mirrors/ma/MarvelHeroes

你还在为Android架构设计烦恼?还在纠结如何优雅地处理网络请求与本地数据?本文将带你深入剖析MarvelHeroes开源项目,掌握MVVM+Repository模式的精髓,学会用Kotlin Coroutines、Room和Koin构建高质量Android应用。读完本文,你将能够:

  • 理解现代Android应用架构的核心组件与交互流程
  • 实现网络请求与本地数据库的无缝集成
  • 掌握依赖注入在实际项目中的最佳实践
  • 编写可测试、易维护的Android代码

项目概述:什么是MarvelHeroes?

MarvelHeroes是一个基于MVVM架构的Android示范应用,展示了如何使用现代Android技术栈构建高质量应用。该项目通过Repository模式从网络获取数据并与本地数据库集成,完美诠释了"关注点分离"的设计原则。

核心功能

  • 展示漫威英雄列表与详情
  • 实现数据的本地持久化存储
  • 处理网络请求与错误状态
  • 响应式UI更新

技术栈概览

类别技术作用
编程语言Kotlin官方推荐的Android开发语言,提供协程等现代特性
架构模式MVVM分离UI逻辑与业务逻辑,提高可测试性
异步处理Kotlin Coroutines简化异步代码,避免回调地狱
网络请求Retrofit + OkHttp处理REST API请求
本地存储Room提供SQLite抽象层,简化数据库操作
依赖注入Koin管理对象依赖,降低耦合度
响应式编程Flow处理数据流,实现响应式UI

架构深度解析:MVVM + Repository模式

MarvelHeroes采用了清晰的分层架构,遵循单一职责原则,使代码更易于维护和扩展。

整体架构图

mermaid

核心组件详解

1. Model层:数据模型与状态管理

Model层负责定义应用的数据结构和业务逻辑,主要包含以下组件:

数据类

// Poster.kt
data class Poster(
    val id: Long,
    val name: String,
    val realName: String,
    val team: String,
    val firstAppearance: String,
    val createdBy: String,
    val publisher: String,
    val imageUrl: String,
    val bio: String
)

错误处理模型

// PosterErrorResponse.kt
data class PosterErrorResponse(
    val code: Int,
    val message: String
)
2. ViewModel层:连接UI与数据

ViewModel负责为UI准备数据,独立于配置变化,是MVVM架构的核心枢纽。

// MainViewModel.kt
class MainViewModel(
    private val mainRepository: MainRepository
) : DisposableViewModel() {

    private val _posterList = MutableLiveData<List<Poster>>()
    val posterList: LiveData<List<Poster>> = _posterList

    private val _isLoading = MutableLiveData<Boolean>()
    val isLoading: LiveData<Boolean> = _isLoading

    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error

    fun fetchMarvelPosters() {
        _isLoading.value = true
        mainRepository.loadMarvelPosters(
            disposable = compositeDisposable,
            coroutineScope = viewModelScope,
            error = { message ->
                _isLoading.value = false
                _error.value = message
            }
        ).onEach { posters ->
            _isLoading.value = false
            _posterList.value = posters
        }.launchIn(viewModelScope)
    }
}
3. Repository层:数据访问统一接口

Repository层为ViewModel提供统一的数据访问接口,屏蔽了数据来源(网络或本地)的细节。

// MainRepository.kt
class MainRepository constructor(
    private val marvelClient: MarvelClient,
    private val marvelDataSource: ResponseDataSource<List<Poster>>,
    private val posterDao: PosterDao
) : Repository {

    @WorkerThread
    @ExperimentalCoroutinesApi
    fun loadMarvelPosters(
        disposable: CompositeDisposable,
        coroutineScope: CoroutineScope,
        error: (String?) -> Unit
    ) = callbackFlow {
        val posters = posterDao.getPosterList()
        if (posters.isEmpty()) {
            // 从网络获取数据
            marvelClient.fetchMarvelPosters(marvelDataSource, disposable, coroutineScope) { apiResponse ->
                apiResponse
                    .suspendOnSuccess {
                        posterDao.insertPosterList(data)
                        send(data)
                    }
                    .suspendOnError {
                        map(ErrorResponseMapper) { error("[Code: $code]: $message") }
                    }
                    .suspendOnException { error(message()) }
                close()
            }
        } else {
            // 从本地数据库获取数据
            send(posters)
            close()
        }
        awaitClose()
    }.flowOn(Dispatchers.IO)
}
4. View层:用户界面与交互

View层包括Activity、Fragment和布局文件,负责展示数据和处理用户交互。

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MainViewModel
    private val adapter by lazy { PosterAdapter() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 初始化ViewModel
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        
        // 设置RecyclerView
        binding.recyclerView.adapter = adapter
        
        // 观察数据变化
        viewModel.posterList.observe(this) { posters ->
            adapter.submitList(posters)
        }
        
        viewModel.isLoading.observe(this) { isLoading ->
            binding.progressBar.isVisible = isLoading
        }
        
        viewModel.error.observe(this) { error ->
            Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
        }
        
        // 加载数据
        viewModel.fetchMarvelPosters()
    }
}

核心模块实现:从依赖注入到数据流动

1. 依赖注入:用Koin解耦组件

Koin是一个轻量级的依赖注入框架,在MarvelHeroes中被广泛用于解耦组件,提高代码的可测试性。

// ViewModelModule.kt
val viewModelModule = module {
    viewModel { MainViewModel(get()) }
    viewModel { (posterId: Long) -> PosterDetailViewModel(posterId, get()) }
}

// NetworkModule.kt
val networkModule = module {
    single {
        OkHttpClient.Builder()
            .addInterceptor(RequestInterceptor())
            .build()
    }
    
    single {
        Retrofit.Builder()
            .client(get<OkHttpClient>())
            .baseUrl("https://gist.githubusercontent.com/skydoves/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    single { get<Retrofit>().create(MarvelService::class.java) }
    single { MarvelClient(get()) }
    single { ResponseDataSource<List<Poster>>() }
}

2. 网络请求:Retrofit + OkHttp

项目使用Retrofit处理网络请求,结合OkHttp进行拦截器配置。

// MarvelService.kt
interface MarvelService {
    @GET("bc3bdb7286d47de7377a964d9586e3f85/raw/86e5292a6a5323b395c197ed39f4a405778e4fef/marvel.json")
    suspend fun fetchMarvelPosters(): ApiResponse<List<Poster>>
}

// RequestInterceptor.kt
class RequestInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val originalHttpUrl = original.url
        
        val url = originalHttpUrl.newBuilder()
            .build()
            
        val request = original.newBuilder()
            .url(url)
            .build()
            
        return chain.proceed(request)
    }
}

3. 本地存储:Room数据库

Room提供了SQLite的抽象层,简化了本地数据持久化操作。

// AppDatabase.kt
@Database(
    entities = [Poster::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun posterDao(): PosterDao
    
    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        
        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "MarvelHeroes.db"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

// PosterDao.kt
@Dao
interface PosterDao {
    @Query("SELECT * FROM Poster")
    fun getPosterList(): List<Poster>
    
    @Query("SELECT * FROM Poster WHERE id = :id")
    fun getPosterById(id: Long): Poster?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertPosterList(posters: List<Poster>)
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertPoster(poster: Poster)
}

项目实战:快速上手MarvelHeroes

1. 环境准备

开发环境要求
  • Android Studio Hedgehog | 2023.1.1 或更高版本
  • Kotlin 1.9.0 或更高版本
  • Gradle 8.0 或更高版本
  • SDK API Level 21 (Android 5.0) 或更高
源码获取
git clone https://gitcode.com/gh_mirrors/ma/MarvelHeroes.git
cd MarvelHeroes

2. 项目结构解析

MarvelHeroes/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/skydoves/marvelheroes/
│   │   │   │   ├── base/           # 基础类
│   │   │   │   ├── binding/        # 视图绑定相关
│   │   │   │   ├── di/             # 依赖注入模块
│   │   │   │   ├── model/          # 数据模型
│   │   │   │   ├── network/        # 网络相关
│   │   │   │   ├── persistence/    # 本地存储
│   │   │   │   ├── repository/     # 数据仓库
│   │   │   │   └── view/           # 视图层
│   │   │   └── res/                # 资源文件
│   │   └── test/                   # 测试代码

3. 关键功能实现步骤

步骤1:配置依赖注入

在Application类中初始化Koin:

// MarvelHeroesApplication.kt
class MarvelHeroesApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MarvelHeroesApplication)
            modules(
                listOf(
                    appComponent,
                    networkModule,
                    persistenceModule,
                    repositoryModule,
                    viewModelModule
                )
            )
        }
    }
}
步骤2:实现数据模型

定义数据类和转换逻辑:

// Poster.kt
@Entity(tableName = "Poster")
data class Poster(
    @PrimaryKey val id: Long,
    val name: String,
    @ColumnInfo(name = "real_name") val realName: String,
    val team: String,
    @ColumnInfo(name = "first_appearance") val firstAppearance: String,
    @ColumnInfo(name = "created_by") val createdBy: String,
    val publisher: String,
    @ColumnInfo(name = "image_url") val imageUrl: String,
    val bio: String
)
步骤3:创建Repository

实现数据获取与存储逻辑:

// DetailRepository.kt
class DetailRepository constructor(
    private val marvelClient: MarvelClient,
    private val posterDao: PosterDao
) : Repository {

    init {
        Timber.d("Injection DetailRepository")
    }

    @WorkerThread
    suspend fun getPosterById(id: Long) = posterDao.getPosterById(id)
}
步骤4:编写ViewModel

连接Repository与View:

// PosterDetailViewModel.kt
class PosterDetailViewModel(
    private val posterId: Long,
    private val detailRepository: DetailRepository
) : DisposableViewModel() {

    private val _poster = MutableLiveData<Poster?>()
    val poster: LiveData<Poster?> = _poster

    init {
        fetchPosterDetail()
    }

    private fun fetchPosterDetail() {
        viewModelScope.launch {
            _poster.postValue(detailRepository.getPosterById(posterId))
        }
    }
}
步骤5:实现UI界面

创建Activity和布局文件:

// PosterDetailActivity.kt
class PosterDetailActivity : AppCompatActivity() {

    private lateinit var binding: ActivityPosterDetailBinding
    private lateinit var viewModel: PosterDetailViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPosterDetailBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 获取传递的参数
        val posterId = intent.getLongExtra("posterId", 0)
        
        // 初始化ViewModel
        viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return PosterDetailViewModel(posterId, get()) as T
            }
        }).get(PosterDetailViewModel::class.java)
        
        // 观察数据变化
        viewModel.poster.observe(this) { poster ->
            poster?.let { bindPoster(it) }
        }
    }
    
    private fun bindPoster(poster: Poster) {
        binding.poster = poster
        binding.executePendingBindings()
    }
}

测试策略:确保代码质量

MarvelHeroes项目采用了全面的测试策略,包括单元测试和集成测试,确保代码的可靠性和稳定性。

单元测试示例

// MainRepositoryTest.kt
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [24])
class MainRepositoryTest {

    @get:Rule
    val coroutinesTestRule = MainCoroutinesRule()

    private lateinit var mainRepository: MainRepository
    private val mockPosterDao = mock(PosterDao::class.java)
    private val mockMarvelClient = mock(MarvelClient::class.java)
    private val mockResponseDataSource = ResponseDataSource<List<Poster>>()

    @Before
    fun setup() {
        mainRepository = MainRepository(
            mockMarvelClient,
            mockResponseDataSource,
            mockPosterDao
        )
    }

    @Test
    fun loadMarvelPosters_fromNetwork() = runBlocking {
        // Given
        val testPosters = listOf(Poster(1, "Iron Man", "Tony Stark", "Avengers", "1963", "Stan Lee", "Marvel", "", ""))
        `when`(mockPosterDao.getPosterList()).thenReturn(emptyList())
        
        // When
        val result = mainRepository.loadMarvelPosters(CompositeDisposable(), coroutineScope).first()
        
        // Then
        assertEquals(testPosters.size, result.size)
        verify(mockPosterDao).insertPosterList(testPosters)
    }
}

高级技巧与最佳实践

1. 使用Data Binding减少模板代码

<!-- item_poster.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="poster"
            type="com.skydoves.marvelheroes.model.Poster" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/item_poster"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            app:imageUrl="@{poster.imageUrl}"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/item_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{poster.name}"
            app:layout_constraintTop_toBottomOf="@id/item_poster" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

2. 用扩展函数增强代码可读性

// ViewExtensions.kt
fun View.visible() {
    visibility = View.VISIBLE
}

fun View.invisible() {
    visibility = View.INVISIBLE
}

fun View.gone() {
    visibility = View.GONE
}

var View.isVisible: Boolean
    get() = visibility == View.VISIBLE
    set(value) = if (value) visible() else gone()

3. 统一错误处理机制

// ErrorResponseMapper.kt
class ErrorResponseMapper : ApiErrorModelMapper<PosterErrorResponse> {
    override fun map(apiErrorResponse: ApiErrorResponse): PosterErrorResponse {
        return PosterErrorResponse(
            code = apiErrorResponse.code,
            message = apiErrorResponse.message()
        )
    }
}

项目优化与性能提升

1. 图片加载优化

使用Glide进行图片加载与缓存:

// ImageViewExtensions.kt
fun ImageView.loadImage(
    url: String,
    placeHolder: Drawable? = ContextCompat.getDrawable(context, R.drawable.marvel)
) {
    Glide.with(context)
        .load(url)
        .placeholder(placeHolder)
        .into(this)
}

2. 列表性能优化

使用RecyclerView的DiffUtil:

// PosterAdapter.kt
class PosterAdapter : ListAdapter<Poster, PosterViewHolder>(PosterDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PosterViewHolder {
        val binding = ItemPosterBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return PosterViewHolder(binding)
    }

    override fun onBindViewHolder(holder: PosterViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class PosterDiffCallback : DiffUtil.ItemCallback<Poster>() {
        override fun areItemsTheSame(oldItem: Poster, newItem: Poster): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Poster, newItem: Poster): Boolean {
            return oldItem == newItem
        }
    }
}

总结与展望

通过MarvelHeroes项目,我们深入理解了MVVM架构在Android应用开发中的实践。该项目展示了如何将现代Android技术栈有机结合,构建出健壮、可测试、易维护的应用。

核心要点回顾

  1. 架构设计:MVVM+Repository模式实现关注点分离
  2. 数据处理:网络请求与本地存储的无缝集成
  3. 依赖注入:使用Koin管理组件依赖
  4. 响应式编程:用Coroutines和Flow处理异步操作
  5. 代码质量:遵循单一职责原则,提高代码可测试性

进阶方向

  1. 添加单元测试覆盖率:完善ViewModel和Repository的测试
  2. 实现数据刷新机制:添加下拉刷新和分页加载功能
  3. 支持深色模式:实现日间/夜间主题切换
  4. 添加依赖管理:使用versions.gradle统一管理依赖版本

希望本文能帮助你掌握现代Android应用开发的核心技能。如果你觉得这个项目有帮助,请给它一个Star支持作者!同时也欢迎关注我的其他开源项目,获取更多Android开发资源和最佳实践。

最后,附上项目地址:https://gitcode.com/gh_mirrors/ma/MarvelHeroes,快去Clone代码,动手实践吧!

【免费下载链接】MarvelHeroes ❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, Room, Repository, Koin) architecture. 【免费下载链接】MarvelHeroes 项目地址: https://gitcode.com/gh_mirrors/ma/MarvelHeroes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值