从0到1掌握WanAndroid:Jetpack MVVM架构最佳实践全解析
你是否还在为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、OkHttp | API数据获取 |
| 依赖注入 | 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采用清晰的分层架构,严格遵循单一职责原则,使各组件之间低耦合、高内聚。
整体架构图
分层详解
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实现高效、简洁的网络请求。
网络架构
实现细节
- 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) }
}
- 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)
}
}
}
- 协程作用域管理
在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组件,如TitleBar、ArticleItem等:
@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架构设计的最佳实践:
架构设计最佳实践
- 严格分层:遵循单一职责原则,明确划分UI层、ViewModel层、Repository层和DataSource层
- 数据驱动UI:通过ViewModel和LiveData实现数据变化自动更新UI
- 状态管理:使用统一的UI状态模型,清晰管理加载、成功、失败等状态
- 依赖注入:使用Koin等依赖注入框架,减少组件耦合,提高可测试性
- 协程管理:使用viewModelScope管理协程生命周期,避免内存泄漏
- 统一网络处理:封装Retrofit请求,统一处理API响应和异常
代码规范建议
- 命名规范:使用清晰的命名,如
XxxActivity、XxxViewModel、XxxRepository - 常量管理:集中管理常量,避免硬编码
- 扩展函数:合理使用Kotlin扩展函数,简化代码
- Base类设计:提取通用功能到Base类,如
BaseActivity、BaseViewModel - 资源管理:使用资源文件管理字符串、颜色、尺寸等,便于国际化和主题切换
性能优化建议
- 图片优化:使用适当分辨率的图片,考虑使用WebP格式
- 列表优化:使用RecyclerView或LazyColumn的复用机制,避免过度绘制
- 内存管理:注意图片缓存大小,及时释放不再需要的资源
- 避免ANR:将耗时操作放在后台线程执行
- 启动优化:减少启动时的初始化工作,考虑延迟加载非关键组件
结语
WanAndroid项目展示了如何在实际开发中应用Jetpack MVVM架构,通过Kotlin、Coroutines、Retrofit等现代技术栈构建高质量的Android应用。本文详细解析了项目的架构设计、技术实现和最佳实践,希望能为你的Android开发之路提供参考和启发。
掌握这些架构设计原则和实践方法,将帮助你构建更加健壮、可维护和可扩展的Android应用。无论是开发新项目还是重构现有项目,这些经验都将对你有所帮助。
最后,鼓励你亲自克隆项目代码,深入研究并动手实践,这将是提升Android架构设计能力的最佳方式。
项目地址:https://gitcode.com/gh_mirrors/wan/wanandroid
祝你在Android开发的道路上不断进步!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



