封装BaseViewModel

This commit is contained in:
Hsy
2025-10-15 15:03:16 +08:00
parent 6e8529caad
commit 05ff710872
24 changed files with 727 additions and 333 deletions

View File

@@ -0,0 +1,97 @@
package com.taskttl.core.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* 基础视图模型
* @author DevTTL
* @date 2025/10/15
* @constructor 创建[BaseViewModel]
* @param [initialState] 初始状态
*/
abstract class BaseViewModel<S : BaseUiState, I, E>(initialState: S) : ViewModel() {
// 状态流
private val _state = MutableStateFlow(initialState)
val state: StateFlow<S> = _state.asStateFlow()
// 事件流
private val _effects = MutableSharedFlow<E>()
val effects: SharedFlow<E> = _effects.asSharedFlow()
// 对外入口处理 Intent
fun processIntent(intent: I) {
viewModelScope.launch { handleIntent(intent) }
}
/**
* 处理意图的具体实现 - 子类必须实现此方法
* @param intent 意图
*/
protected abstract fun handleIntent(intent: I)
// private fun launchWithProcessing(
// showLoading: Boolean = true,
// block: suspend () -> Unit,
// ) {
// viewModelScope.launch {
// val currentState = _state.value
// if (currentState.isProcessing) return@launch // 防重入
//
// // 设置状态
// updateState { copy(isLoading = showLoading, isProcessing = true) }
// try {
// block()
// } finally {
// updateState { copy(isLoading = false, isProcessing = false) }
// }
// }
// }
/**
* 发送单向事件
*/
protected fun sendEvent(event: E) {
viewModelScope.launch { _effects.emit(event) }
}
/**
* 更新状态
*/
protected fun updateState(reduce: S.() -> S) {
viewModelScope.launch { _state.update { it.reduce() } }
}
// /**
// * 清除错误
// */
// protected fun clearError() {
// _state.value = _state.value.copy(error = null)
// }
}
/**
* 基本ui状态
* @author DevTTL
* @date 2025/10/15
* @constructor 创建[BaseUiState]
* @param [isLoading] 正在加载
* @param [isProcessing] 正在处理
* @param [error] 错误
*/
open class BaseUiState(
open val isLoading: Boolean = false,
open val isProcessing: Boolean = false,
open val error: String? = null,
)

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryStatistics
import com.taskttl.data.local.model.CategoryType
@@ -23,6 +24,9 @@ import com.taskttl.data.local.model.CategoryType
* @param [showDeleteDialog] 显示删除对话
*/
data class CategoryState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
val categories: List<Category> = emptyList(),
val editingCategory: Category? = null,
val taskCategories: List<Category> = emptyList(),
@@ -30,12 +34,10 @@ data class CategoryState(
val categoryStatistics: List<CategoryStatistics> = emptyList(),
val selectedCategory: Category? = null,
val selectedType: CategoryType = CategoryType.TASK,
val isLoading: Boolean = false,
val error: String? = null,
val showAddDialog: Boolean = false,
val showEditDialog: Boolean = false,
val showDeleteDialog: Boolean = false
)
): BaseUiState()
/**
* 类别意图

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.Countdown
@@ -17,14 +18,15 @@ import com.taskttl.data.local.model.Countdown
* @param [error] 错误
*/
data class CountdownState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
val countdowns: List<Countdown> = emptyList(),
val categories: List<Category> = emptyList(),
val editingCountdown: Countdown? = null,
val filteredCountdowns: List<Countdown> = emptyList(),
val selectedCategory: Category? = null,
val isLoading: Boolean = false,
val error: String? = null
)
): BaseUiState()
/**
* 倒数日意图

View File

@@ -1,11 +1,13 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.network.domain.req.FeedbackReq
data class FeedbackState(
val isLoading: Boolean = false,
val error: String? = null
)
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
) : BaseUiState()
sealed class FeedbackIntent {
/**

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.OnboardingPage
/**
@@ -9,9 +10,11 @@ import com.taskttl.data.local.model.OnboardingPage
* @constructor 创建[OnboardingState]
*/
data class OnboardingState(
val isLoading: Boolean = false,
val pages: List<OnboardingPage>
)
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
val pages: List<OnboardingPage> = OnboardingPage.entries,
) : BaseUiState()
/**
* 引导意图
@@ -19,26 +22,29 @@ data class OnboardingState(
* @date 2025/09/06
* @constructor 创建[OnboardingIntent]
*/
sealed class OnboardingIntent {}
sealed class OnboardingIntent {
object NextPage : OnboardingIntent()
object MarkOnboardingCompleted : OnboardingIntent()
}
/**
* 引导活动
* @author DevTTL
* @date 2025/09/06
* @constructor 创建[OnboardingEvent]
* @constructor 创建[OnboardingEffect]
*/
sealed class OnboardingEvent {
sealed class OnboardingEffect {
/**
* 下一页
* @author DevTTL
* @date 2025/09/06
*/
data object NextPage : OnboardingEvent()
data object NextPage : OnboardingEffect()
/**
* 导航Main
* @author admin
* @date 2025/10/05
*/
data object NavMain : OnboardingEvent()
data object NavMain : OnboardingEffect()
}

View File

@@ -1,5 +1,7 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
/**
* 设置状态
* @author DevTTL
@@ -9,9 +11,10 @@ package com.taskttl.data.state
* @param [error] 错误
*/
data class SettingsState(
val isLoading: Boolean = false,
val error: String? = null,
)
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
) : BaseUiState()
/**
* 设置意图
@@ -25,7 +28,7 @@ sealed class SettingsIntent {
* @author DevTTL
* @date 2025/10/14
*/
object OpenAppRating: SettingsIntent()
object OpenAppRating : SettingsIntent()
/**
* 打开网址
@@ -34,7 +37,7 @@ sealed class SettingsIntent {
* @constructor 创建[OpenUrl]
* @param [url] 网址
*/
class OpenUrl(val url:String): SettingsIntent()
class OpenUrl(val url: String) : SettingsIntent()
}

View File

@@ -1,30 +1,37 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
/**
* 启动页状态
* @author admin
* @date 2025/10/05
* @constructor 创建[SplashState]
*/
sealed interface SplashState {
/**
* 加载中
* @author admin
* @date 2025/08/11
*/
data object Loading : SplashState
data class SplashState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
) : BaseUiState()
sealed class SplashIntent {
object LoadApp : SplashIntent()
}
sealed class SplashEffect {
/**
* 导航到首页
* @author admin
* @date 2025/08/11
*/
data object NavigateToMain : SplashState
data object NavigateToMain : SplashEffect()
/**
* 导航到引导页
* @author admin
* @date 2025/08/11
*/
data object NavigateToOnboarding : SplashState
data object NavigateToOnboarding : SplashEffect()
}

View File

@@ -1,5 +1,6 @@
package com.taskttl.data.state
import com.taskttl.core.viewmodel.BaseUiState
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.Task
@@ -18,6 +19,9 @@ import com.taskttl.data.local.model.Task
* @param [showCompleted] 显示已完成
*/
data class TaskState(
override val isLoading: Boolean = false,
override val isProcessing: Boolean = false,
override val error: String? = null,
val tasks: List<Task> = emptyList(),
val categories: List<Category> = emptyList(),
val editingTask: Task? = null,
@@ -25,10 +29,8 @@ data class TaskState(
val selectedCategory: Category? = null,
val isSearch: Boolean = false,
val searchQuery: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val showCompleted: Boolean = false
)
): BaseUiState()
/**
* 任务意图

View File

@@ -1,19 +1,13 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.repository.CategoryRepository
import com.taskttl.data.state.CategoryEffect
import com.taskttl.data.state.CategoryIntent
import com.taskttl.data.state.CategoryState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -37,20 +31,15 @@ import taskttl.composeapp.generated.resources.category_update_success
* @constructor 创建[CategoryViewModel]
* @param [categoryRepository] 类别存储库
*/
class CategoryViewModel(private val categoryRepository: CategoryRepository) : ViewModel() {
private val _state = MutableStateFlow(CategoryState())
val state: StateFlow<CategoryState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<CategoryEffect>()
val effects: SharedFlow<CategoryEffect> = _effects.asSharedFlow()
class CategoryViewModel(private val categoryRepository: CategoryRepository) :
BaseViewModel<CategoryState, CategoryIntent, CategoryEffect>(CategoryState()) {
init {
handleIntent(CategoryIntent.LoadCategories)
handleIntent(CategoryIntent.LoadCategoryStatistics)
processIntent(CategoryIntent.LoadCategories)
processIntent(CategoryIntent.LoadCategoryStatistics)
}
fun handleIntent(intent: CategoryIntent) {
public override fun handleIntent(intent: CategoryIntent) {
when (intent) {
is CategoryIntent.LoadCategories -> loadCategories()
is CategoryIntent.LoadCategoriesByType -> loadCategoriesByType(intent.type)
@@ -78,25 +67,25 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
*/
private fun loadCategories() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
updateState { copy(isLoading = true, error = null) }
try {
categoryRepository.getAllCategories().collect { categories ->
val taskCategories = categories.filter { it.type == CategoryType.TASK }
val countdownCategories =
categories.filter { it.type == CategoryType.COUNTDOWN }
_state.value = _state.value.copy(
categories = categories,
taskCategories = taskCategories,
countdownCategories = countdownCategories,
isLoading = false
)
updateState {
copy(
categories = categories,
taskCategories = taskCategories,
countdownCategories = countdownCategories,
isLoading = false
)
}
}
} catch (e: Exception) {
_state.value = _state.value.copy(
isLoading = false,
error = e.message ?: getString(Res.string.category_load_failed)
)
val errStr = getString(Res.string.category_load_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -109,10 +98,10 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
val category = categoryRepository.getCategoryById(categoryId)
_state.value = _state.value.copy(editingCategory = category)
updateState { copy(editingCategory = category) }
} catch (e: Exception) {
_state.value =
_state.value.copy(error = e.message ?: getString(Res.string.category_not_found))
val errStr = getString(Res.string.category_not_found)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -127,18 +116,17 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
categoryRepository.getCategoriesByType(type).collect { categories ->
when (type) {
CategoryType.TASK -> {
_state.value = _state.value.copy(taskCategories = categories)
updateState { copy(taskCategories = categories) }
}
CategoryType.COUNTDOWN -> {
_state.value = _state.value.copy(countdownCategories = categories)
updateState { copy(countdownCategories = categories) }
}
}
}
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_load_failed)
)
val errStr = getString(Res.string.category_load_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -150,22 +138,21 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.getCategoryStatistics().collect { statistics ->
_state.value = _state.value.copy(categoryStatistics = statistics)
updateState { copy(categoryStatistics = statistics) }
}
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_stat_failed)
)
val errStr = getString(Res.string.category_stat_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun selectCategory(category: Category?) {
_state.value = _state.value.copy(selectedCategory = category)
updateState { copy(selectedCategory = category) }
}
private fun selectType(type: CategoryType) {
_state.value = _state.value.copy(selectedType = type)
updateState { copy(selectedType = type) }
}
/**
@@ -176,12 +163,11 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.insertCategory(category)
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_add_success)))
_effects.emit(CategoryEffect.NavigateBack)
sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_add_success)))
sendEvent(CategoryEffect.NavigateBack)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_add_failed)
)
val errStr = getString(Res.string.category_add_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -190,13 +176,12 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.updateCategory(category)
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_update_success)))
_effects.emit(CategoryEffect.NavigateBack)
sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_update_success)))
sendEvent(CategoryEffect.NavigateBack)
hideEditDialog()
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_update_failed)
)
val errStr = getString(Res.string.category_update_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -205,12 +190,11 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.deleteCategory(categoryId)
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_delete_success)))
sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_delete_success)))
hideDeleteDialog()
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_delete_failed)
)
val errStr = getString(Res.string.category_delete_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -219,11 +203,10 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
viewModelScope.launch {
try {
categoryRepository.initializeDefaultCategories()
_effects.emit(CategoryEffect.ShowMessage(getString(Res.string.category_init_success)))
sendEvent(CategoryEffect.ShowMessage(getString(Res.string.category_init_success)))
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_init_failed)
)
val errStr = getString(Res.string.category_init_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -233,50 +216,37 @@ class CategoryViewModel(private val categoryRepository: CategoryRepository) : Vi
try {
categoryRepository.updateCategoryCounts()
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.category_count_update_failed)
)
val errStr = getString(Res.string.category_count_update_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun showAddDialog() {
_state.value = _state.value.copy(showAddDialog = true)
updateState { copy(showAddDialog = true) }
}
private fun hideAddDialog() {
_state.value = _state.value.copy(showAddDialog = false)
updateState { copy(showAddDialog = false) }
}
private fun showEditDialog(category: Category) {
_state.value = _state.value.copy(
selectedCategory = category,
showEditDialog = true
)
updateState { copy(selectedCategory = category, showEditDialog = true) }
}
private fun hideEditDialog() {
_state.value = _state.value.copy(
selectedCategory = null,
showEditDialog = false
)
updateState { copy(selectedCategory = null, showEditDialog = false) }
}
private fun showDeleteDialog(category: Category) {
_state.value = _state.value.copy(
selectedCategory = category,
showDeleteDialog = true
)
updateState { copy(selectedCategory = category, showDeleteDialog = true) }
}
private fun hideDeleteDialog() {
_state.value = _state.value.copy(
selectedCategory = null,
showDeleteDialog = false
)
updateState { copy(selectedCategory = null, showDeleteDialog = false) }
}
private fun clearError() {
_state.value = _state.value.copy(error = null)
updateState { copy(error = null) }
}
}

View File

@@ -1,7 +1,7 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.local.model.Countdown
@@ -10,12 +10,6 @@ import com.taskttl.data.repository.CountdownRepository
import com.taskttl.data.state.CountdownEffect
import com.taskttl.data.state.CountdownIntent
import com.taskttl.data.state.CountdownState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -37,20 +31,15 @@ import taskttl.composeapp.generated.resources.countdown_update_success
*/
class CountdownViewModel(
private val countdownRepository: CountdownRepository,
private val categoryRepository: CategoryRepository
) : ViewModel() {
private val categoryRepository: CategoryRepository,
) : BaseViewModel<CountdownState, CountdownIntent, CountdownEffect>(CountdownState()) {
private val _state = MutableStateFlow(CountdownState())
val state: StateFlow<CountdownState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<CountdownEffect>()
val effects: SharedFlow<CountdownEffect> = _effects.asSharedFlow()
init {
handleIntent(CountdownIntent.LoadCountdowns)
processIntent(CountdownIntent.LoadCountdowns)
}
fun handleIntent(intent: CountdownIntent) {
public override fun handleIntent(intent: CountdownIntent) {
when (intent) {
is CountdownIntent.LoadCountdowns -> loadCountdowns()
is CountdownIntent.GetCountdownById -> getCountdownById(intent.countdownId)
@@ -64,29 +53,27 @@ class CountdownViewModel(
private fun loadCountdowns() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
updateState { copy(isLoading = true) }
try {
launch {
categoryRepository.getCategoriesByType(CategoryType.COUNTDOWN)
.collect { categories ->
_state.value = _state.value.copy(categories = categories)
}
.collect { categories -> updateState { copy(categories = categories) } }
}
launch {
countdownRepository.getAllCountdowns().collect { countdowns ->
_state.value = _state.value.copy(
countdowns = countdowns,
filteredCountdowns = filterCountdowns(countdowns),
isLoading = false
)
updateState {
copy(
countdowns = countdowns,
filteredCountdowns = filterCountdowns(countdowns)
)
}
}
}
} catch (e: Exception) {
_state.value =
_state.value.copy(
isLoading = false,
error = e.message ?: getString(Res.string.countdown_load_failed)
)
val errStr = getString(Res.string.countdown_load_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
} finally {
updateState { copy(isLoading = false) }
}
}
}
@@ -96,11 +83,10 @@ class CountdownViewModel(
viewModelScope.launch {
try {
val countdown = countdownRepository.getCountdownById(countdownId)
_state.value = _state.value.copy(editingCountdown = countdown)
updateState { copy(editingCountdown = countdown) }
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.countdown_query_failed)
)
val errStr = getString(Res.string.countdown_query_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -109,12 +95,11 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.insertCountdown(countdown)
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_add_success)))
_effects.emit(CountdownEffect.NavigateBack)
sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_add_success)))
sendEvent(CountdownEffect.NavigateBack)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.countdown_add_failed)
)
val errStr = getString(Res.string.countdown_add_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -123,11 +108,11 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.updateCountdown(countdown)
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_update_success)))
sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_update_success)))
sendEvent(CountdownEffect.NavigateBack)
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.countdown_update_failed)
)
val errStr = getString(Res.string.countdown_update_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -136,26 +121,29 @@ class CountdownViewModel(
viewModelScope.launch {
try {
countdownRepository.deleteCountdown(countdownId)
_effects.emit(CountdownEffect.ShowMessage(getString(Res.string.countdown_delete_success)))
sendEvent(CountdownEffect.ShowMessage(getString(Res.string.countdown_delete_success)))
} catch (e: Exception) {
_state.value = _state.value.copy(error = e.message ?: getString(Res.string.countdown_delete_failed))
val errStr = getString(Res.string.countdown_delete_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
private fun filterByCategory(category: Category?) {
_state.value = _state.value.copy(
selectedCategory = category,
filteredCountdowns = filterCountdowns(_state.value.countdowns)
)
updateState {
copy(
selectedCategory = category,
filteredCountdowns = filterCountdowns(state.value.countdowns)
)
}
}
private fun clearError() {
_state.value = _state.value.copy(error = null)
updateState { copy(error = null) }
}
private fun filterCountdowns(countdowns: List<Countdown>): List<Countdown> {
val currentState = _state.value
val currentState = state.value
return countdowns.filter { countdown ->
currentState.selectedCategory?.let { countdown.category == it } ?: true
}

View File

@@ -1,19 +1,12 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.network.TaskTTLApi
import com.taskttl.data.network.domain.req.FeedbackReq
import com.taskttl.data.state.FeedbackEffect
import com.taskttl.data.state.FeedbackIntent
import com.taskttl.data.state.FeedbackState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -26,16 +19,11 @@ import taskttl.composeapp.generated.resources.feedback_success
* @date 2025/10/12
* @constructor 创建[FeedbackViewModel]
*/
class FeedbackViewModel() : ViewModel() {
private val _state = MutableStateFlow(FeedbackState())
val state: StateFlow<FeedbackState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<FeedbackEffect>()
val effects: SharedFlow<FeedbackEffect> = _effects.asSharedFlow()
class FeedbackViewModel() :
BaseViewModel<FeedbackState, FeedbackIntent, FeedbackEffect>(FeedbackState()) {
fun handleIntent(intent: FeedbackIntent) {
public override fun handleIntent(intent: FeedbackIntent) {
when (intent) {
is FeedbackIntent.SubmitFeedback -> submitFeedback(intent.feedback)
is FeedbackIntent.ClearError -> clearError()
@@ -44,20 +32,17 @@ class FeedbackViewModel() : ViewModel() {
private fun submitFeedback(feedback: FeedbackReq) {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
if (state.value.isProcessing) return@launch
updateState { copy(isLoading = true, isProcessing = true) }
try {
delay(10_000)
TaskTTLApi.postFeedback(feedback)
_effects.emit(FeedbackEffect.ShowMessage(getString(Res.string.feedback_success)))
_state.value = _state.value.copy(isLoading = false)
_effects.emit(FeedbackEffect.NavigateBack)
sendEvent(FeedbackEffect.ShowMessage(getString(Res.string.feedback_success)))
sendEvent(FeedbackEffect.NavigateBack)
} catch (e: Exception) {
_state.value = _state.value.copy(isLoading = false, error = e.message)
_effects.emit(
FeedbackEffect.ShowMessage(
e.message ?: getString(Res.string.feedback_error)
)
)
val errStr = getString(Res.string.feedback_error)
sendEvent(FeedbackEffect.ShowMessage(e.message ?: errStr))
} finally {
updateState { copy(isLoading = false, isProcessing = false) }
}
}
@@ -67,7 +52,7 @@ class FeedbackViewModel() : ViewModel() {
* 清除错误
*/
private fun clearError() {
_state.value = _state.value.copy(error = null)
updateState { copy(error = null) }
}
}

View File

@@ -1,13 +1,12 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.repository.CategoryRepository
import com.taskttl.data.repository.OnboardingRepository
import com.taskttl.data.state.OnboardingEvent
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import com.taskttl.data.state.OnboardingEffect
import com.taskttl.data.state.OnboardingIntent
import com.taskttl.data.state.OnboardingState
import kotlinx.coroutines.launch
/**
@@ -20,30 +19,37 @@ import kotlinx.coroutines.launch
*/
class OnboardingViewModel(
private val onboardingRepository: OnboardingRepository,
private val categoryRepository: CategoryRepository
) : ViewModel() {
private val categoryRepository: CategoryRepository,
) : BaseViewModel<OnboardingState, OnboardingIntent, OnboardingEffect>(initialState = OnboardingState()) {
private val _events = Channel<OnboardingEvent>()
val events: Flow<OnboardingEvent> = _events.receiveAsFlow()
override fun handleIntent(intent: OnboardingIntent) {
when (intent) {
is OnboardingIntent.NextPage -> nextPage()
is OnboardingIntent.MarkOnboardingCompleted -> markOnboardingCompleted()
}
}
/**
* 发送事件 - 提供统一的事件发送机制
* @param event 事件
* 下一页
*/
fun sendEvent(event: OnboardingEvent) {
viewModelScope.launch {
_events.trySend(event)
}
private fun nextPage() {
sendEvent(OnboardingEffect.NextPage)
}
/**
* 标记引导完成
*/
fun markOnboardingCompleted() {
private fun markOnboardingCompleted() {
viewModelScope.launch {
categoryRepository.initializeDefaultCategories()
onboardingRepository.markLaunched()
_events.trySend(OnboardingEvent.NavMain)
try {
if (state.value.isProcessing) return@launch
updateState { copy(isLoading = false, isProcessing = true) }
categoryRepository.initializeDefaultCategories()
onboardingRepository.markLaunched()
sendEvent(OnboardingEffect.NavMain)
} finally {
updateState { copy(isLoading = false, isProcessing = false) }
}
}
}
}
}

View File

@@ -1,17 +1,11 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.utils.ExternalAppLauncher
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.state.SettingsEffect
import com.taskttl.data.state.SettingsIntent
import com.taskttl.data.state.SettingsState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
@@ -20,16 +14,11 @@ import kotlinx.coroutines.launch
* @date 2025/10/12
* @constructor 创建[SettingsViewModel]
*/
class SettingsViewModel() : ViewModel() {
private val _state = MutableStateFlow(SettingsState())
val state: StateFlow<SettingsState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<SettingsEffect>()
val effects: SharedFlow<SettingsEffect> = _effects.asSharedFlow()
class SettingsViewModel() :
BaseViewModel<SettingsState, SettingsIntent, SettingsEffect>(SettingsState()) {
fun handleIntent(intent: SettingsIntent) {
public override fun handleIntent(intent: SettingsIntent) {
when (intent) {
is SettingsIntent.OpenAppRating -> openAppRating()
is SettingsIntent.OpenUrl -> openUrl(intent.url)
@@ -42,7 +31,7 @@ class SettingsViewModel() : ViewModel() {
}
}
private fun openUrl(url:String) {
private fun openUrl(url: String) {
viewModelScope.launch {
ExternalAppLauncher.openUrl(url)
}
@@ -52,7 +41,7 @@ class SettingsViewModel() : ViewModel() {
* 清除错误
*/
private fun clearError() {
_state.value = _state.value.copy(error = null)
updateState { copy(error = null) }
}
}

View File

@@ -1,17 +1,15 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.domain.constant.PointEvent
import com.taskttl.core.utils.DeviceUtils
import com.taskttl.core.utils.LogUtils
import com.taskttl.core.utils.StorageUtils
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.network.TaskTTLApi
import com.taskttl.data.repository.OnboardingRepository
import com.taskttl.data.state.SplashEffect
import com.taskttl.data.state.SplashIntent
import com.taskttl.data.state.SplashState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
@@ -23,12 +21,19 @@ import kotlinx.coroutines.launch
*/
class SplashViewModel(
private val onboardingRepository: OnboardingRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow<SplashState>(SplashState.Loading)
val uiState: StateFlow<SplashState> = _uiState.asStateFlow()
) : BaseViewModel<SplashState, SplashIntent, SplashEffect>(SplashState()) {
init {
processIntent(SplashIntent.LoadApp)
}
override fun handleIntent(intent: SplashIntent) {
when (intent) {
is SplashIntent.LoadApp -> loadApp()
}
}
private fun loadApp() {
viewModelScope.launch {
DeviceUtils.getUniqueId()
@@ -40,8 +45,11 @@ class SplashViewModel(
TaskTTLApi.postPoint(PointEvent.AppLaunch)
}
val hasLaunched = onboardingRepository.isLaunchedBefore()
_uiState.value =
if (hasLaunched) SplashState.NavigateToOnboarding else SplashState.NavigateToMain
if (hasLaunched) {
sendEvent(SplashEffect.NavigateToOnboarding)
} else {
sendEvent(SplashEffect.NavigateToMain)
}
}
}
}

View File

@@ -1,8 +1,8 @@
package com.taskttl.data.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.taskttl.core.utils.LogUtils
import com.taskttl.core.viewmodel.BaseViewModel
import com.taskttl.data.local.model.Category
import com.taskttl.data.local.model.CategoryType
import com.taskttl.data.local.model.Task
@@ -11,12 +11,6 @@ import com.taskttl.data.repository.TaskRepository
import com.taskttl.data.state.TaskEffect
import com.taskttl.data.state.TaskIntent
import com.taskttl.data.state.TaskState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import taskttl.composeapp.generated.resources.Res
@@ -40,20 +34,15 @@ import taskttl.composeapp.generated.resources.task_update_success
*/
class TaskViewModel(
private val taskRepository: TaskRepository,
private val categoryRepository: CategoryRepository
) : ViewModel() {
private val categoryRepository: CategoryRepository,
) : BaseViewModel<TaskState, TaskIntent, TaskEffect>(TaskState()) {
private val _state = MutableStateFlow(TaskState())
val state: StateFlow<TaskState> = _state.asStateFlow()
private val _effects = MutableSharedFlow<TaskEffect>()
val effects: SharedFlow<TaskEffect> = _effects.asSharedFlow()
init {
handleIntent(TaskIntent.LoadTasks)
processIntent(TaskIntent.LoadTasks)
}
fun handleIntent(intent: TaskIntent) {
public override fun handleIntent(intent: TaskIntent) {
when (intent) {
is TaskIntent.LoadTasks -> loadTasks()
is TaskIntent.GetTaskById -> getTaskById(intent.taskId)
@@ -76,7 +65,7 @@ class TaskViewModel(
*/
private fun navigateBack() {
viewModelScope.launch {
_effects.emit(TaskEffect.NavigateBack)
sendEvent(TaskEffect.NavigateBack)
}
}
@@ -85,7 +74,7 @@ class TaskViewModel(
*/
private fun navigateToEditTask() {
viewModelScope.launch {
_effects.emit(TaskEffect.NavigateToEditTask)
sendEvent(TaskEffect.NavigateToEditTask)
}
}
@@ -94,31 +83,30 @@ class TaskViewModel(
*/
private fun loadTasks() {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
updateState { copy(isLoading = true, error = null) }
try {
launch {
categoryRepository.getCategoriesByType(CategoryType.TASK)
.collect { categories ->
LogUtils.e("DevTTL", categories.toString())
_state.value = _state.value.copy(categories = categories)
updateState { copy(categories = categories) }
}
}
launch {
taskRepository.getAllTasks().collect { tasks ->
_state.value = _state.value.copy(
tasks = tasks,
filteredTasks = filterTasks(tasks),
isLoading = false
)
updateState {
copy(
tasks = tasks,
filteredTasks = filterTasks(tasks),
isLoading = false
)
}
}
}
} catch (e: Exception) {
LogUtils.e("DevTTL", e.message.toString())
_state.value =
_state.value.copy(
isLoading = false,
error = e.message ?: getString(Res.string.task_load_failed)
)
val errStr = getString(Res.string.task_load_failed)
updateState { copy(isLoading = false, error = e.message ?: errStr) }
}
}
}
@@ -131,10 +119,10 @@ class TaskViewModel(
viewModelScope.launch {
try {
val task = taskRepository.getTaskById(taskId)
_state.value = _state.value.copy(editingTask = task)
updateState { copy(editingTask = task) }
} catch (e: Exception) {
_state.value =
_state.value.copy(error = e.message ?: getString(Res.string.task_query_failed))
val errStr = getString(Res.string.task_query_failed)
updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -147,11 +135,11 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.insertTask(task)
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
_effects.emit(TaskEffect.NavigateBack)
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_add_success)))
sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) {
_state.value =
_state.value.copy(error = e.message ?: getString(Res.string.task_add_failed))
val errStr = getString(Res.string.task_add_failed)
updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -164,11 +152,11 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.updateTask(task)
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
_effects.emit(TaskEffect.NavigateBack)
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_update_success)))
sendEvent(TaskEffect.NavigateBack)
} catch (e: Exception) {
_state.value =
_state.value.copy(error = e.message ?: getString(Res.string.task_update_failed))
val errStr = getString(Res.string.task_update_failed)
updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -181,10 +169,10 @@ class TaskViewModel(
viewModelScope.launch {
try {
taskRepository.deleteTask(taskId)
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_delete_success)))
} catch (e: Exception) {
_state.value =
_state.value.copy(error = e.message ?: getString(Res.string.task_delete_failed))
val errStr = getString(Res.string.task_delete_failed)
updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -196,16 +184,15 @@ class TaskViewModel(
private fun toggleTaskCompletion(taskId: String) {
viewModelScope.launch {
try {
val task = _state.value.tasks.find { it.id == taskId }
val task = state.value.tasks.find { it.id == taskId }
task?.let {
val updatedTask = it.copy(isCompleted = !it.isCompleted)
taskRepository.updateTask(updatedTask)
_effects.emit(TaskEffect.ShowMessage(getString(Res.string.task_status_update_success)))
sendEvent(TaskEffect.ShowMessage(getString(Res.string.task_status_update_success)))
}
} catch (e: Exception) {
_state.value = _state.value.copy(
error = e.message ?: getString(Res.string.task_status_update_failed)
)
val errStr = getString(Res.string.task_status_update_failed)
updateState { copy(error = e.message ?: errStr) }
}
}
}
@@ -215,8 +202,8 @@ class TaskViewModel(
* @param [category] 类别
*/
private fun filterByCategory(category: Category?) {
_state.value = _state.value.copy(selectedCategory = category)
_state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
updateState { copy(selectedCategory = category) }
updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
@@ -224,8 +211,8 @@ class TaskViewModel(
* @param [query] 怎么翻译
*/
private fun searchTasks(query: String) {
_state.value = _state.value.copy(searchQuery = query)
_state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
updateState { copy(searchQuery = query) }
updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
@@ -233,22 +220,22 @@ class TaskViewModel(
* @param [show] 显示
*/
private fun toggleShowCompleted(show: Boolean) {
_state.value = _state.value.copy(showCompleted = show)
_state.value = _state.value.copy(filteredTasks = filterTasks(_state.value.tasks))
updateState { copy(showCompleted = show) }
updateState { copy(filteredTasks = filterTasks(state.value.tasks)) }
}
/**
* 搜索视图
*/
private fun searchView() {
_state.value = _state.value.copy(isSearch = !_state.value.isSearch)
updateState { copy(isSearch = !state.value.isSearch) }
}
/**
* 清除错误
*/
private fun clearError() {
_state.value = _state.value.copy(error = null)
updateState { copy(error = null) }
}
/**
@@ -257,7 +244,7 @@ class TaskViewModel(
* @return [List<Task>]
*/
private fun filterTasks(tasks: List<Task>): List<Task> {
val currentState = _state.value
val currentState = state.value
return tasks.filter { task ->
val categoryMatch = currentState.selectedCategory?.let { task.category == it } ?: true
@@ -274,4 +261,4 @@ class TaskViewModel(
categoryMatch && searchMatch && completionMatch
}
}
}
}

View File

@@ -23,6 +23,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -32,7 +34,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.taskttl.core.routes.Routes
import com.taskttl.data.local.model.OnboardingPage
import com.taskttl.data.state.OnboardingEvent
import com.taskttl.data.state.OnboardingEffect
import com.taskttl.data.state.OnboardingIntent
import com.taskttl.data.viewmodel.OnboardingViewModel
import kotlinx.coroutines.flow.collectLatest
import org.jetbrains.compose.resources.stringResource
@@ -52,21 +55,22 @@ import taskttl.composeapp.generated.resources.skip_text
@Composable
fun OnboardingScreen(
navigatorToRoute: (Routes) -> Unit,
viewModel: OnboardingViewModel = koinViewModel()
viewModel: OnboardingViewModel = koinViewModel(),
) {
val onboardingPages = OnboardingPage.entries
val state by viewModel.state.collectAsState()
val onboardingPages = state.pages
val pagerState =
rememberPagerState(0, initialPageOffsetFraction = 0f, pageCount = { onboardingPages.size })
LaunchedEffect(Unit) {
viewModel.events.collectLatest { event ->
viewModel.effects.collectLatest { event ->
when (event) {
is OnboardingEvent.NextPage -> {
is OnboardingEffect.NextPage -> {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
is OnboardingEvent.NavMain -> {
is OnboardingEffect.NavMain -> {
navigatorToRoute(Routes.Main)
}
}
@@ -76,7 +80,7 @@ fun OnboardingScreen(
Box(modifier = Modifier.fillMaxSize()) {
// 右上角跳过
TextButton(
onClick = { viewModel.markOnboardingCompleted() },
onClick = { viewModel.processIntent(OnboardingIntent.MarkOnboardingCompleted) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 32.dp, end = 16.dp)
@@ -122,7 +126,7 @@ fun OnboardingScreen(
Button(
onClick = {
if (pagerState.currentPage < onboardingPages.lastIndex) {
viewModel.sendEvent(OnboardingEvent.NextPage)
viewModel.processIntent(OnboardingIntent.NextPage)
}
},
modifier = Modifier.fillMaxWidth(),
@@ -141,7 +145,7 @@ fun OnboardingScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = { viewModel.markOnboardingCompleted() },
onClick = { viewModel.processIntent(OnboardingIntent.MarkOnboardingCompleted) },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF667EEA)),
shape = MaterialTheme.shapes.medium

View File

@@ -21,7 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -33,7 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.taskttl.core.routes.Routes
import com.taskttl.data.state.SplashState
import com.taskttl.data.state.SplashEffect
import com.taskttl.data.viewmodel.SplashViewModel
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
@@ -51,15 +50,20 @@ import taskttl.composeapp.generated.resources.app_name_remark
@Composable
fun SplashScreen(
navigatorToRoute: (Routes) -> Unit,
viewModel: SplashViewModel = koinViewModel()
viewModel: SplashViewModel = koinViewModel(),
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(state) {
when (state) {
SplashState.NavigateToOnboarding -> navigatorToRoute(Routes.Onboarding)
SplashState.NavigateToMain -> navigatorToRoute(Routes.Main)
else -> {}
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
when (effect) {
is SplashEffect.NavigateToOnboarding -> {
navigatorToRoute(Routes.Onboarding)
}
is SplashEffect.NavigateToMain -> {
navigatorToRoute(Routes.Main)
}
}
}
}

View File

@@ -71,14 +71,14 @@ import taskttl.composeapp.generated.resources.total_tasks
fun StatisticsScreen(
navController: NavHostController,
taskViewModel: TaskViewModel = koinViewModel(),
countdownViewModel: CountdownViewModel = koinViewModel()
countdownViewModel: CountdownViewModel = koinViewModel(),
) {
val taskState by taskViewModel.state.collectAsState()
val countdownState by countdownViewModel.state.collectAsState()
LaunchedEffect(Unit) {
taskViewModel.handleIntent(TaskIntent.LoadTasks)
taskViewModel.processIntent(TaskIntent.LoadTasks)
countdownViewModel.handleIntent(CountdownIntent.LoadCountdowns)
}
@@ -220,7 +220,7 @@ private fun StatisticCard(
value: String,
icon: ImageVector,
color: Color,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier,
@@ -264,7 +264,7 @@ private fun CategoryStatisticItem(
category: Category,
totalCount: Int,
completedCount: Int,
typeRes: StringResource
typeRes: StringResource,
) {
if (totalCount == 0) return

View File

@@ -81,7 +81,7 @@ import taskttl.composeapp.generated.resources.title_task
@Preview
fun TaskScreen(
navController: NavHostController,
viewModel: TaskViewModel = koinViewModel()
viewModel: TaskViewModel = koinViewModel(),
) {
val state by viewModel.state.collectAsState()
@@ -283,7 +283,7 @@ fun TaskCardItem(
onClick: () -> Unit,
onToggleComplete: () -> Unit,
onDeleteTask: () -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
ActionButtonListItem(
modifier = Modifier
@@ -339,13 +339,29 @@ fun TaskCardItem(
if (task.description.isNotBlank()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = task.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row() {
Text(
text = task.description,
style = MaterialTheme.typography.bodySmall,
textDecoration = if (task.isCompleted) TextDecoration.LineThrough else null,
color = if (task.isCompleted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// 结束时间
task.dueDate?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = task.dueDate.toString(),
style = MaterialTheme.typography.bodySmall,
textDecoration = if (task.isCompleted) TextDecoration.LineThrough else null,
color = if (task.isCompleted) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}