封装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

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1,316 @@
<resources>
<!-- Onboarding -->
<string name="continue_text">继续</string>
<string name="skip_text">跳过</string>
<string name="get_start_text">开始使用</string>
<string name="onboarding_welcome_title">欢迎使用 TaskTTL</string>
<string name="onboarding_welcome_desc">一个简洁而强大的任务管理工具,帮助您高效管理日常任务和重要日期</string>
<string name="onboarding_smart_title">智能任务管理</string>
<string name="onboarding_smart_desc">创建、分类和跟踪您的任务。设置优先级,添加截止日期,让工作更有条理</string>
<string name="onboarding_dates_title">重要日期提醒</string>
<string name="onboarding_dates_desc">设置重要日期的倒数计时,永远不会错过生日、纪念日或重要的截止日期</string>
<string name="onboarding_ready_title">准备就绪!</string>
<string name="onboarding_ready_desc">现在您可以开始创建第一个任务,让我们一起提高工作效率吧!</string>
<!-- 应用信息 -->
<string name="app_name">TaskTTL</string>
<string name="app_name_description">任务管理与倒数日应用</string>
<string name="app_name_remark">让每一天都更有意义</string>
<string name="version">版本</string>
<string name="build_version">构建版本</string>
<!-- 导航栏 -->
<string name="nav_todo">待办</string>
<string name="nav_countdown">倒数日</string>
<string name="nav_statistics">统计</string>
<string name="nav_settings">设置</string>
<!-- 任务模块 -->
<string name="search_placeholder">搜索任务...</string>
<string name="title_task">我的任务</string>
<string name="title_task_info">任务详情</string>
<string name="title_add_task">添加任务</string>
<string name="title_edit_task">编辑任务</string>
<string name="label_task_list">任务列表</string>
<string name="label_show_completed">显示已完成</string>
<string name="text_no_tasks">暂无任务</string>
<string name="text_add_task_hint">点击右下角按钮添加新任务</string>
<string name="desc_completed">已完成</string>
<string name="desc_incomplete">未完成</string>
<string name="title_task_title">任务标题</string>
<string name="title_task_description">任务描述</string>
<string name="title_select_category">选择分类</string>
<string name="title_priority">优先级</string>
<string name="title_due_date">截止日期(可选)</string>
<string name="desc_select_date">选择日期</string>
<string name="title_tags">标签(用逗号分隔)</string>
<string name="hint_tags">例如:重要,紧急,工作</string>
<string name="text_task_not_found">任务不存在</string>
<string name="label_due_date">截止日期:</string>
<string name="label_none"></string>
<string name="label_created_at">创建时间:</string>
<string name="label_description">任务描述</string>
<string name="task_add_success">任务添加成功</string>
<string name="task_add_failed">添加任务失败</string>
<string name="task_update_success">任务更新成功</string>
<string name="task_update_failed">更新任务失败</string>
<string name="task_delete_success">任务删除成功</string>
<string name="task_delete_failed">删除任务失败</string>
<string name="task_load_failed">加载任务失败</string>
<string name="task_query_failed">查询任务失败</string>
<string name="task_status_update_success">更新任务状态成功</string>
<string name="task_status_update_failed">更新任务状态失败</string>
<!-- 倒数日模块 -->
<string name="title_countdown">倒数日</string>
<string name="title_countdown_info">倒数日详情</string>
<string name="title_add_countdown">添加倒数日</string>
<string name="title_edit_countdown">编辑倒数日</string>
<string name="label_countdown_list">倒数日列表</string>
<string name="label_days"></string>
<string name="text_no_countdowns">暂无倒数日</string>
<string name="text_add_countdown_tip">点击右下角按钮添加新的倒数日</string>
<string name="desc_add_countdown">添加倒数日</string>
<string name="label_countdown_title">倒数日标题</string>
<string name="label_countdown_description">倒数日描述</string>
<string name="label_target_date">目标日期</string>
<string name="label_notification_setting">通知设置</string>
<string name="countdown_not_found">倒数日不存在</string>
<string name="event_description">事件描述</string>
<string name="detail_information">详细信息</string>
<string name="reminder">提醒</string>
<string name="countdown_add_success">倒数日添加成功</string>
<string name="countdown_add_failed">添加倒数日失败</string>
<string name="countdown_update_success">倒数日更新成功</string>
<string name="countdown_update_failed">更新倒数日失败</string>
<string name="countdown_delete_success">倒数日删除成功</string>
<string name="countdown_delete_failed">删除倒数日失败</string>
<string name="countdown_load_failed">加载倒数日失败</string>
<string name="countdown_query_failed">查询倒数日失败</string>
<!-- 统计模块 -->
<string name="title_statistics">统计</string>
<string name="overview">总览</string>
<string name="category_statistics">分类统计</string>
<string name="total_tasks">总任务</string>
<string name="completed">已完成</string>
<string name="completion_rate">完成率</string>
<string name="total_countdowns">倒数日总数</string>
<string name="active">活跃中</string>
<!-- 分类模块 -->
<string name="category_task">任务</string>
<string name="category_countdown">倒数日</string>
<string name="title_category">分类管理</string>
<string name="title_add_category">添加分类</string>
<string name="title_edit_category">编辑分类</string>
<string name="label_no_category">暂无分类</string>
<string name="label_add_category_hint">点击右下角按钮添加新分类</string>
<string name="label_category_name">分类名称</string>
<string name="placeholder_category_name">输入分类名称...</string>
<string name="label_category_type">分类类型</string>
<string name="label_select_color">选择颜色</string>
<string name="label_select_icon">选择图标</string>
<string name="label_task_category">任务分类</string>
<string name="label_countdown_category">倒数日分类</string>
<string name="label_task_count">%1$d 个任务</string>
<string name="label_countdown_count">%1$d 个倒数日</string>
<!-- 分类操作反馈 -->
<string name="category_add_success">分类添加成功</string>
<string name="category_add_failed">添加分类失败</string>
<string name="category_update_success">分类更新成功</string>
<string name="category_update_failed">更新分类失败</string>
<string name="category_delete_success">分类删除成功</string>
<string name="category_delete_failed">删除分类失败</string>
<string name="category_load_failed">加载分类失败</string>
<string name="category_stat_failed">加载统计数据失败</string>
<string name="category_init_success">默认分类初始化成功</string>
<string name="category_init_failed">初始化默认分类失败</string>
<string name="category_not_found">查询分类失败</string>
<string name="category_count_update_failed">更新分类计数失败</string>
<!-- 通用操作 -->
<string name="enter">进入</string>
<string name="edit">编辑</string>
<string name="cancel">取消</string>
<string name="confirm">确定</string>
<string name="delete">删除</string>
<string name="export">导出</string>
<string name="import">导入</string>
<string name="back">返回</string>
<string name="action">操作</string>
<string name="search">搜索</string>
<string name="clear_text">清除</string>
<string name="all_text">全部</string>
<string name="retry">重试</string>
<string name="choose_file">选择文件</string>
<string name="error_title">错误</string>
<string name="loading">正在加载...</string>
<string name="webview_loading_error">加载失败,请检查网络连接</string>
<!-- 数据管理 -->
<string name="title_data_management">数据管理</string>
<string name="title_export_data">导出数据</string>
<string name="desc_export_data">将所有任务和倒数日导出为文件</string>
<string name="title_import_data">导入数据</string>
<string name="desc_import_data">从文件导入任务和倒数日</string>
<string name="title_auto_backup">自动备份</string>
<string name="desc_auto_backup">定期自动备份数据到云端</string>
<string name="title_clear_all_data">清除所有数据</string>
<string name="desc_clear_all_data">删除所有任务、倒数日和设置</string>
<string name="desc_clear_all_data_dialog">此操作将删除所有任务、倒数日和设置,且无法恢复。</string>
<string name="title_clear_completed_tasks">清理已完成任务</string>
<string name="title_clear_expired_countdowns">清理过期倒数日</string>
<string name="desc_clear_completed_tasks">删除所有已完成的任务</string>
<string name="desc_clear_expired_countdowns">删除所有已过期的倒数日</string>
<string name="title_backup_restore">备份与恢复</string>
<string name="title_data_clean">数据清理</string>
<string name="label_select_import_file">选择要导入的文件</string>
<string name="label_select_file">选择文件</string>
<string name="label_select_export_format">选择导出格式</string>
<string name="label_json_format">JSON格式</string>
<string name="label_csv_format">CSV格式</string>
<!-- 设置模块 -->
<string name="title_app_settings">应用设置</string>
<string name="section_general_settings">通用设置</string>
<string name="section_data_management">数据管理</string>
<string name="section_social_share">社交分享</string>
<string name="section_help_feedback">帮助与反馈</string>
<string name="setting_push_notification">推送通知</string>
<string name="setting_push_notification_desc">接收任务和倒数日提醒</string>
<string name="setting_dark_mode">深色模式</string>
<string name="setting_dark_mode_desc">使用深色主题</string>
<string name="setting_language">语言设置</string>
<string name="setting_language_desc">简体中文</string>
<string name="setting_category_management">分类管理</string>
<string name="setting_category_management_desc">管理分类</string>
<string name="setting_data_management">数据管理</string>
<string name="setting_data_management_desc">备份和恢复数据</string>
<string name="setting_share_achievement">分享成就</string>
<string name="setting_share_achievement_desc">分享任务完成成就</string>
<string name="setting_invite_friend">推荐给朋友</string>
<string name="setting_invite_friend_desc">邀请朋友使用 TaskMaster</string>
<string name="setting_feedback">意见反馈</string>
<string name="setting_feedback_desc">告诉我们您的想法</string>
<string name="setting_privacy_policy">隐私政策</string>
<string name="setting_privacy_policy_desc">了解我们如何保护您的数据</string>
<string name="setting_privacy_rate">应用评价</string>
<string name="setting_privacy_rate_desc">如果喜欢,欢迎在商店留下五星好评</string>
<string name="setting_about_app">关于应用</string>
<string name="app_version_code">100001</string>
<string name="app_version_name">1.0.1</string>
<string name="setting_about_app_desc">版本 1.0.1</string>
<!-- 反馈与帮助 -->
<string name="title_feedback">意见反馈</string>
<string name="feedback_type">反馈类型</string>
<string name="feedback_issue">问题反馈</string>
<string name="feedback_suggestion">功能建议</string>
<string name="feedback_description">问题描述</string>
<string name="feedback_placeholder">请详细描述您遇到的问题或建议...</string>
<string name="feedback_contact">联系方式(可选)</string>
<string name="feedback_contact_placeholder">您的邮箱地址,方便我们回复</string>
<string name="feedback_submitted">感谢您的反馈!我们会尽快处理。</string>
<string name="feedback_error_empty">请填写反馈内容</string>
<string name="button_send_feedback">发送反馈</string>
<!-- 关于页面 -->
<string name="title_about">关于</string>
<string name="app_intro_title">应用介绍</string>
<string name="app_intro_content">TaskTTL 是一款现代化的任务管理与倒数日应用,\n支持分类管理、优先级设置与统计分析让生活更有条理。</string>
<string name="title_privacy">隐私协议</string>
<string name="tech_stack">技术栈</string>
<string name="tech_stack_kmp">Kotlin Multiplatform跨平台开发框架</string>
<string name="tech_stack_compose">Jetpack Compose现代化 UI 框架)</string>
<string name="tech_stack_room">Room Database本地存储</string>
<string name="tech_stack_koin">Koin依赖注入框架</string>
<string name="tech_stack_ktor">Ktor网络请求</string>
<string name="tech_stack_mvi">MVI Architecture响应式架构模式</string>
<string name="developer_text">开发者</string>
<string name="devttl_team">DevTTL 团队</string>
<string name="contact_us">联系我们</string>
<string name="email_text">电子邮箱</string>
<string name="setting_privacy_email_uri">mailto:%1$s</string>
<string name="email">admin@devttl.com</string>
<string name="web_text">官方网站</string>
<string name="web_url">https://devttl.com</string>
<string name="copyright_year">2025</string>
<string name="all_rights_reserved">保留所有权利</string>
<string name="priority_low"></string>
<string name="priority_medium"></string>
<string name="priority_high"></string>
<string name="priority_urgent">紧急</string>
<string name="reminder_once">一次</string>
<string name="reminder_daily">每天</string>
<string name="reminder_weekly">每周</string>
<string name="reminder_monthly">每月</string>
<string name="reminder_off">关闭</string>
<!-- 工作与学习 -->
<string name="category_briefcase">工作</string>
<string name="category_book">学习</string>
<string name="category_exam">考试</string>
<string name="category_project">项目</string>
<!-- 生活与家庭 -->
<string name="category_home">家庭</string>
<string name="category_coffee">休闲</string>
<string name="category_shopping">购物</string>
<string name="category_food">美食</string>
<string name="category_cleaning">家务</string>
<!-- 健康与运动 -->
<string name="category_heart">健康</string>
<string name="category_dumbbell">健身</string>
<string name="category_sleep">作息</string>
<!-- 娱乐与兴趣 -->
<string name="category_music">音乐</string>
<string name="category_gamepad">娱乐</string>
<string name="category_camera">摄影</string>
<string name="category_movie">影视</string>
<!-- 出行与旅行 -->
<string name="category_car">出行</string>
<string name="category_plane">旅行</string>
<string name="category_walk">步行</string>
<!-- 节日与纪念 -->
<string name="category_birthday">生日</string>
<string name="category_festival">节日</string>
<string name="category_anniversary">纪念日</string>
<!-- 财务与计划 -->
<string name="category_money">理财</string>
<string name="category_goal">目标</string>
<string name="category_reminder">提醒</string>
<string name="privacy_url">https://sites.google.com/view/taskttl/privacy</string>
<string name="feedback_success">反馈成功</string>
<string name="feedback_error">反馈失败,请检查网络连接或稍后重试</string>
</resources>

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
)
}
}
}
}