Document best practices for Clock/Time handling (#6340)

This commit is contained in:
Patrick Honkonen 2026-01-09 09:53:38 -05:00 committed by GitHub
parent 557b667dab
commit 7bfd4b5a6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 133 additions and 0 deletions

View File

@ -11,6 +11,7 @@ Quick reference for Bitwarden Android architectural patterns during code reviews
- [Hilt Dependency Injection](#hilt-dependency-injection)
- [ViewModels](#viewmodels)
- [Repositories and Managers](#repositories-and-managers)
- [Clock/Time Handling](#clocktime-handling)
- [Module Organization](#module-organization)
- [Error Handling](#error-handling)
- [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)
@ -210,6 +211,43 @@ abstract class DataModule {
---
### Clock/Time Handling
Time-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.
**✅ GOOD - Injected Clock**:
```kotlin
// ViewModel with Clock injection
class MyViewModel @Inject constructor(
private val clock: Clock,
) {
fun save() {
val timestamp = clock.instant()
}
}
// Extension function with Clock parameter
fun State.getTimestamp(clock: Clock): Instant =
existingTime ?: clock.instant()
```
**❌ BAD - Static/direct calls**:
```kotlin
// Hidden dependency, non-testable
val timestamp = Instant.now()
val dateTime = DateTime.now()
```
**Key Rules**:
- Inject `Clock` via Hilt constructor (like other dependencies)
- Pass `Clock` as parameter to extension functions
- `Clock` is provided via `CoreModule` as singleton
- Enables deterministic testing with `Clock.fixed(...)`
Reference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`
---
## Module Organization
```
@ -299,6 +337,7 @@ Reference: `docs/ARCHITECTURE.md#error-handling`
- [ ] Business logic in Repository, not ViewModel?
- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?
- [ ] Injecting interfaces, not implementations?
- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?
- [ ] Correct module placement?
### Error Handling

View File

@ -773,3 +773,97 @@ Special consideration should be taken to avoid unnecessary recompositions. There
- https://developer.android.com/jetpack/compose/performance/bestpractices
- https://getstream.io/blog/jetpack-compose-guidelines/
- https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/
## Best Practices : Time and Clock Handling
To ensure testability and deterministic behavior, all code that needs the current time should use an injected `Clock` rather than calling `Instant.now()` or `DateTime.now()` directly.
### Why
- Direct calls to `Instant.now()` or `DateTime.now()` create non-deterministic behavior
- Testing requires brittle `mockkStatic` that can interfere across tests
- Injected `Clock` enables deterministic testing with `Clock.fixed(...)`
- Follows the dependency injection principle (no hidden dependencies)
### Pattern
**In ViewModels and classes with DI:**
```kotlin
// Good: Clock injected via Hilt
class MyViewModel @Inject constructor(
private val clock: Clock,
// other dependencies...
) : BaseViewModel<...>(...) {
private fun handleSaveClick() {
val item = Item(
createdAt = clock.instant(),
// ...
)
}
}
// Bad: Direct call creates hidden dependency
class MyViewModel @Inject constructor(
// missing Clock...
) : BaseViewModel<...>(...) {
private fun handleSaveClick() {
val item = Item(
createdAt = Instant.now(), // ❌ Non-testable
// ...
)
}
}
```
**In extension functions and utilities:**
```kotlin
// Good: Accept Clock as parameter
fun SomeState.getRevisionDate(
originalItem: Item?,
clock: Clock,
): Instant = originalItem?.revisionDate ?: clock.instant()
// Bad: Hidden dependency on system clock
fun SomeState.getRevisionDate(
originalItem: Item?,
): Instant = originalItem?.revisionDate ?: Instant.now() // ❌
```
### Testing
```kotlin
// Good: Fixed clock for deterministic tests
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
@Test
fun `test time-dependent logic`() = runTest {
val viewModel = MyViewModel(
clock = FIXED_CLOCK,
// ...
)
// Test with predictable time
}
// Bad: Static mocking is fragile
mockkStatic(Instant::class) // ❌ Avoid
every { Instant.now() } returns fixedInstant
```
### Clock Provider
The `Clock` is provided via Hilt in `CoreModule`:
```kotlin
@Provides
@Singleton
fun provideClock(): Clock = Clock.systemDefaultZone()
```
Reference: `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt`