基于日历和 Scrum 的任务管理应用不仅需要统一设计如何保存日程,还需要设计如何在多个设备间同步、管理地图位置以及控制广告 SDK 的生命周期。在集成了 Google Maps、Firebase 和 AdMob 的 Android 应用中,至关重要的是将数据层、领域逻辑、UI 和外部服务集成分开,而不是将每个 SDK 直接链接到屏幕上。
设计原则:定义作为任务基础的数据模型,并区分本地状态和 Firebase 同步状态。地图和广告的生命周期将作为独立功能进行管理,并且该架构能够确保即使出现网络断开、权限被拒绝或 SDK 初始化失败等情况,应用的主要任务操作也能继续进行。
虽然日历应用程序和基于 Scrum 的任务管理系统可能具有相似的显示格式,但它们处理的数据含义却不同。
日历约会需要开始日期、结束日期和时区。而 Scrum 工作项则需要状态、优先级、迭代周期和负责人等属性。试图将所有这些属性都塞进一个庞大的模型中,会增加每个屏幕上的条件分支数量,从而难以维护数据一致性。
Kotlin 允许您清晰地定义计划时间信息,同时保持通用标识符和任务属性。
import java.time.Instant
enum class TaskStatus {
TODO,
IN_PROGRESS,
DONE}
data class PlannerTask(
val id: String,
val title: String,
val status: TaskStatus,
val dueAt: Instant?,
val updatedAt: Long,
val syncRevision: Long)
此模型是表示基本任务状态的示例。在实际产品中,您应该将全天预约、重复预约、迭代周期、负责人、删除状态等视为单独的字段或相关模型。
尤其需要注意的是,如果使用相同的时间戳来表示全天事件和特定时间发生的事件,在切换时区时很容易导致日期不一致。因此,根据事件的具体含义选择合适的格式至关重要,例如,全天事件使用日历日期,而特定时间发生的事件则使用相应的日期和时间表示方法。
对于任务管理应用程序而言,至关重要的是,即使在网络连接不稳定的环境中,例如在地铁上或旅行途中,用户也能继续输入数据。
使用 Cloud Firestore 时,本地缓存和服务器上的数据状态并不总是相同的。您不应将本地可编辑的数据等同于已成功同步到服务器的数据。
例如,在用户界面中,区分以下几种状态:
已保存/已同步:数据已确认写入服务器。
本地更改已生效:本地已反映更改内容,但与服务器的同步尚未完成。
同步错误:由于身份验证撤销、网络故障或权限不足等原因,该过程失败。
潜在冲突:同一任务在多个设备上进行了编辑,因此需要对更新进行调整。
虽然 Firestore 的离线功能很方便,但它不会自动定义应用程序特定的冲突解决规则。例如,如果同一任务的标题和截止日期在不同设备上发生更改,应用程序需要决定哪个优先。
虽然简单地根据最后修改时间覆盖数据很容易实现,但这可能会导致丢失意外更改。对于关键工作项,请考虑字段级更新、变更跟踪和冲突通知。
此外,您不应无条件updatedAt信任客户端发送的任何内容。您需要使用可信服务器,并设计包含更新权限的安全规则。syncRevision
直接执行从屏幕点击到 Firestore 读/写操作的所有操作,可以让您专注于处理和测试 UI 中的通信错误。
在 Android 中,使用 Repository 和 ViewModel 将数据访问与屏幕状态分离是一种有效的设计方法。
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class PlannerUiState(
val isSaving: Boolean = false,
val errorMessage: String? = null)
interface PlannerRepository {
suspend fun updateTask(task: PlannerTask)}
class PlannerViewModel(
private val repository: PlannerRepository) : ViewModel() {
private val _uiState = MutableStateFlow(PlannerUiState())
val uiState: StateFlow<PlannerUiState> = _uiState.asStateFlow()
fun saveTask(task: PlannerTask) {
viewModelScope.launch {
_uiState.value = PlannerUiState(isSaving = true)
try {
repository.updateTask(task)
_uiState.value = PlannerUiState()
} catch (e: Exception) {
_uiState.value = PlannerUiState(
errorMessage = "タスクを保存できませんでした。"
)
}
}
}}
这段代码是一个框架,用于分离用户界面和存储库的职责。PlannerRepository具体的实现、离线保存、重试、取消和冲突解决等机制需要单独设计。
此外,区分本地写入 Firestore 成功和服务器接受写入操作这两个时间点至关重要。在实际的用户界面中,根据存储库返回的结果和同步状态来显示保存过程的进度是合适的。
将地点与预约关联起来的功能,将保存地址和坐标的过程与显示地图的过程分开。
例如,保存任务的经纬度位置信息后,您可以将其用于在地图上显示标记,或在另一个地图屏幕上重新显示。但是,保存坐标和获取设备的当前位置是两个独立的功能。
如果需要用户当前位置信息,系统会请求位置权限;如果权限被拒绝,用户仍然可以手动输入地址或在地图上选择位置。如果用户只需要管理已安排的位置,则无需持续访问位置信息。
每次打开地图屏幕都无条件地重复初始化过程会使生命周期管理变得复杂。通过将地图 SDK 的初始化、屏幕状态的恢复和标记的更新分开,即使在屏幕重新生成或设备旋转后,也能正确维护状态。
此外,使用 Google Maps Platform 时,您应该配置 API 密钥限制、选择要使用的 API 并监控使用情况。即使您的应用中包含 API 密钥,也不应分发无限量的密钥,而应根据 Android 应用的识别信息和目标 API 应用相应的限制。
广告 SDK 的生命周期与日历和任务的主要功能不同。
该应用不会在每次显示广告时都重置整个应用,而是在应用启动时执行必要的初始化,并在适当的时间管理广告的加载、显示和丢弃。
在屏幕上放置横幅广告时,系统会根据屏幕尺寸变化、屏幕切换和 Activity 销毁等情况来处理广告视图的生命周期。对于全屏广告(例如插页式广告),系统会检查已加载的广告是否仍然有效,并在广告显示完毕或过期后考虑重新加载。
防止广告加载失败影响任务保存或日历显示至关重要。设计系统时,应确保即使广告加载失败,用户仍然可以创建和编辑任务。
此外,我们将审查广告 SDK 的初始化顺序、用户同意管理、隐私设置以及广告 ID 的使用,以确保其符合目标地区和 Google Play 的要求。在需要用户同意的情况下,我们将实施相应的同意流程,而不是预先加载广告。
将日历和 Scrum 功能集成在一起可能会导致同一个任务在多个屏幕上以不同状态显示的问题。
例如,如果您在日历屏幕上更改了截止日期,但 Scrum 看板显示的内容仍然过时,则可能是因为每个屏幕都使用各自独立的数据。与其在每个屏幕上重复管理显示数据,不如将数据结构化,使其源自同一个数据源,这样更容易保持一致性。
为了进行验证,我们将准备以下场景:
在离线状态下创建一个任务,并在连接恢复后检查同步状态是否发生变化。
在两台设备上编辑同一任务,并验证如果出现冲突,结果是否符合既定规则。
更改时区,并检查全天和已安排的预约是否按预期显示。
即使地图权限被拒绝,是否仍然可以手动输入位置或编辑现有任务。
即使广告 SDK 初始化或加载失败,也应验证任务是否可以保存和显示。
这些测试结果会记录设备信息、安卓版本、网络状态和认证状态。与其填写未实际测量的同步速度和成功率的估计值,不如记录重现步骤和实际结果,这样便于故障排除。
在考虑集成日历、任务管理、地图、广告和云同步等功能的项目时,例如 Done | Pro calendar | planner | google maps | admob | firebase | android | scrum,不仅要检查功能数量,还要检查各个组件之间的职责以及它们在发生故障时的行为。在查看相关 Android 项目的示例配置时,务必在将其纳入设计决策之前,检查代码中的实际依赖项、Firebase 数据结构、广告初始化方法和权限处理。
实施和发布前的检查清单
数据模型:它能否区分全天预约和特定时间预约,并在时区变更后仍能保持预定的日期和时间?
Firebase Sync:您是否已验证离线更改、服务器同步、跨多个设备的冲突以及身份验证撤销等方面的行为?
Google 地图:您是否已确认位置访问权限和拒绝权限,正确限制了 API 密钥,并准备了地图功能不可用时的替代方法?
AdMob:将广告初始化、加载和销毁与屏幕生命周期分离,以确保广告故障不会中断核心功能。
集成测试:即使同时出现离线、权限被拒绝、屏幕重新生成、时区更改和广告加载失败等情况,任务状态是否保持一致?