首先进入安卓架构入门的代码仓库:
Android Architecture Starter Templates:
https://github.com/android/architecture-templates
先看看介绍,简单分析一下:
提供了两个模板,单模块和多模块,单模块和多模块没有绝对的谁好谁坏。
先粗略点开app模块的所有代码,简单看看。
在plugins上方出现一个 @Suppress (忽略警告的注解)。点开链接,发现问题在Gradle 8.1+版本已经解决,这个注解可以删掉。
@Suppress("DSL_SCOPE_VIOLATION") // Remove when fixed https://youtrack.jetbrains.com/issue/KTIJ-19369
在android内部出现一个 packagingOptions 弃用警告,点开发现被packaging替代,二者的参数是一样的,都是Packaging接口的无参扩展函数,所以可以直接替换。
发现在 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
就只有一个简单的 @HiltAndroidApp 注解,Hilt 依赖注入会附加到这个Application的生命周期,并提供依赖项。所有使用Hilt的应用都必须有这个注解。
MainActivity上方有个 @AndroidEntryPoint 注解,也是 Hilt 依赖注入的注解。
setContent 使用了 theme 里的 MyApplicationTheme ,然后一个铺满全屏的背景色。content调用了MainNavigation。
使用了NavHostController和NavHost,但和没有用一样,就只有一个MyModelScreen。
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上方出现 @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
}
界面状态,分为三种情况:
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)
interface MyModelRepository {
val myModels: Flow<List<String>>
suspend fun add(name: String)
}
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))
}
}
提供虚假的数据给 androidTest 使用,没什么特殊的。
@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类型的对象时会一直使用这个对象。
@Database(entities = [MyModel::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun myModelDao(): MyModelDao
}
@Entity
data class MyModel(
val name: String
) {
@PrimaryKey(autoGenerate = true)
var uid: Int = 0
}
单模块的代码已经讲的很详细了,这里仅仅讲一下差异。
代码文件:
build.gradle.kts
implementation(project(":core-ui"))
implementation(project(":feature-mymodel"))
之前的theme目录,放一些通用的compose可组合项,必须是通用的。
自定义测试Application。
之前的 androidTest 目录。
之前的data目录。
代码文件:
之前的database目录。
之前的mymodel目录,值得注意的是这里也有androidTest,这里的测试针对的是当前模块的,并没有使用FakeMyModelRepository,而是直接给了个list。可见多模块的情况下,官方对于测试也没有很优雅的解决方案。