详解安卓架构入门

· 浏览次数 : 0

小编点评

**代码拆分后的结构:** ```kotlin // uiState 的定义 val uiState: StateFlow = myModelRepository .myModels .map { list -> MyModelUiState.Success(it) } .catch { error -> MyModelUiState.Error(error) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading) // add 函数的定义 fun add(name: String) { myModelRepository.add(name) } ``` **代码说明:** 1. **uiState 的定义**定义了一个 StateFlow,该 Flow 是由 `myModels` 中的 `Success` 和 `Error` 元素组成的。`map` 函数用于处理 `myModels` 中的 `Success` 元素,并将其转换为 `MyModelUiState.Success`。如果出现异常,`catch` 函数将转换为 `MyModelUiState.Error`,并使用 `emit` 方法通知 UI。`stateIn` 方法用于绑定 UI 的协程生命周期,并设置延迟。 2. **add 函数**定义了一个 `add` 方法,它接收 `name` 的字符串参数。首先,它调用 `myModelRepository.add()` 方法添加一个 `MyModel` 对象。如果添加成功,它使用 `MyModelUiState.Success` 来通知 UI。如果添加失败,它使用 `MyModelUiState.Error` 来通知 UI。 **其他说明:** * `MyModelUiState` 是一个自定义的数据类型,它包含 UI 状态信息。 * `myModelDao` 是一个抽象类,它提供 `MyModel` 对象的增删操作。 * `AppDatabase` 是一个抽象类,它提供数据库操作的抽象。 * `MyModel` 是一个实体类,它表示数据库中的 `MyModel` 数据表。

正文

准备

首先进入安卓架构入门的代码仓库:

Android Architecture Starter Templates:
https://github.com/android/architecture-templates

先看看介绍,简单分析一下:

  • 架构入门的模板
  • UI 界面非常简陋
  • Navigation 导航
  • 协程和Flow
  • Hilt 依赖注入
  • Hilt 虚假数据进行UI测试

提供了两个模板,单模块多模块,单模块和多模块没有绝对的谁好谁坏。

  • 单模块使用起来简单快速,但开发维护会随着项目变大越来越难。
  • 多模块会增加额外的负担,如:不同模块配置Build难以保持一致、模块间的交互需要精细化的设计,但开发维护的难度变化不会太大。

单模块

移除无用代码和文件

先粗略点开app模块的所有代码,简单看看。

build.gradle.kts

plugins上方出现一个 @Suppress (忽略警告的注解)。点开链接,发现问题在Gradle 8.1+版本已经解决,这个注解可以删掉。

@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369

android内部出现一个 packagingOptions 弃用警告,点开发现被packaging替代,二者的参数是一样的,都是Packaging接口的无参扩展函数,所以可以直接替换。

Theme.kt

发现在 SideEffect(每次重组后都会执行) 内的修改顶部状态栏的代码标记弃用。

(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()  
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme

改为:

val window = (view.context as Activity).window  
window.statusBarColor = colorScheme.primary.toArgb()  
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme

MyApplication.kt

就只有一个简单的 @HiltAndroidApp 注解,Hilt 依赖注入会附加到这个Application的生命周期,并提供依赖项。所有使用Hilt的应用都必须有这个注解。

MainActivity.kt

MainActivity上方有个 @AndroidEntryPoint 注解,也是 Hilt 依赖注入的注解。
setContent 使用了 theme 里的 MyApplicationTheme ,然后一个铺满全屏的背景色。content调用了MainNavigation。

使用了NavHostController和NavHost,但和没有用一样,就只有一个MyModelScreen。

MyModelScreen.kt

fun MyModelScreen(modifier: Modifier = Modifier, viewModel: MyModelViewModel = hiltViewModel())

参数1 使用了官方推荐的写法,modifier可以被传入。
参数2 把viewModel的默认参数设置为hiltViewModel(),交由Hilt注入。也可以由Activity创建ViewModel然后传递过来。

val items by viewModel.uiState.collectAsStateWithLifecycle()

uiState 的类型是StateFlow<MyModelUiState>,也就是只读类型的Flow,StateFlow和Compose的State无关!
collectAsStateWithLifecycle 是compose为协程增加的生命周期扩展的函数之一,可以只在Compose的生命周期里收集协程传来的数据。需要添加以下依赖:

androidx.lifecycle:lifecycle-runtime-compose


if (items is MyModelUiState.Success) {  
    MyModelScreen(  
        items = (items as MyModelUiState.Success).data,  
        onSave = viewModel::addMyModel,  
        modifier = modifier  
    )  
}

当item是MyModelUiState.Success类型时,显示MyModelScreen屏幕。
但是我感觉这里不应该这样写,MyModelUiState密封接口有三个状态,Loading、Error、Success,应该三种情况都要写出来,应该改成这样:

when(items){  
    MyModelUiState.Loading -> {  
        //TODO Loading  
    }  
    is MyModelUiState.Error -> {  
        //TODO ERROR  
    }  
    is MyModelUiState.Success -> {  
        MyModelScreen(  
            items = (items as MyModelUiState.Success).data,  
            onSave = viewModel::addMyModel,  
            modifier = modifier  
        )  
    }  
}

MyModelScreen、DefaultPreview、PortraitPreview 这三个函数就是简单的绘制和预览,没有什么好说的。

MyModelViewModel.kt

在MyModelViewModel上方出现 @HiltViewModel 注解,使这个ViewModel可以提供给Hilt注入。主构造函数出现 @Inject 注解,注入MyModelRepository到myModelRepository。

sealed interface MyModelUiState {  
    object Loading : MyModelUiState  
    data class Error(val throwable: Throwable) : MyModelUiState  
    data class Success(val data: List<String>) : MyModelUiState  
}

界面状态,分为三种情况:

  • Loading 加载中
  • Error 加载失败
  • Success 加载成功

fun addMyModel(name: String) {  
    viewModelScope.launch {  
        myModelRepository.add(name)  
    }  
}

使用和viewModel生命周期绑定的协程,向myModelRepository添加一个name。

val uiState: StateFlow<MyModelUiState> = myModelRepository  
    .myModels.map<List<String>, MyModelUiState>(::Success)  
    .catch { emit(Error(it)) }  
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading)

我把代码拆一下:

val uiState: StateFlow<MyModelUiState> = myModelRepository  
    .myModels.map<List<String>, MyModelUiState>{  
        MyModelUiState.Success(it)  
    }  
    .catch {  
        emit(MyModelUiState.Error(it))  
    }  
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MyModelUiState.Loading)
  • myModelRepository .myModels 的类型是 Flow<List<String>>。
  • map是flow的拓展函数,把Flow存储的A类型转为B类型。把 List<String> 类型作为入参的类型,把MyModelUiState作为出参的类型。MyModelUiState.Success(it) 里的 it 就是原本的 List<String> 类型的参数。这里泛型指定 MyModelUiState 的原因是下方的需要catch需要emit MyModelUiState.Error,如果不需要异常状态可以去掉这里的显式指定异常。
  • stateIn,绑定viewModel的协程生命周期,订阅者和共享协程停止时的延迟,Flow在collect时默认参数。
  • SharingStarted.WhileSubscribed(5000),在没有Flow的订阅者时,会停止collect,延迟设定为5000毫秒,如果5秒内有了新的订阅者,就不会停止collect。与之对应的是SharingStarted.Lazily,永远不会停止collect。

MyModelRepository.kt

interface MyModelRepository {  
    val myModels: Flow<List<String>>  
  
    suspend fun add(name: String)  
}
  • myModels 的类型是Flow<List<String>>,用来在协程中收集数据。
  • add 函数前有 suspend 表示需要在协程中执行。

class DefaultMyModelRepository @Inject constructor(  
    private val myModelDao: MyModelDao  
) : MyModelRepository {  
  
    override val myModels: Flow<List<String>> =  
        myModelDao.getMyModels().map { items -> items.map { it.name } }  
  
    override suspend fun add(name: String) {  
        myModelDao.insertMyModel(MyModel(name = name))  
    }  
}
  • myModels通过 myModelDao 获取 Flow<List<MyModel>>类型的对象转换为Flow<List<String>>类型。
  • add 通过 myModelDao 插入一个 MyMode l对象。

DataModule.kt

提供虚假的数据给 androidTest 使用,没什么特殊的。

DatabaseModule.kt

@Module  
@InstallIn(SingletonComponent::class)

Hilt 单例绑定,注入Application。整个Application只会出现一个实例。

@Provides  
fun provideMyModelDao(appDatabase: AppDatabase): MyModelDao {  
    return appDatabase.myModelDao()  
}

@Provides 作用域是整个生命周期,告诉Hilt,这个函数可以提供MyModelDao类型的对象。
这整段代码的意思是,在Hilt注解需要注入MyModelDao类型的对象时,通过这个函数获取。

@Provides  
@Singleton
fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase {  
    return Room.databaseBuilder(  
        appContext,  
        AppDatabase::class.java,  
        "MyModel"  
    ).build()  
}

@Singleton 整个Application的生命周期只会生成一次。
这整段代码的意思是创建一个AppDatabase类型的对象,这段代码只会执行一次,后续需要AppDatabase类型的对象时会一直使用这个对象。

AppDatabase.kt

@Database(entities = [MyModel::class], version = 1)  
abstract class AppDatabase : RoomDatabase() {  
    abstract fun myModelDao(): MyModelDao  
}
  • @Database 注解 和 继承 RoomDatabase,创建一个数据库。
  • entities = [MyModel::class] 数据库中有个MyModel类型的表。
  • myModelDao() 获取MyModelDao实例对象。

MyModel.kt

@Entity  
data class MyModel(  
    val name: String  
) {  
    @PrimaryKey(autoGenerate = true)  
    var uid: Int = 0  
}
  • @Entity 提供给AppDatabase的数据表。
  • name: String 数据表的字段。
  • @PrimaryKey(autoGenerate = true) 声明这个数据表的主键。

多模块

单模块的代码已经讲的很详细了,这里仅仅讲一下差异。

app模块

代码文件:

  • MyApplication.kt
  • MainActivity.kt
  • MainNavigation.kt

build.gradle.kts

implementation(project(":core-ui"))
implementation(project(":feature-mymodel"))


core-ui

之前的theme目录,放一些通用的compose可组合项,必须是通用的。

core-testing

自定义测试Application。

test-app

之前的 androidTest 目录。

core-data

之前的data目录。
代码文件:

  • DataModule.kt
  • MyModelRepository.kt

    implementation

core-database

之前的database目录。

feature-mymodel

之前的mymodel目录,值得注意的是这里也有androidTest,这里的测试针对的是当前模块的,并没有使用FakeMyModelRepository,而是直接给了个list。可见多模块的情况下,官方对于测试也没有很优雅的解决方案。

与详解安卓架构入门相似的内容:

详解安卓架构入门

准备 首先进入安卓架构入门的代码仓库: Android Architecture Starter Templates: https://github.com/android/architecture-templates 先看看介绍,简单分析一下: 架构入门的模板 UI 界面非常简陋 Navigati

[转帖]玩转zabbix之超详细的二进制安装

https://zhuanlan.zhihu.com/p/212281069 #初始配置 #centos7添加阿里云镜像 wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo #安

详解C#委托与事件

在C#中,委托是一种引用类型的数据类型,允许我们封装方法的引用。通过使用委托,我们可以将方法作为参数传递给其他方法,或者将多个方法组合在一起,从而实现更灵活的编程模式。委托类似于函数指针,但提供了类型安全和垃圾回收等现代语言特性。 基本概念 定义委托 定义委托需要指定它所代表的方法的原型,包括返回类

详解Web应用安全系列(8)不足的日志记录和监控

在Web安全领域,不足的日志记录和监控是一个重要的安全隐患,它可能导致攻击者能够更隐蔽地进行攻击,同时增加了攻击被检测和响应的难度。以下是对Web攻击中不足的日志记录和监控漏洞的详细介绍。 一、日志记录不足的问题 日志缺失或不完整 关键操作未记录:如用户登录、敏感数据访问、系统管理员操作等关键操作未

详解Web应用安全系列(5)敏感数据泄露漏洞

在最近几年,这是最常见的,最具影响力的攻击。这个领域最常见的漏洞是不对敏感数据进行加密。在数据加密过程中,常见的问题是不安全的密钥生成和管理以及使用弱密码算法,弱协议和弱密码。特别是使用弱的哈希算法来保护密码。在服务端,检测数据传输过程中的数据弱点很容易,但检测存储数据的弱点却非常困难。 敏感数据泄

详解Web应用安全系列(4)失效的访问控制

在Web安全中,失效的访问控制(也称为权限控制失效或越权访问)是指用户在不具备相应权限的情况下访问了受限制的资源或执行了不允许的操作。这通常是由于Web应用系统未能建立合理的权限控制机制,或者权限控制机制失效所导致的。 危害 数据泄漏:攻击者可能通过越权访问获取敏感数据,如用户个人信息、财务数据、家

详解Web应用安全系列(3)失效的身份认证

大多数身份和访问管理系统的设计和实现,普遍存在身份认证失效的问题。会话管理是身份验证和访问控制的基础,并且存在于所有有状态的应用程序中。攻击者可以使用指南手册来检测失效的身份认证,但通常会关注密码转储,字典攻击,或者在类似于钓鱼或社会工程攻击之后,发现失效的身份认证。 确认用户的身份,身份验证和会话

详解Web应用安全系列(2)注入漏洞之XSS攻击

上一篇介绍了SQL注入漏洞,今天我们来介绍另一个注入漏洞,即XSS跨站脚本攻击。XSS 全称(Cross Site Scripting) 跨站脚本攻击, 是Web应用中常见的漏洞。指攻击者在网页中嵌入客户端脚本(一般是JavaScript),当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到

详解Web应用安全系列(1)注入漏洞之SQL注入

注入漏洞通常是指在可输入参数的地方,通过构造恶意代码,进而威胁应用安全和数据库安全。常见的注入漏洞包括:SQL注入和XSS跨站脚本攻击。 这篇文章我们主要讲SQL注入,SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加

详解Kubernetes Pod优雅退出

1、概述 Pod优雅关闭是指在Kubernetes中,当Pod因为某种原因(如版本更新、资源不足、故障等)需要被终止时,Kubernetes不会立即强制关闭Pod,而是首先尝试以一种“优雅”的方式关闭Pod。这个过程允许Pod中的容器有足够的时间来响应终止信号(默认为SIGTERM),并在终止前完成