feat: 实现运动页与运动历史子页
This commit is contained in:
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewmodel.CreationExtras
|
|||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import com.placeholder.ringhealth.feature.home.HomeViewModel
|
import com.placeholder.ringhealth.feature.home.HomeViewModel
|
||||||
|
import com.placeholder.ringhealth.feature.workout.WorkoutViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel 工厂:把容器中的仓库注入各 ViewModel。
|
* ViewModel 工厂:把容器中的仓库注入各 ViewModel。
|
||||||
@@ -19,6 +20,11 @@ object AppViewModelProvider {
|
|||||||
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
|
ringDeviceRepository = ringHealthApplication().container.ringDeviceRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
initializer {
|
||||||
|
WorkoutViewModel(
|
||||||
|
healthDataRepository = ringHealthApplication().container.healthDataRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import com.placeholder.ringhealth.feature.device.DeviceSettingsPlaceholder
|
|||||||
import com.placeholder.ringhealth.feature.detail.MetricDetailPlaceholder
|
import com.placeholder.ringhealth.feature.detail.MetricDetailPlaceholder
|
||||||
import com.placeholder.ringhealth.feature.home.HomeScreen
|
import com.placeholder.ringhealth.feature.home.HomeScreen
|
||||||
import com.placeholder.ringhealth.feature.profile.ProfileScreen
|
import com.placeholder.ringhealth.feature.profile.ProfileScreen
|
||||||
import com.placeholder.ringhealth.feature.workout.WorkoutHistoryPlaceholder
|
import com.placeholder.ringhealth.feature.workout.WorkoutHistoryScreen
|
||||||
import com.placeholder.ringhealth.feature.workout.WorkoutScreen
|
import com.placeholder.ringhealth.feature.workout.WorkoutScreen
|
||||||
|
|
||||||
/** 底部导航项定义。 */
|
/** 底部导航项定义。 */
|
||||||
@@ -49,13 +49,28 @@ private val BOTTOM_TABS = listOf(
|
|||||||
BottomTab(Routes.PROFILE, R.string.tab_profile, Icons.Filled.Person),
|
BottomTab(Routes.PROFILE, R.string.tab_profile, Icons.Filled.Person),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把当前路由归到它所属的底部 Tab。
|
||||||
|
*
|
||||||
|
* 子页位于栈顶时,底栏应当保持父 Tab 点亮。这里必须与 `Routes.DETAIL_PATTERN`
|
||||||
|
* 比较 —— `NavDestination.route` 给出的是路由**模板**而非填充后的实际路由。
|
||||||
|
* 本 App 的路由是扁平注册的(没有嵌套导航图),`destination.hierarchy` 帮不上忙,
|
||||||
|
* 只能显式映射。
|
||||||
|
*/
|
||||||
|
private fun tabRouteFor(route: String?): String? = when (route) {
|
||||||
|
Routes.DETAIL_PATTERN -> Routes.HOME
|
||||||
|
Routes.DEVICE_SETTINGS -> Routes.DEVICE
|
||||||
|
Routes.WORKOUT_HISTORY -> Routes.WORKOUT
|
||||||
|
else -> route
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用主界面:Scaffold + 底部导航 + NavHost。
|
* 应用主界面:Scaffold + 底部导航 + NavHost。
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RingHealthApp(navController: NavHostController = rememberNavController()) {
|
fun RingHealthApp(navController: NavHostController = rememberNavController()) {
|
||||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = backStackEntry?.destination?.route
|
val currentRoute = tabRouteFor(backStackEntry?.destination?.route)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
@@ -81,7 +96,7 @@ fun RingHealthApp(navController: NavHostController = rememberNavController()) {
|
|||||||
HomeScreen(onOpenDetail = { metric -> navController.navigate(Routes.detail(metric)) })
|
HomeScreen(onOpenDetail = { metric -> navController.navigate(Routes.detail(metric)) })
|
||||||
}
|
}
|
||||||
composable(Routes.WORKOUT) {
|
composable(Routes.WORKOUT) {
|
||||||
WorkoutScreen()
|
WorkoutScreen(onOpenHistory = { navController.navigate(Routes.WORKOUT_HISTORY) })
|
||||||
}
|
}
|
||||||
composable(Routes.DEVICE) {
|
composable(Routes.DEVICE) {
|
||||||
DeviceScreen()
|
DeviceScreen()
|
||||||
@@ -93,7 +108,7 @@ fun RingHealthApp(navController: NavHostController = rememberNavController()) {
|
|||||||
DeviceSettingsPlaceholder()
|
DeviceSettingsPlaceholder()
|
||||||
}
|
}
|
||||||
composable(Routes.WORKOUT_HISTORY) {
|
composable(Routes.WORKOUT_HISTORY) {
|
||||||
WorkoutHistoryPlaceholder()
|
WorkoutHistoryScreen()
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Routes.DETAIL_PATTERN,
|
route = Routes.DETAIL_PATTERN,
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ object Routes {
|
|||||||
const val DEVICE_SETTINGS = "device/settings"
|
const val DEVICE_SETTINGS = "device/settings"
|
||||||
const val WORKOUT_HISTORY = "workout/history"
|
const val WORKOUT_HISTORY = "workout/history"
|
||||||
|
|
||||||
/** 指标详情路由模板,参数为 MetricType.name。 */
|
/** 路由参数的键名。必须声明在 DETAIL_PATTERN 之前:模板引用它。 */
|
||||||
const val DETAIL_PATTERN = "detail/{metric}"
|
|
||||||
const val DETAIL_ARG_METRIC = "metric"
|
const val DETAIL_ARG_METRIC = "metric"
|
||||||
|
|
||||||
|
/** 指标详情路由模板,参数为 MetricType.name。 */
|
||||||
|
const val DETAIL_PATTERN = "detail/{$DETAIL_ARG_METRIC}"
|
||||||
|
|
||||||
/** 拼出某个指标的详情路由。 */
|
/** 拼出某个指标的详情路由。 */
|
||||||
fun detail(metric: MetricType): String = "detail/${metric.name}"
|
fun detail(metric: MetricType): String = "detail/${metric.name}"
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-3
@@ -1,18 +1,64 @@
|
|||||||
package com.placeholder.ringhealth.feature.workout
|
package com.placeholder.ringhealth.feature.workout
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.placeholder.ringhealth.AppViewModelProvider
|
||||||
import com.placeholder.ringhealth.R
|
import com.placeholder.ringhealth.R
|
||||||
|
import com.placeholder.ringhealth.core.designsystem.MetricGreen
|
||||||
|
|
||||||
/** 运动历史子页(占位,待 Task 9 实现)。 */
|
/** 运动历史子页:展示全部运动记录。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkoutHistoryPlaceholder(modifier: Modifier = Modifier) {
|
fun WorkoutHistoryScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: WorkoutViewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
// 必须先判 loading 再判空(ledger Ruling R25):Mock 仓库有 200ms 延迟,
|
||||||
|
// records 在加载完成前就是 emptyList,若先判空会先闪一下「暂无运动记录」,
|
||||||
|
// 数据到了再跳成列表 —— 用户看到的是「明明有记录却说没有」。
|
||||||
|
if (state.loading) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(stringResource(R.string.placeholder_workout_history))
|
CircularProgressIndicator(color = MetricGreen)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (state.records.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize().padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.workout_empty),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
items(state.records) { record ->
|
||||||
|
WorkoutRecordCard(record = record)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,141 @@
|
|||||||
package com.placeholder.ringhealth.feature.workout
|
package com.placeholder.ringhealth.feature.workout
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.FitnessCenter
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.placeholder.ringhealth.AppViewModelProvider
|
||||||
import com.placeholder.ringhealth.R
|
import com.placeholder.ringhealth.R
|
||||||
|
import com.placeholder.ringhealth.core.common.formatCalories
|
||||||
|
import com.placeholder.ringhealth.core.common.formatClock
|
||||||
|
import com.placeholder.ringhealth.core.common.formatDate
|
||||||
|
import com.placeholder.ringhealth.core.common.formatDistance
|
||||||
|
import com.placeholder.ringhealth.core.common.formatDuration
|
||||||
|
import com.placeholder.ringhealth.core.designsystem.MetricGreen
|
||||||
|
import com.placeholder.ringhealth.core.designsystem.TextSecondary
|
||||||
|
import com.placeholder.ringhealth.core.designsystem.component.HealthCard
|
||||||
|
import com.placeholder.ringhealth.core.designsystem.component.SectionHeader
|
||||||
|
import com.placeholder.ringhealth.domain.model.WorkoutRecord
|
||||||
|
|
||||||
/** 运动页(占位,待 Task 9 实现)。 */
|
/** 运动页。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkoutScreen(modifier: Modifier = Modifier) {
|
fun WorkoutScreen(
|
||||||
|
onOpenHistory: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: WorkoutViewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
WorkoutContent(state = state, onOpenHistory = onOpenHistory, modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无状态运动页内容。 */
|
||||||
|
@Composable
|
||||||
|
private fun WorkoutContent(
|
||||||
|
state: WorkoutUiState,
|
||||||
|
onOpenHistory: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (state.loading) {
|
||||||
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text(stringResource(R.string.tab_workout))
|
CircularProgressIndicator(color = MetricGreen)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
HealthCard {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.FitnessCenter,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MetricGreen,
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.padding(start = 12.dp)) {
|
||||||
|
Text(text = stringResource(R.string.workout_start), style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.workout_start_desc),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = TextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.workout_recent),
|
||||||
|
trailing = {
|
||||||
|
TextButton(onClick = onOpenHistory) {
|
||||||
|
Text(text = stringResource(R.string.action_view_all))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(state.records) { record ->
|
||||||
|
WorkoutRecordCard(record = record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单条运动记录卡片。 */
|
||||||
|
@Composable
|
||||||
|
internal fun WorkoutRecordCard(record: WorkoutRecord, modifier: Modifier = Modifier) {
|
||||||
|
HealthCard(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(text = record.type, style = MaterialTheme.typography.titleSmall)
|
||||||
|
// 日期 + 空格 + 时间。这里**不**违反「不硬编码文案」(ledger Ruling R25):
|
||||||
|
// 两个值都由 Formatters 做本地化格式化(SimpleDateFormat + Locale.CHINA),
|
||||||
|
// 中间的连接符是一个空格,不含任何需要翻译的文字;也没有第二份资源与它重复。
|
||||||
|
Text(
|
||||||
|
text = "${formatDate(record.startTime)} ${formatClock(record.startTime)}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = TextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.workout_item_summary, formatDuration(record.durationMinutes), formatDistance(record.distanceKm)),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.workout_detail_calories, formatCalories(record.calories)),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = TextSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.placeholder.ringhealth.feature.workout
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.placeholder.ringhealth.domain.model.WorkoutRecord
|
||||||
|
import com.placeholder.ringhealth.domain.repository.HealthDataRepository
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
/** 运动页 UI 状态。 */
|
||||||
|
data class WorkoutUiState(
|
||||||
|
val loading: Boolean,
|
||||||
|
val records: List<WorkoutRecord>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 运动页 ViewModel。 */
|
||||||
|
class WorkoutViewModel(
|
||||||
|
healthDataRepository: HealthDataRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
/** 页面状态:仓库数据到达前为 loading,到达后为记录列表。 */
|
||||||
|
val uiState: StateFlow<WorkoutUiState> = healthDataRepository.observeWorkouts()
|
||||||
|
.map { WorkoutUiState(loading = false, records = it) }
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
|
||||||
|
initialValue = WorkoutUiState(loading = true, records = emptyList()),
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val STOP_TIMEOUT_MILLIS = 5_000L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,4 +36,12 @@
|
|||||||
<string name="metric_temperature">体温</string>
|
<string name="metric_temperature">体温</string>
|
||||||
<string name="metric_blood_pressure">血压</string>
|
<string name="metric_blood_pressure">血压</string>
|
||||||
<string name="metric_step">步数</string>
|
<string name="metric_step">步数</string>
|
||||||
|
|
||||||
|
<string name="workout_start">开始运动</string>
|
||||||
|
<string name="workout_start_desc">选择运动类型,记录你的训练</string>
|
||||||
|
<string name="workout_recent">最近运动</string>
|
||||||
|
<string name="workout_empty">暂无运动记录</string>
|
||||||
|
<string name="workout_item_summary">%1$s · %2$s 公里</string>
|
||||||
|
<string name="workout_detail_calories">消耗 %1$s 千卡</string>
|
||||||
|
<string name="action_view_all">查看全部</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.placeholder.ringhealth.feature.workout
|
||||||
|
|
||||||
|
import com.placeholder.ringhealth.data.mock.MockHealthDataRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [WorkoutViewModel] 的单元测试:验证加载完成后输出的运动记录列表。
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class WorkoutViewModelTest {
|
||||||
|
|
||||||
|
private val dispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
Dispatchers.setMain(dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `加载完成后输出运动记录`() = runTest(dispatcher) {
|
||||||
|
val viewModel = WorkoutViewModel(MockHealthDataRepository())
|
||||||
|
|
||||||
|
// 与 HomeViewModelTest 同理:uiState 用 WhileSubscribed 构建,
|
||||||
|
// 没有订阅者时 .value 会停在 initialValue(loading = true)。
|
||||||
|
backgroundScope.launch { viewModel.uiState.collect {} }
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val state = viewModel.uiState.value
|
||||||
|
assertFalse(state.loading)
|
||||||
|
assertEquals(5, state.records.size)
|
||||||
|
// Mock 的运动记录是倒序生成的,最近的在最前
|
||||||
|
assertEquals("户外跑步", state.records.first().type)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user