109
社区成员
![](https://csdnimg.cn/release/cmsfe/public/img/topic.427195d5.png)
![](https://csdnimg.cn/release/cmsfe/public/img/me.40a70ab0.png)
![](https://csdnimg.cn/release/cmsfe/public/img/task.87b52881.png)
![](https://csdnimg.cn/release/cmsfe/public/img/share-circle.3e0b7822.png)
这个作业属于哪个课程 | https://bbs.csdn.net/forums/2401_CS_SE_FZU |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/topics/619470310 |
这个作业的目标 | 选择软工实践中一个相关技术进行总结 |
其他参考文献 | 无 |
Jetpack Compose 与传统的 XML 编写 Android UI 的方式不同,采用声明式 UI,通过声明式方法来定义界面,like this
// 最简单的示例
@Composable
fun Greeting() {
// Text 是 Compose 提供的一个 @Composable 函数
Text(text = "Hello, World!")
}
想要使用它,最简单的办法是这样
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting()
}
}
}
科普:如果你没有任何android基础,目前你只需要知道:MainActivity是app的入口,就像别的语言的main函数那样,暂时不需要管它的继承和override到底在干嘛
效果是这样的:
回到Greeting函数:Text 是Jetpack Compose 的一个基本组件,别的常用组件有 Column(列) Row(行)Button(按钮等),这些组件的基本用法可以参考
https://jetpackcompose.cn/docs/
当然,我们的app不可能只有这么一点ui,Composable函数是可以嵌套的
在MainActivity中传入Rootui
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
// 使用 remember 保存主题选项的状态
val themeOption = remember { mutableStateOf(SettingPreferences(this).getThemeOption()) }
val darkTheme = when (themeOption.value) {
ThemeOption.AUTO -> isSystemInDarkTheme()
ThemeOption.LIGHT -> false
ThemeOption.DARK -> true
}
MyApplicationTheme(darkTheme = darkTheme) {
println(darkTheme)
RootUi(
systemAction = SystemAction(
onFinish = { finish() },
onBack = { onBackPressedDispatcher }
),
onThemeChange = { newTheme ->
themeOption.value = newTheme
SettingPreferences(this).setThemeOption(newTheme) // 保存新主题
}
)
}
}
}
}
在 RootUi 中,我们使用了 Voyager 提供的核心组件 Navigator:
Navigator(screen = SplashScreen()) { navigator ->
KoinApplication(application = {
modules(
koinModule(
MyRootAction(navigator),
systemAction,
navigator,
onThemeChange
)
)
}) {
CurrentScreen()
}
}
MyRootAction 是一个自定义导航逻辑类,实现了 RootAction 接口,其中定义了所有页面跳转的方法。
class MyRootAction(private val navigator: Navigator) : RootAction {
override fun navigateToMain() {
navigator.replaceAll(MainScreen()) // 替换栈中所有页面为 MainScreen
}
override fun navigateToLogin() {
navigator.push(LoginScreen()) // 添加 LoginScreen 到页面栈顶
}
override fun navigateToBack() {
navigator.pop() // 移除当前页面,返回上一页面
}
}
这种设计解耦了页面跳转的具体逻辑,使得代码更易于扩展和维护。
所有页面如 MainScreen、LoginScreen 等都是 Voyager 的 Screen,需要在各自的文件中实现。
class MainScreen : Screen {
@Composable
override fun Content() {
Text(text = "Welcome to Main Screen")
}
}
每个 Screen 都需要实现 Content() 方法,在其中定义页面的 UI 组件。
如果需要在页面跳转时传递参数,可以直接通过构造函数传递
override fun navigateToLocation(onLocationSelect: (String) -> Unit) {
navigator.push(LocationScreen(onLocationSelect))
}
这里,LocationScreen 接收了一个回调函数 onLocationSelect,可以在目标页面中使用:
class LocationScreen(private val onLocationSelect: (String) -> Unit) : Screen {
@Composable
override fun Content() {
// 用户选择位置后调用
onLocationSelect("Selected Location")
}
}
代码中结合了 Koin 用于依赖注入:
Navigator(screen = SplashScreen()) { navigator ->
KoinApplication(application = {
modules(
koinModule(
MyRootAction(navigator),
systemAction,
navigator,
onThemeChange
)
)
}) {
CurrentScreen()
}
}
fun koinModule(
rootAction: RootAction,
systemAction: SystemAction,
navigator: Navigator,
onThemeChange: (ThemeOption) -> Unit
) = module {
// 界面跳转
single { rootAction }
// finish和back操作
single { systemAction }
// 导航操作
single { navigator }
// 主题切换操作
single { onThemeChange }
// 注入http client
httpClientInjection()
// 注入repository
repositoryInjection()
// 注入viewModel
viewModelInjection()
}
在界面中通过koin获取ViewModel 和 RootAction依赖
class LoginScreen : Screen {
@Composable
override fun Content() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// 通过koin 注入 viewmodel,rootaction
val loginViewModel = koinInject<LoginViewModel>()
val loginState = loginViewModel.loginResponse.collectAsState()
val rootAction = koinInject<RootAction>()
...
Column {
TopReturnBar("登录")
Body(loginViewModel, isLoading, rootAction)
}
}
}
通过这种方式,可以在界面中注入需要的依赖,比如 ViewModel 和 RootAction,进一步增强了代码的灵活性和模块化。
NetworkResult 是一个泛型接口,用来表示网络请求的状态,它有以下几种实现:
通过接口和数据类组合,实现对请求状态的封装:
interface NetworkResult<T> {
val key: MutableState<Int>// 表示某种状态的唯一键值,用来更新某个特定的 UI 组件状态。
var showToast: Boolean // 是否显示Toast提示
var hasDeal: Boolean // 结果是否已经被处理
// 定义接口的6种情况:访问成功,接口错误,加载中(附带操作),加载中(无操作),未发送
data class Success<T>(
val dataForShow: T,
val rawData: T? = null,
override var showToast: Boolean = true,
override val key: MutableState<Int> = mutableIntStateOf(0),
override var hasDeal: Boolean = false,
) : NetworkResult<T>
data class Error<T>(
val errorForShow: Throwable,
val rawError: Throwable,
override var showToast: Boolean = true,
override val key: MutableState<Int> = mutableIntStateOf(0),
override var hasDeal: Boolean = false,
) : NetworkResult<T>
class LoadingWithAction<T>(
override var showToast: Boolean = true,
override val key: MutableState<Int> = mutableIntStateOf(0),
override var hasDeal: Boolean = false,
) : NetworkResult<T>
class LoadingWithOutAction<T>(
override var showToast: Boolean = true,
override val key: MutableState<Int> = mutableIntStateOf(0),
override var hasDeal: Boolean = false,
) : NetworkResult<T>
class UnSend<T>(
override var showToast: Boolean = true,
override val key: MutableState<Int> = mutableIntStateOf(0),
override var hasDeal: Boolean = false,
) : NetworkResult<T>
}
fun <T> networkErrorWithLog(error: Throwable, newDescribe: String) =
NetworkResult.Error<T>(rawError = error, errorForShow = Throwable(newDescribe))
fun <T> networkErrorWithLog(errorCode: Int, newDescribe: String) =
NetworkResult.Error<T>(
rawError = Throwable("Error Code : $errorCode"),
errorForShow = Throwable(newDescribe),
)
ViewModel 使用 MutableStateFlow 来维护状态,提供了流式的数据更新机制:
例如,userLogin 方法中,状态更新严格按照逻辑流程:
// MutableStateFlow<T>(initialValue: T)
private val _loginResponse =
MutableStateFlow<NetworkResult<UserLoginResponse>>(NetworkResult.UnSend())
// 一个 MutableStateFlow 对应一个 StateFlow,StateFlow是 只读的 MutableStateFlow,确保不会被外部修改
// 我们在在ui界面(screen)调用 StateFlow.collectAsState 订阅StateFlow
// MutableStateFlow 的变化 通知它的 StateFlow 订阅者,所有 collect 或 collectAsState 的调用都会获取到新值
val loginResponse = _loginResponse.asStateFlow()
fun userLogin(username: String, password: String) {
viewModelScope.launch {
_loginResponse.logicIfNotLoading {
userRepository.login(username, password).actionWithLabel(
"user login",
// 如果 catch 到错误,将 NetworkResult reset 为 NetworkResult<Error>
catchAction = { label, error ->
_loginResponse.resetWithLog(
label,
networkErrorWithLog(error, "登录失败")
)
},
// 如果成功collect,则将获取到的 response 转化为 NetworkResult<Success>
collectAction = { label, data ->
_loginResponse.resetWithLog(label, data.toNetworkResult())
},
)
}
}
}
val loginState = loginViewModel.loginResponse.collectAsState()
LaunchedEffect(loginState.value) {
when (val result = loginState.value) {
is NetworkResult.Success -> { /* 登录成功逻辑 */ }
is NetworkResult.Error -> { /* 登录失败逻辑 */ }
}
}
// 可传入两个挂起的lambda表达式
// 先执行preAction,如果NetworkResult不在[loading]状态,将NetworkResult果设置为loading,并执行block
suspend fun <T> MutableStateFlow<NetworkResult<T>>.logicIfNotLoading(
preAction: suspend () -> Unit = {},
block: suspend () -> Unit,
) {
preAction.invoke()
if (this.value !is NetworkResult.LoadingWithAction) {
this.resetWithoutLog(NetworkResult.LoadingWithAction())
block.invoke()
}
}
LogicIfNotLoading
用于避免重复发送网络请求。当 NetworkResult 状态不是 LoadingWithAction 时,才会执行请求。
// 可传入两个挂起的lambda表达式
// 先执行preAction,如果NetworkResult不在[loading]状态,将NetworkResult果设置为loading,并执行block
suspend fun <T> MutableStateFlow<NetworkResult<T>>.logicIfNotLoading(
preAction: suspend () -> Unit = {},
block: suspend () -> Unit,
) {
preAction.invoke()
if (this.value !is NetworkResult.LoadingWithAction) {
this.resetWithoutLog(NetworkResult.LoadingWithAction())
block.invoke()
}
}
catchWithMessage 和 actionWithLabel
捕获 Flow 执行中的异常,支持自定义异常处理逻辑,并且收集 Flow 对象返回的数据
package com.android.bafang.flow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
// 捕获异常,打印错误信息并执行 action
suspend fun <T> Flow<T>.catchWithMessage(
label: String = "",
action: (suspend (label: String, Throwable) -> Unit)? = null,
): Flow<T> {
// 通常我们会在 catch 后使用 collect,like this Flow<T>.catch{}.collect{}
// 注意这个方法返回了一个 Flow 对象,以便于后续 collect,而 collect 是 Flow 的最终消费,所以没有返回值
// 如果 catch 到异常,collect 会被终止
return this.catch { error ->
println("$label error: ${error.message}")
action?.invoke(label, error)
}
}
// 收集信息,打印标签信息并执行 action
suspend fun <T> Flow<T>.collectWithMessage(
label: String = "",
action: suspend (label: String, data: T) -> Unit,
) {
// flowOn(Dispatchers.IO):将上游切换到 IO 线程
// 因为上游操作包含网络请求,切换到协程防止主线程阻塞
// collect仍在主线程中运行
this.flowOn(Dispatchers.IO).collect { data ->
println("$label collect: $data")
action(label, data)
}
}
// 在ViewModel中,所有 Repository 层的方法必须使用这个扩展函数来收集 Response 并定义对应的逻辑
// suspend表示挂起函数,表示这个方法可以使用协程(例如:切换到IO线程)
suspend fun <T> Flow<T>.actionWithLabel(
label: String,// 日志label
catchAction: suspend (label: String, error: Throwable) -> Unit, // catch 到错误后的 action
// error 是捕获到的错误
collectAction: suspend (label: String, data: T) -> Unit,// 成功 collect 后的 action
) {
// 注意,kotlin 扩展函数中的 this 表示调用它的对象,这里表示 Flow<Response> 对象
this.catchWithMessage(label = label, action = catchAction)
.collectWithMessage(label = label, action = collectAction)
}
Repository 专注于封装具体的网络请求,并返回一个 Flow 对象:
fun login(username: String, password: String): Flow<UserLoginResponse> {
return flow {
val response = client.submitForm(
url = "/bafang/user/login",
formParameters = parameters {
append("username", username)
append("password", password)
}
).body<UserLoginResponse>()
emit(response)
}
}
本文的设计说明可能比较粗略,如果有什么地方有疑问,欢迎联系我!