首先,我不是开发人员或其他任何人,但我想要一个我的手机上不存在的应用程序,并且我了解最少量的编码,完成了 Android Kotlin/Jetpack Compose 基础课程,请耐心等待,因为我只明白我在说什么,而且并不总是知道正确的术语。提前抱歉,这花了多长时间,但我试图弄清楚正在发生的事情,并且不知道如何使其更简洁或哪些内容相关或不相关。
上下文是这样的,我正在尝试制作一个仅适合我的应用程序,以适应我想要它做的事情,这不适合公众。它将是一个数据库收集应用程序,顶部应用栏显示屏幕名称(尽管只有两个屏幕),底部应用栏按顺序有 4 个图标:导航到收集屏幕、导航到统计屏幕、应用 SQL 过滤器功能(在收集或统计屏幕上或同时在两者上或其他东西上,带有选项的弹出式卡片抽屉),添加到数据库功能(调出卡片或其他东西以供输入)。然后是这些应用程序栏之间的内容主体。只有两个屏幕,一个主屏幕和一个统计屏幕。我只是从一些架构开始,已经开始设置 Dao 等。实际上,我是从纸杯蛋糕库存应用程序开始的,作为开始此操作的基础。
我希望主屏幕的主体以包含切换图标的行开头,用于在表视图和列表视图之间切换。问题是,我的 ViewModel 已经有一个 StateFlow 用于调用数据库列表本身,并且开关的示例本身也是视图模型中的状态流,所以我不知道如何让这个 StateFlow 和另一个 StateFlow 都通过进入 ViewModel StateFlow 但保持各自独立的部分?我试图找到某种例子来表明有两个像这样的东西是独立的东西。我有一个用于集合本身的数据库和一个用于存储视图模式(表与列表)的首选项存储库。到目前为止,这就是该存储库的全部设置。
然后我查看了 Dessert Clicker 示例中的视图更改开关,其架构有所不同,但我能够将其放在需要的位置,但随后我尝试找出如何将开关垃圾添加到视图中模型和我最终得到的结果不会在工作室中给出错误并实际编译,但当我运行模拟器时崩溃。这就是我试图做的,但它显然不起作用,而且我不太了解 StateFlows。
我不确定是 ViewModel 未正确完成,还是主屏幕中的实现导致我尝试在模拟器中运行它时崩溃。我也找不到任何显示像这样的 ViewModel 或任何使用“合并”属性(功能?)的示例,我有点……难住了。这是处理 ViewModel 的正确方法吗?
当前viewmodel组合尝试 HomeViewModel.kt:
class HomeViewModel(
itemsRepository: ItemsRepository,
private val preferencesRepo: PreferencesRepo
): ViewModel() {
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
val homeUiState: StateFlow<HomeUiState> =
merge(
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
),
preferencesRepo.isTableView.map { isTableView -> HomeUiState(isTableView = isTableView) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
)as StateFlow<HomeUiState>
/* Toggle Cellar View */
fun selectView(isTableView: Boolean) {
viewModelScope.launch {
preferencesRepo.saveViewPreference(isTableView)
}
}
}
data class HomeUiState(
val items: List<Items> = listOf(),
val isTableView: Boolean = true,
val toggleContentDescription: Int =
if (isTableView) R.string.table_view_toggle else R.string.list_view_toggle,
val toggleIcon: Int =
if (isTableView) R.drawable.list_view else R.drawable.table_view
)
还有 HomeScreen.kt(抱歉,代码非常混乱,忽略一些愚蠢的值,例如非标题位置的标题和大 .dp 大小,这只是我夸大了事情,所以我知道是什么影响了什么以及在哪里) :
object HomeDestination : NavigationDestination {
override val route = "home"
override val titleRes = R.string.home_title
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
navigateToHome: () -> Unit,
navigateToStats: () -> Unit,
modifier: Modifier = Modifier,
viewmodel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val homeUiState by viewmodel.homeUiState.collectAsState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
CellarTopAppBar(
title = stringResource(HomeDestination.titleRes),
scrollBehavior = scrollBehavior,
canNavigateBack = false,
)
},
bottomBar = {
CellarBottomAppBar(
navigateToHome = navigateToHome,
navigateToStats = navigateToStats,
)
},
) { innerPadding ->
HomeBody(
items = homeUiState.items,
homeUiState = homeUiState,
selectView = viewmodel::selectView,
modifier = modifier.fillMaxSize(),
contentPadding = innerPadding,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun HomeBody(
items: List<Items>,
homeUiState: HomeUiState,
selectView: (Boolean) -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(8.dp),
) {
Column(
modifier = modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
//Switch table/list view//
Text(
text ="Switch View:",
textAlign = TextAlign.Start,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(contentPadding),
)
IconButton(
onClick = {
val isTableView = true
selectView(!isTableView)
}
) {
Icon(
painter = painterResource(homeUiState.toggleIcon),
contentDescription = stringResource(R.string.list_view_toggle),
tint = onPrimaryContainerLight,
)
}
//Search bar//
Text(
text = "(TODO \"Search bar\")",
textAlign = TextAlign.Start,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(contentPadding),
)
/* TODO Search bar */
}
if (items.isEmpty()) {
Text(
text = stringResource(R.string.no_items),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(contentPadding),
)
} else {
if (homeUiState.isTableView) {
TableViewMode()
} else {
ListViewMode()
}
}
}
}
/* View Mode functions */
@Composable
fun TableViewMode(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
){
/* TODO Table View */
}
@Composable
fun ListViewMode(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
){
/* TODO List View */
}
如果合并两个 StateFlow,您只会得到一个普通 Flow(一个冷流,当开始收集时,它将开始从两个热 StateFlow 收集)。这是因为
merge
函数只是一个常规的 Flow 运算符。所以合并它们的正确方法是先合并两个冷香草流,然后对它们调用stateIn
:
val homeUiState: StateFlow<HomeUiState> =
merge(
itemsRepository.getAllItemsStream().map { HomeUiState(it) },
preferencesRepo.isTableView.map { isTableView -> HomeUiState(isTableView = isTableView) }
).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
这会创建一个热 StateFlow,当订阅时,开始收集上游流(合并流,在收集时开始从其上游流收集)。