• 详解安卓架构入门


    准备

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

    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,也就是只读类型的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) : MyModelUiState  
    }
    

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

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

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

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

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

    我把代码拆一下:

    val uiState: StateFlow = myModelRepository  
        .myModels.map, MyModelUiState>{  
            MyModelUiState.Success(it)  
        }  
        .catch {  
            emit(MyModelUiState.Error(it))  
        }  
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MyModelUiState.Loading)
    
    • myModelRepository .myModels 的类型是 Flow>。
    • map是flow的拓展函数,把Flow存储的A类型转为B类型。把 List 类型作为入参的类型,把MyModelUiState作为出参的类型。MyModelUiState.Success(it) 里的 it 就是原本的 List 类型的参数。这里泛型指定 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>  
      
        suspend fun add(name: String)  
    }
    
    • myModels 的类型是Flow>,用来在协程中收集数据。
    • add 函数前有 suspend 表示需要在协程中执行。

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

    代码文件:

    • DataModule.kt
    • MyModelRepository.kt
      之前的data目录。

    core-database

    之前的database目录。

    feature-mymodel

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

  • 相关阅读:
    spark sql 的join调优
    软著申请注意事项
    软考 - 系统架构设计师 - 基于口令的认证方式和基于公钥体系的认证方式
    大数据架构
    解决 不生效的问题
    冒泡排序法的逐步优化
    rust类型转换
    记一次分析接口
    生成器和表达式
    [NCTF2019]SQLi regexp 盲注
  • 原文地址:https://www.cnblogs.com/laomuji666/p/18168236