个人技术总结——基于jetpack compose 的Android项目基建

222200311李梓玄 2024-12-14 21:51:00
这个作业属于哪个课程https://bbs.csdn.net/forums/2401_CS_SE_FZU
这个作业要求在哪里https://bbs.csdn.net/topics/619470310
这个作业的目标选择软工实践中一个相关技术进行总结
其他参考文献

目录

  • Jetpack Compose与@Composable 注解
  • 使用voyger进行页面导航
  • 引入 Navigator
  • 创建导航逻辑
  • 定义页面
  • 支持参数传递
  • 整合依赖注入
  • 网络交互设计
  • NetworkResult 接口
  • ViewModel 与 UI 层交互
  • Flow 扩展函数的设计
  • Repository 层的职责

Jetpack Compose与@Composable 注解

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/

img

当然,我们的app不可能只有这么一点ui,Composable函数是可以嵌套的

使用voyger进行页面导航

在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) // 保存新主题
                    }
                )
            }
        }
    }
}

引入 Navigator

在 RootUi 中,我们使用了 Voyager 提供的核心组件 Navigator:

Navigator(screen = SplashScreen()) { navigator ->
    KoinApplication(application = {
        modules(
            koinModule(
                MyRootAction(navigator),
                systemAction,
                navigator,
                onThemeChange
            )
        )
    }) {
        CurrentScreen()
    }
}
  • Navigator(screen = SplashScreen()):定义了导航的起始页面为 SplashScreen。
  • Lambda 传入的 navigator 是导航的控制器,负责页面栈的管理。
  • 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() // 移除当前页面,返回上一页面
    }
}
  • replaceAll():清空页面栈并导航到指定页面。
  • push():将新的页面压入导航栈。
  • 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 接口

NetworkResult 是一个泛型接口,用来表示网络请求的状态,它有以下几种实现:

  • Success: 请求成功,包含数据。
  • Error: 请求失败,包含错误信息。
  • LoadingWithAction: 正在加载,附带某些操作。
  • LoadingWithOutAction: 正在加载,但无额外操作。
  • UnSend: 请求尚未发送。

通过接口和数据类组合,实现对请求状态的封装:

  1. showToast:控制是否显示 Toast 提示。
  2. key:保证即使状态数据相同,UI 也能被强制更新。
  3. hasDeal:标记状态是否已被处理,避免重复响应。
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 与 UI 层交互

ViewModel 使用 MutableStateFlow 来维护状态,提供了流式的数据更新机制:

例如,userLogin 方法中,状态更新严格按照逻辑流程:

  1. 使用LoadingWithAction防止短时间内重复点击
  2. 执行网络请求并根据结果更新为 Success 或 Error
  3. UI 层通过 collectAsState 订阅 StateFlow 的变化,做到状态驱动 UI 的更新。
// 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()
    }
}

Flow 扩展函数的设计

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 层的职责

Repository 专注于封装具体的网络请求,并返回一个 Flow 对象:

  • 使用 ktor 客户端进行请求。
  • 不直接处理数据,而是将请求结果通过 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)
    }
}

本文的设计说明可能比较粗略,如果有什么地方有疑问,欢迎联系我!

...全文
47 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

109

社区成员

发帖
与我相关
我的任务
社区描述
202401_CS_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_TeacherL
  • 032002124林日臻
  • 助教姜词杰
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧