从0到1掌握WanAndroid:Jetpack MVVM架构最佳实践全解析

从0到1掌握WanAndroid:Jetpack MVVM架构最佳实践全解析

【免费下载链接】wanandroid Jetpack MVVM For Wanandroid 最佳实践 ! 【免费下载链接】wanandroid 项目地址: https://gitcode.com/gh_mirrors/wan/wanandroid

你是否还在为Android架构设计烦恼?面对层出不穷的框架和模式感到无从下手?本文将带你深入剖析WanAndroid开源项目,通过实战案例详解Jetpack MVVM架构的最佳实践,让你彻底掌握现代Android应用开发的精髓。

读完本文,你将获得:

  • 清晰理解Jetpack MVVM架构在实际项目中的应用
  • 掌握Kotlin + Coroutines + Retrofit的网络请求最佳实践
  • 学会使用依赖注入优化项目结构
  • 了解Compose UI与ViewModel的完美结合
  • 掌握数据状态管理与UI交互的设计模式

项目概述:WanAndroid是什么?

WanAndroid是一个基于Jetpack MVVM架构的Android应用,旨在为开发者提供一个架构清晰、代码规范的开源项目示例。该项目采用Kotlin语言开发,整合了现代Android开发的主流技术栈,包括ViewModel、LiveData、Coroutines、Retrofit、Room、Koin等,是学习Android架构设计的绝佳案例。

项目地址:https://gitcode.com/gh_mirrors/wan/wanandroid

技术栈解析:现代Android开发的主流技术

WanAndroid项目整合了当前Android开发的前沿技术,形成了一套完整的技术体系。以下是项目主要技术栈的组成:

核心技术栈概览

技术类别主要技术作用
架构组件ViewModel、LiveData数据管理与UI交互
异步处理Kotlin Coroutines简化异步操作
网络请求Retrofit、OkHttpAPI数据获取
依赖注入Koin组件解耦与依赖管理
UI开发Jetpack Compose声明式UI构建
数据存储DataStore、Preference本地数据持久化
图片加载Coil高效图片加载

架构演进:从MVP到MVVM的转变

WanAndroid项目最初采用MVP(Model-View-Presenter)架构,经过多次迭代后,最终转型为基于Jetpack组件的MVVM架构。这一转变带来了以下优势:

  • 数据驱动UI:通过LiveData实现数据变化自动更新UI
  • 生命周期感知:ViewModel与LifecycleOwner协同工作,避免内存泄漏
  • 减少模板代码:相比MVP,大幅减少了接口定义和数据传递的模板代码
  • 更好的可测试性:业务逻辑集中在ViewModel,便于单元测试

架构设计:深入理解项目架构

WanAndroid采用清晰的分层架构,严格遵循单一职责原则,使各组件之间低耦合、高内聚。

整体架构图

mermaid

分层详解

1. UI层(Presentation Layer)

UI层负责用户界面的展示和用户交互,主要由Activity、Fragment和Compose组件构成。在WanAndroid项目中,UI层采用了两种实现方式:

  • 传统View系统:基于XML布局的Activity和Fragment
  • Jetpack Compose:采用声明式UI构建的界面组件

ComposeMainActivity为例,它是项目的主Activity,负责加载Compose UI:

class ComposeMainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WanTheme {
                val navController = rememberNavController()
                val viewModel: MainViewModel = viewModel()
                
                Scaffold(
                    bottomBar = {
                        // 底部导航栏
                        BottomNavigationBar(navController)
                    }
                ) { innerPadding ->
                    // 导航宿主
                    NavHost(
                        navController = navController,
                        startDestination = Screens.Home.route,
                        modifier = Modifier.padding(innerPadding)
                    ) {
                        // 各个页面的导航配置
                        homeGraph(navController)
                        projectGraph(navController)
                        systemGraph(navController)
                        squareGraph(navController)
                        profileGraph(navController)
                    }
                }
            }
        }
    }
}
2. ViewModel层

ViewModel层负责管理与UI相关的数据,遵循生命周期感知型存储的设计理念。WanAndroid项目中,所有ViewModel都继承自BaseViewModel

open class BaseViewModel : ViewModel() {
    // 加载状态
    open class UiState<T>(
        val isLoading: Boolean = false,
        val isRefresh: Boolean = false,
        val isSuccess: T? = null,
        val isError: String? = null
    )
    
    // 基础UI模型
    open class BaseUiModel<T>(
        var showLoading: Boolean = false,
        var showError: String? = null,
        var showSuccess: T? = null,
        var showEnd: Boolean = false, // 加载更多
        var isRefresh: Boolean = false // 刷新
    )

    // 异常处理
    val mException: MutableLiveData<Throwable> = MutableLiveData()
    
    // UI线程启动协程
    fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
        viewModelScope.launch { block() }
    }

    // IO线程启动协程
    suspend fun <T> launchOnIO(block: suspend CoroutineScope.() -> T) {
        withContext(Dispatchers.IO) {
            block
        }
    }
}

BaseViewModel提供了以下核心功能:

  • 统一的UI状态管理(加载中、成功、失败等)
  • 协程作用域管理,避免内存泄漏
  • 异常处理机制

LoginViewModel为例,展示ViewModel如何与Repository交互:

class LoginViewModel(private val repository: LoginRepository) : BaseViewModel() {
    // 登录状态
    private val _loginState = MutableLiveData<UiState<User>>()
    val loginState: LiveData<UiState<User>> = _loginState
    
    // 登录方法
    fun login(username: String, password: String) {
        _loginState.value = UiState(isLoading = true)
        
        launchOnUI {
            try {
                val result = repository.login(username, password)
                _loginState.value = UiState(isSuccess = result)
            } catch (e: Exception) {
                _loginState.value = UiState(isError = e.message)
                mException.value = e
            }
        }
    }
    
    // 注册方法
    fun register(username: String, password: String, repassword: String) {
        // 实现类似登录的逻辑
    }
}
3. Repository层

Repository层作为数据访问的统一入口,负责协调本地数据和远程数据的获取与存储。它对ViewModel层提供统一的数据接口,屏蔽了数据来源的细节。

在WanAndroid中,BaseRepository是所有Repository的基类:

open class BaseRepository {
    // 处理网络请求结果
    suspend fun <T> executeRequest(block: suspend () -> WanResponse<T>): T {
        val response = block()
        if (response.errorCode == 0) {
            return response.data ?: throw Exception("数据为空")
        } else {
            throw Exception(response.errorMsg)
        }
    }
}

HomeRepository为例,展示Repository如何工作:

class HomeRepository(private val service: WanService) : BaseRepository() {
    // 获取首页文章列表
    suspend fun getHomeArticles(page: Int): ArticleList {
        return executeRequest { service.getHomeArticles(page) }
    }
    
    // 获取轮播图数据
    suspend fun getBanners(): List<Banner> {
        return executeRequest { service.getBanner() }
    }
}
4. DataSource层

DataSource层负责实际的数据获取和存储,分为远程数据源和本地数据源:

  • 远程数据源:通过Retrofit与后端API交互
  • 本地数据源:通过Room或DataStore进行本地数据存储

WanAndroid使用WanService定义所有API接口:

interface WanService {
    companion object {
        const val BASE_URL = "https://www.wanandroid.com"
    }

    // 获取首页文章列表
    @GET("/article/list/{page}/json")
    suspend fun getHomeArticles(@Path("page") page: Int): WanResponse<ArticleList>

    // 获取轮播图
    @GET("/banner/json")
    suspend fun getBanner(): WanResponse<List<Banner>>

    // 登录
    @FormUrlEncoded
    @POST("/user/login")
    suspend fun login(
        @Field("username") userName: String, 
        @Field("password") passWord: String
    ): WanResponse<User>
    
    // 更多API接口...
}

网络请求:Retrofit + Coroutines最佳实践

网络请求是大多数应用的核心功能,WanAndroid项目采用Retrofit + Kotlin Coroutines实现高效、简洁的网络请求。

网络架构

mermaid

实现细节

  1. Retrofit客户端封装

BaseRetrofitClient提供了Retrofit的基础配置:

open class BaseRetrofitClient {
    // 创建Retrofit实例
    private val retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    // OkHttp客户端配置
    private val okHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(LoggingInterceptor())
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .build()
    }
    
    // 创建服务实例
    fun <T> getService(clazz: Class<T>): T = retrofit.create(clazz)
    
    // 抽象属性,子类实现baseUrl
    protected abstract val baseUrl: String
}

WanRetrofitClient继承自BaseRetrofitClient,实现具体的API服务:

object WanRetrofitClient : BaseRetrofitClient() {
    override val baseUrl: String
        get() = WanService.BASE_URL
    
    val service: WanService by lazy { getService(WanService::class.java) }
}
  1. API响应处理

项目定义了统一的API响应格式WanResponse

class WanResponse<T> {
    var errorCode: Int = 0
    var errorMsg: String = ""
    var data: T? = null
    
    // 判断请求是否成功
    fun isSuccess() = errorCode == 0
}

BaseRepository中统一处理API响应:

open class BaseRepository {
    suspend fun <T> executeRequest(block: suspend () -> WanResponse<T>): T {
        val response = block()
        if (response.errorCode == 0) {
            return response.data ?: throw Exception("数据为空")
        } else {
            throw Exception(response.errorMsg)
        }
    }
}
  1. 协程作用域管理

在ViewModel中使用viewModelScope管理协程生命周期:

// BaseViewModel中的协程启动方法
fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
    viewModelScope.launch { block() }
}

// 在具体ViewModel中使用
class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
    private val _articles = MutableLiveData<BaseUiModel<ArticleList>>()
    val articles: LiveData<BaseUiModel<ArticleList>> = _articles
    
    fun getHomeArticles(page: Int) {
        _articles.value = BaseUiModel(showLoading = true)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(page)
                _articles.value = BaseUiModel(showSuccess = result)
            } catch (e: Exception) {
                _articles.value = BaseUiModel(showError = e.message)
                mException.value = e
            }
        }
    }
}

依赖注入:使用Koin优化组件依赖

WanAndroid项目使用Koin作为依赖注入框架,有效解耦组件之间的依赖关系,提高代码的可测试性和可维护性。

依赖注入配置

项目通过AppModule定义依赖注入模块:

class AppModule {
    val appModule = module {
        // 单例提供WanService
        single { WanRetrofitClient.service }
        
        // 仓库依赖
        single { HomeRepository(get()) }
        single { LoginRepository(get()) }
        single { SquareRepository(get()) }
        single { CollectRepository(get()) }
        // 更多仓库...
        
        // ViewModel依赖
        viewModel { HomeViewModel(get()) }
        viewModel { LoginViewModel(get()) }
        viewModel { SquareViewModel(get()) }
        viewModel { SystemViewModel(get()) }
        // 更多ViewModel...
        
        // 协程调度器
        single { CoroutinesDispatcherProvider() }
    }
}

Application类中初始化Koin:

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // 初始化Koin
        startKoin {
            androidContext(this@App)
            modules(AppModule().appModule)
        }
    }
}

在ViewModel中使用依赖注入

通过构造函数注入依赖:

// 声明需要注入的依赖
class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
    // ViewModel实现...
}

// 在Koin模块中配置
viewModel { HomeViewModel(get()) }

// 在Activity或Fragment中获取ViewModel
val viewModel: HomeViewModel by viewModel()

UI开发:Jetpack Compose的应用

WanAndroid项目采用Jetpack Compose构建现代化UI,实现了声明式、响应式的界面开发。

Compose页面结构

HomePage为例,展示Compose页面的基本结构:

@Composable
fun HomePage(viewModel: HomeViewModel = viewModel()) {
    val uiState by viewModel.articles.observeAsState()
    val banners by viewModel.banners.observeAsState()
    
    // 处理加载状态
    when {
        uiState?.showLoading == true && uiState?.isRefresh == false -> {
            LoadingView()
        }
        uiState?.showError != null -> {
            ErrorView(message = uiState?.showError ?: "加载失败") {
                viewModel.getHomeArticles(0)
            }
        }
        uiState?.showSuccess != null -> {
            HomeContent(
                articles = uiState?.showSuccess?.datas ?: emptyList(),
                banners = banners ?: emptyList(),
                onLoadMore = { viewModel.loadMore() }
            )
        }
    }
}

@Composable
fun HomeContent(
    articles: List<Article>,
    banners: List<Banner>,
    onLoadMore: () -> Unit
) {
    LazyColumn {
        // 轮播图
        item {
            BannerView(banners = banners)
        }
        
        // 文章列表
        items(articles) { article ->
            ArticleItem(article = article)
        }
        
        // 加载更多
        item {
            LoadMoreView(hasMore = true, onLoadMore = onLoadMore)
        }
    }
}

自定义Compose组件

项目中封装了多个可复用的Compose组件,如TitleBarArticleItem等:

@Composable
fun TitleBar(
    title: String,
    leftIcon: ImageVector? = null,
    onLeftClick: (() -> Unit)? = null,
    rightIcon: ImageVector? = null,
    onRightClick: (() -> Unit)? = null
) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(56.dp)
            .background(Color.White)
    ) {
        // 左侧图标
        if (leftIcon != null && onLeftClick != null) {
            IconButton(
                onClick = onLeftClick,
                modifier = Modifier.align(Alignment.CenterStart)
            ) {
                Icon(imageVector = leftIcon, contentDescription = null)
            }
        }
        
        // 标题
        Text(
            text = title,
            fontSize = 18.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.align(Alignment.Center)
        )
        
        // 右侧图标
        if (rightIcon != null && onRightClick != null) {
            IconButton(
                onClick = onRightClick,
                modifier = Modifier.align(Alignment.CenterEnd)
            ) {
                Icon(imageVector = rightIcon, contentDescription = null)
            }
        }
    }
}

数据状态管理:UI与数据的完美同步

WanAndroid项目采用基于ViewModel + LiveData的数据状态管理方案,确保UI与数据的一致性。

统一UI状态定义

BaseViewModel中定义了统一的UI状态模型:

open class UiState<T>(
    val isLoading: Boolean = false,
    val isRefresh: Boolean = false,
    val isSuccess: T? = null,
    val isError: String? = null
)

open class BaseUiModel<T>(
    var showLoading: Boolean = false,
    var showError: String? = null,
    var showSuccess: T? = null,
    var showEnd: Boolean = false, // 加载更多
    var isRefresh: Boolean = false // 刷新
)
  • UiState:适用于一次性加载的数据
  • BaseUiModel:适用于列表数据,包含加载更多、刷新等状态

状态流转示例

以文章列表加载为例,展示完整的状态流转过程:

class HomeViewModel(private val repository: HomeRepository) : BaseViewModel() {
    private val _articles = MutableLiveData<BaseUiModel<ArticleList>>()
    val articles: LiveData<BaseUiModel<ArticleList>> = _articles
    
    private var currentPage = 0
    
    // 初始加载
    fun getHomeArticles() {
        currentPage = 0
        _articles.value = BaseUiModel(showLoading = true)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(currentPage)
                _articles.value = BaseUiModel(showSuccess = result)
            } catch (e: Exception) {
                _articles.value = BaseUiModel(showError = e.message)
            }
        }
    }
    
    // 加载更多
    fun loadMore() {
        currentPage++
        _articles.value = _articles.value?.copy(showLoading = true, isRefresh = false)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(currentPage)
                val oldData = _articles.value?.showSuccess?.datas ?: emptyList()
                val newData = oldData + result.datas
                val newArticleList = result.copy(datas = newData)
                
                // 判断是否还有更多数据
                val hasMore = currentPage < result.pageCount - 1
                
                _articles.value = BaseUiModel(
                    showSuccess = newArticleList,
                    showEnd = !hasMore
                )
            } catch (e: Exception) {
                currentPage-- // 加载失败,回退页码
                _articles.value = _articles.value?.copy(showError = e.message)
            }
        }
    }
    
    // 下拉刷新
    fun refresh() {
        currentPage = 0
        _articles.value = _articles.value?.copy(isRefresh = true)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(currentPage)
                _articles.value = BaseUiModel(showSuccess = result, isRefresh = false)
            } catch (e: Exception) {
                _articles.value = _articles.value?.copy(
                    showError = e.message, 
                    isRefresh = false
                )
            }
        }
    }
}

UI状态观察与渲染

在Compose中观察ViewModel状态并渲染UI:

@Composable
fun HomePage(viewModel: HomeViewModel = viewModel()) {
    val uiState by viewModel.articles.observeAsState()
    
    Scaffold(
        topBar = { TitleBar(title = "首页") }
    ) { padding ->
        Box(modifier = Modifier.padding(padding)) {
            when {
                uiState?.showLoading == true && !uiState!!.isRefresh -> {
                    // 初始加载中
                    Center { CircularProgressIndicator() }
                }
                uiState?.showError != null -> {
                    // 加载失败
                    ErrorView(
                        message = uiState?.showError ?: "加载失败",
                        onRetry = { viewModel.getHomeArticles() }
                    )
                }
                uiState?.showSuccess != null -> {
                    // 成功加载数据
                    val pullRefreshState = rememberPullRefreshState(
                        refreshing = uiState?.isRefresh == true,
                        onRefresh = { viewModel.refresh() }
                    )
                    
                    PullRefresh(
                        state = pullRefreshState,
                        refreshing = uiState?.isRefresh == true
                    ) {
                        ArticleList(
                            articles = uiState?.showSuccess?.datas ?: emptyList(),
                            onLoadMore = { if (!uiState?.showEnd!!) viewModel.loadMore() },
                            hasMore = !uiState?.showEnd!!
                        )
                    }
                }
            }
        }
    }
}

项目实践:功能模块解析

WanAndroid包含多个功能模块,每个模块都遵循相同的架构设计。以下以几个核心模块为例,展示架构在实际功能中的应用。

登录模块

登录模块是大多数应用的必备功能,WanAndroid的登录模块展示了如何处理用户认证流程。

数据模型
// 用户数据模型
class User {
    var id: Int = 0
    var username: String = ""
    var password: String = ""
    var icon: String? = null
    var type: Int = 0
    var collectIds: List<Int> = emptyList()
}

// 登录UI状态
class LoginUiState(
    val username: String = "",
    val password: String = "",
    val isUsernameValid: Boolean = true,
    val isPasswordValid: Boolean = true,
    val isLoginEnable: Boolean = false,
    val isLoading: Boolean = false
)
LoginRepository
class LoginRepository(private val service: WanService) : BaseRepository() {
    // 登录
    suspend fun login(username: String, password: String): User {
        return executeRequest { service.login(username, password) }
    }
    
    // 注册
    suspend fun register(username: String, password: String, repassword: String): User {
        return executeRequest { service.register(username, password, repassword) }
    }
}
LoginViewModel
class LoginViewModel(private val repository: LoginRepository) : BaseViewModel() {
    private val _loginState = MutableLiveData<UiState<User>>()
    val loginState: LiveData<UiState<User>> = _loginState
    
    private val _uiState = MutableLiveData<LoginUiState>()
    val uiState: LiveData<LoginUiState> = _uiState
    
    init {
        _uiState.value = LoginUiState()
    }
    
    // 更新用户名
    fun updateUsername(username: String) {
        val currentState = _uiState.value ?: LoginUiState()
        val isUsernameValid = username.isNotBlank()
        val isLoginEnable = isUsernameValid && currentState.password.isNotBlank()
        
        _uiState.value = currentState.copy(
            username = username,
            isUsernameValid = isUsernameValid,
            isLoginEnable = isLoginEnable
        )
    }
    
    // 更新密码
    fun updatePassword(password: String) {
        val currentState = _uiState.value ?: LoginUiState()
        val isPasswordValid = password.isNotBlank()
        val isLoginEnable = currentState.username.isNotBlank() && isPasswordValid
        
        _uiState.value = currentState.copy(
            password = password,
            isPasswordValid = isPasswordValid,
            isLoginEnable = isLoginEnable
        )
    }
    
    // 执行登录
    fun doLogin() {
        val currentState = _uiState.value ?: return
        
        _loginState.value = UiState(isLoading = true)
        
        launchOnUI {
            try {
                val user = repository.login(currentState.username, currentState.password)
                // 保存用户信息到本地
                saveUserInfo(user)
                _loginState.value = UiState(isSuccess = user)
            } catch (e: Exception) {
                _loginState.value = UiState(isError = e.message)
                mException.value = e
            }
        }
    }
    
    // 保存用户信息
    private fun saveUserInfo(user: User) {
        // 使用Preference保存用户信息
        Preference.getInstance().setUser(user)
    }
}
登录界面(Compose)
@Composable
fun LoginPage(
    viewModel: LoginViewModel = viewModel(),
    onLoginSuccess: () -> Unit
) {
    val loginState by viewModel.loginState.observeAsState()
    val uiState by viewModel.uiState.observeAsState()
    val scope = rememberCoroutineScope()
    val context = LocalContext.current
    
    // 处理登录结果
    LaunchedEffect(loginState) {
        when {
            loginState?.isSuccess != null -> {
                // 登录成功,导航到首页
                onLoginSuccess()
            }
            loginState?.isError != null -> {
                // 登录失败,显示错误信息
                Toast.makeText(context, loginState?.isError, Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    // 登录表单
    Box(modifier = Modifier.fillMaxSize()) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 16.dp)
                .verticalScroll(rememberScrollState()),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            // 应用Logo
            Image(
                painter = painterResource(id = R.drawable.ic_logo),
                contentDescription = null,
                modifier = Modifier.size(120.dp)
            )
            
            Spacer(modifier = Modifier.height(48.dp))
            
            // 用户名输入框
            TextField(
                value = uiState?.username ?: "",
                onValueChange = { viewModel.updateUsername(it) },
                label = { Text("用户名") },
                modifier = Modifier.fillMaxWidth(),
                singleLine = true,
                isError = !(uiState?.isUsernameValid ?: true),
                supportingText = {
                    if (!(uiState?.isUsernameValid ?: true)) {
                        Text("用户名不能为空")
                    }
                }
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            // 密码输入框
            TextField(
                value = uiState?.password ?: "",
                onValueChange = { viewModel.updatePassword(it) },
                label = { Text("密码") },
                modifier = Modifier.fillMaxWidth(),
                singleLine = true,
                visualTransformation = PasswordVisualTransformation(),
                isError = !(uiState?.isPasswordValid ?: true),
                supportingText = {
                    if (!(uiState?.isPasswordValid ?: true)) {
                        Text("密码不能为空")
                    }
                }
            )
            
            Spacer(modifier = Modifier.height(32.dp))
            
            // 登录按钮
            Button(
                onClick = { viewModel.doLogin() },
                modifier = Modifier.fillMaxWidth(),
                enabled = uiState?.isLoginEnable == true && loginState?.isLoading != true
            ) {
                if (loginState?.isLoading == true) {
                    CircularProgressIndicator(
                        modifier = Modifier.size(24.dp),
                        color = Color.White,
                        strokeWidth = 2.dp
                    )
                } else {
                    Text("登录")
                }
            }
            
            Spacer(modifier = Modifier.height(16.dp))
            
            // 注册按钮
            TextButton(onClick = { /* 跳转到注册页面 */ }) {
                Text("还没有账号?去注册")
            }
        }
    }
    
    // 加载对话框
    if (loginState?.isLoading == true) {
        LoadingDialog()
    }
}

首页模块

首页模块展示了如何实现复杂的列表页面,包括轮播图、文章列表、下拉刷新和上拉加载等功能。

首页数据模型
// 文章列表
class ArticleList {
    var curPage: Int = 0
    var offset: Int = 0
    var over: Boolean = false
    var pageCount: Int = 0
    var size: Int = 0
    var total: Int = 0
    var datas: List<Article> = emptyList()
}

// 文章
class Article {
    var id: Int = 0
    var title: String = ""
    var chapterId: Int = 0
    var chapterName: String = ""
    var envelopePic: String = ""
    var link: String = ""
    var author: String = ""
    var origin: String = ""
    var publishTime: Long = 0
    var zan: Int = 0
    var desc: String = ""
    var visible: Int = 0
    var niceDate: String = ""
    var courseId: Int = 0
    var collect: Boolean = false
}

// 轮播图
class Banner {
    var id: Int = 0
    var desc: String = ""
    var imagePath: String = ""
    var isVisible: Int = 0
    var order: Int = 0
    var title: String = ""
    var type: Int = 0
    var url: String = ""
}
HomeViewModel
class HomeViewModel(
    private val repository: HomeRepository,
    private val dispatcherProvider: CoroutinesDispatcherProvider
) : BaseViewModel() {
    // 文章列表数据
    private val _articles = MutableLiveData<BaseUiModel<ArticleList>>()
    val articles: LiveData<BaseUiModel<ArticleList>> = _articles
    
    // 轮播图数据
    private val _banners = MutableLiveData<List<Banner>>()
    val banners: LiveData<List<Banner>> = _banners
    
    private var currentPage = 0
    
    // 获取首页数据(文章+轮播图)
    fun getHomeData() {
        getHomeArticles()
        getBanners()
    }
    
    // 获取文章列表
    fun getHomeArticles() {
        currentPage = 0
        _articles.value = BaseUiModel(showLoading = true)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(currentPage)
                _articles.value = BaseUiModel(showSuccess = result)
            } catch (e: Exception) {
                _articles.value = BaseUiModel(showError = e.message)
            }
        }
    }
    
    // 获取轮播图
    fun getBanners() {
        launchOnUI {
            try {
                val result = repository.getBanners()
                _banners.value = result
            } catch (e: Exception) {
                mException.value = e
            }
        }
    }
    
    // 加载更多
    fun loadMore() {
        currentPage++
        _articles.value = _articles.value?.copy(showLoading = true, isRefresh = false)
        
        launchOnUI {
            try {
                withContext(dispatcherProvider.io) {
                    repository.getHomeArticles(currentPage)
                }.let { result ->
                    val oldData = _articles.value?.showSuccess?.datas ?: emptyList()
                    val newData = oldData + result.datas
                    val newArticleList = result.copy(datas = newData)
                    
                    _articles.value = BaseUiModel(
                        showSuccess = newArticleList,
                        showEnd = currentPage >= result.pageCount - 1
                    )
                }
            } catch (e: Exception) {
                currentPage--
                _articles.value = _articles.value?.copy(showError = e.message)
            }
        }
    }
    
    // 下拉刷新
    fun refresh() {
        currentPage = 0
        _articles.value = _articles.value?.copy(isRefresh = true)
        
        launchOnUI {
            try {
                val result = repository.getHomeArticles(currentPage)
                _articles.value = BaseUiModel(showSuccess = result, isRefresh = false)
            } catch (e: Exception) {
                _articles.value = _articles.value?.copy(showError = e.message, isRefresh = false)
            }
        }
    }
}

测试策略:确保代码质量

WanAndroid项目包含单元测试和UI测试,确保核心功能的正确性和稳定性。

单元测试示例

LoginRepositoryTest为例,展示如何测试Repository层:

class LoginRepositoryTest {
    // 使用MockK模拟依赖
    private val mockService = mockk<WanService>()
    private val repository = LoginRepository(mockService)
    
    @Test
    fun `login success returns user`() = runTest {
        // 准备测试数据
        val mockUser = User().apply {
            id = 1
            username = "test"
        }
        
        val mockResponse = WanResponse<User>().apply {
            errorCode = 0
            errorMsg = ""
            data = mockUser
        }
        
        // 模拟API响应
        coEvery { mockService.login(any(), any()) } returns mockResponse
        
        // 执行测试
        val result = repository.login("test", "123456")
        
        // 验证结果
        assertEquals(mockUser.id, result.id)
        assertEquals(mockUser.username, result.username)
        
        // 验证API调用
        coVerify { mockService.login("test", "123456") }
    }
    
    @Test
    fun `login failure throws exception`() = runTest {
        // 准备测试数据
        val errorMessage = "登录失败"
        val mockResponse = WanResponse<User>().apply {
            errorCode = -1
            errorMsg = errorMessage
            data = null
        }
        
        // 模拟API响应
        coEvery { mockService.login(any(), any()) } returns mockResponse
        
        // 执行测试并验证异常
        val exception = assertFailsWith<Exception> {
            repository.login("test", "wrong_password")
        }
        
        assertEquals(errorMessage, exception.message)
    }
}

ViewModel测试示例

class LoginViewModelTest {
    private val mockRepository = mockk<LoginRepository>()
    private val viewModel = LoginViewModel(mockRepository)
    
    @Test
    fun `login button enabled when username and password not empty`() = runTest {
        // 初始状态
        var uiState = viewModel.uiState.value
        assertFalse(uiState?.isLoginEnable ?: true)
        
        // 输入用户名
        viewModel.updateUsername("test")
        uiState = viewModel.uiState.value
        assertFalse(uiState?.isLoginEnable ?: true)
        
        // 输入密码
        viewModel.updatePassword("123456")
        uiState = viewModel.uiState.value
        assertTrue(uiState?.isLoginEnable ?: false)
    }
    
    @Test
    fun `login state changes correctly`() = runTest {
        // 准备测试数据
        val mockUser = User().apply {
            id = 1
            username = "test"
        }
        
        // 模拟Repository响应
        coEvery { mockRepository.login(any(), any()) } returns mockUser
        
        // 执行登录
        viewModel.updateUsername("test")
        viewModel.updatePassword("123456")
        viewModel.doLogin()
        
        // 验证状态变化
        val loginState = viewModel.loginState.value
        assertTrue(loginState?.isLoading == true)
        
        // 等待协程完成
        advanceUntilIdle()
        
        // 验证最终状态
        val finalLoginState = viewModel.loginState.value
        assertNull(finalLoginState?.isLoading)
        assertNotNull(finalLoginState?.isSuccess)
        assertEquals(mockUser.id, finalLoginState?.isSuccess?.id)
    }
}

总结与最佳实践

通过对WanAndroid项目的深入分析,我们可以总结出以下Android架构设计的最佳实践:

架构设计最佳实践

  1. 严格分层:遵循单一职责原则,明确划分UI层、ViewModel层、Repository层和DataSource层
  2. 数据驱动UI:通过ViewModel和LiveData实现数据变化自动更新UI
  3. 状态管理:使用统一的UI状态模型,清晰管理加载、成功、失败等状态
  4. 依赖注入:使用Koin等依赖注入框架,减少组件耦合,提高可测试性
  5. 协程管理:使用viewModelScope管理协程生命周期,避免内存泄漏
  6. 统一网络处理:封装Retrofit请求,统一处理API响应和异常

代码规范建议

  1. 命名规范:使用清晰的命名,如XxxActivityXxxViewModelXxxRepository
  2. 常量管理:集中管理常量,避免硬编码
  3. 扩展函数:合理使用Kotlin扩展函数,简化代码
  4. Base类设计:提取通用功能到Base类,如BaseActivityBaseViewModel
  5. 资源管理:使用资源文件管理字符串、颜色、尺寸等,便于国际化和主题切换

性能优化建议

  1. 图片优化:使用适当分辨率的图片,考虑使用WebP格式
  2. 列表优化:使用RecyclerView或LazyColumn的复用机制,避免过度绘制
  3. 内存管理:注意图片缓存大小,及时释放不再需要的资源
  4. 避免ANR:将耗时操作放在后台线程执行
  5. 启动优化:减少启动时的初始化工作,考虑延迟加载非关键组件

结语

WanAndroid项目展示了如何在实际开发中应用Jetpack MVVM架构,通过Kotlin、Coroutines、Retrofit等现代技术栈构建高质量的Android应用。本文详细解析了项目的架构设计、技术实现和最佳实践,希望能为你的Android开发之路提供参考和启发。

掌握这些架构设计原则和实践方法,将帮助你构建更加健壮、可维护和可扩展的Android应用。无论是开发新项目还是重构现有项目,这些经验都将对你有所帮助。

最后,鼓励你亲自克隆项目代码,深入研究并动手实践,这将是提升Android架构设计能力的最佳方式。

项目地址:https://gitcode.com/gh_mirrors/wan/wanandroid

祝你在Android开发的道路上不断进步!

【免费下载链接】wanandroid Jetpack MVVM For Wanandroid 最佳实践 ! 【免费下载链接】wanandroid 项目地址: https://gitcode.com/gh_mirrors/wan/wanandroid

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

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

抵扣说明:

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

余额充值