Skip to content

Commit 4a916fb

Browse files
committed
Added technical documentation for the main concepts of the app
1 parent bcf0598 commit 4a916fb

13 files changed

+322
-0
lines changed

docs/DatabaseSchema.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Database schema
2+
3+
![Database Schema](images/routinetracker_db_schema.png)
4+
5+
## habitEntity
6+
7+
`habitEntity` stores the habit and it’s properties.
8+
9+
## scheduleEntity
10+
11+
Each habit must have at least one schedule, although we may later need to allow habits to have multiple schedules for different periods of time.
12+
13+
`scheduleEntity` table can accommodate many types of schedules available in the program. This is achieved through **[Single Table Inheritance](https://stackoverflow.com/a/3579462/22295134)** where subtype-specific attributes are given a `NULL` value on rows where these attributes do not apply.
14+
15+
## dueDateEntity
16+
17+
Some types of schedules have due dates defined in this table. E.g. for the `WeeklySchedule` these are days of week, for `MonthlySchedule`, days of month, for `CustomDateSchedule` - dates.
18+
19+
All values are serialized to `INTEGER`s which get deserealized to respective types of values based on the type of schedule. E.g. days of week are stored as numbers where monday is first, sunday is the seventh, dates are stored as numbers as well as the number after the epoch (`1970-01-01`).
20+
21+
There is also functionality that allows users to change the time of activities for separate dates. For example users may want to complete a habit at 5 PM on Monday, but on Wednesday they’d want to completed it on 7 PM. That’s why `dueDateEntity` also stores `completionTimeHour` and `completionTimeMinute` attributes. However, this functionality is not yet available in the release.
22+
23+
## weekDayMonthRelatedEntity
24+
25+
This is an entity for `MonthlySchedule` that allows users to define dates of recurring activities by specifying the number of day of week in the month. This can be useful for creating events that happen e.g. on the second Thursday of November.
26+
27+
## completionHistoryEntity
28+
29+
This is where habit completions are stored. We store the date of the completion and how many times the habit was completed. If the habit is of Yes/No type, it is either 0 or 1, but we also plan to introduce measurable habits in the future. There can be no more than one completion entry for the same date and the same habit.
30+
31+
## cachedStreakEntity
32+
33+
This is where cached habit streaks are stored. Whenever the program computes habit streaks, it caches them in this entity for future retrievals to improve performance. A streak is nothing more than a period with a start date and an end date.
34+
35+
## vacationEntity
36+
37+
`vacationEntity` is very similar to `cachedStreakEntity` but is used for storing habit vacations. A vacation can be just a skipped date, in this case startDate and endDate will be the same or several dates on which the habit is paused and not displayed as due.
38+
39+
## specificDateCustomCompletionTime
40+
41+
There is a functionality that allows users to set a different completion time for activities on separate dates. This is where this completion time is stored. However, this functionality is not yet available in the release.

docs/HandlingOneOffEvents.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Handling one-off events
2+
3+
One-time events such as displaying a snackbar or navigating to a different destination that are typically passed from the ViewModel to the UI are implemented using Compose State. No SharedFlows or Channels involved.
4+
5+
For example, after deleting a habit, ViewModel decides to throw an event that tells the UI to navigate to a different screen. For that it updates the respective `StateFlow` variable. The UI observes this event in a `LaunchedEffect` block, which executes the action once the state gets changed.
6+
7+
## Benefits
8+
9+
- state can't be lost in any scenario ([read more](https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95));
10+
- easy to use not only in `ViewModel`s, but also in plain state holder classes;
11+
- ensures immediate response to new events (such as when a snackbar needs to be updated immediately upon a new event, instead of waiting for the previous message to disappear).
12+
13+
## Limitations
14+
15+
One-off events are handled with state but state is not really suitable for this purpose it keeps the value even after the event is finished. So this requires us to manually reset the state after it’s consumed.
16+
17+
Othewise, some unexpected behavior may happen. Continuing the initial example, if the user tries to navigate back to the screen where the habit was deleted, it will not actually navigate because the state in that screen will immediately tell the program to navigate back.
18+
19+
That’s why resetting the state is unified throughout the project with the help of extensions in [`UiEvent.kt`](https://github.com/DanielRendox/RoutineTracker/blob/main/core/ui/src/main/java/com/rendox/routinetracker/core/ui/helpers/UiEvent.kt)
20+
21+
## How to use
22+
23+
So all events should be objects of `UiEvent` interface and should typcially be nullable so that when the event is fired off, it could be reset to null. This interface has a function called `onConsumed` which should be overriden when the event is fired off. You can also pass data using `val data`.
24+
25+
All events should be observed in the UI using `ObserveUiEvent` composable. This composable automatically calls `event.onConsumed()` once the action is finished.
26+
27+
Back to our example with deleting a habit. We would implement navigation as follows:
28+
29+
```kotlin
30+
class RoutineDetailsScreenViewModel: ViewModel() {
31+
private val _navigateBackEvent: MutableStateFlow<UiEvent<Any>?> = MutableStateFlow(null)
32+
val navigateBackEvent = _navigateBackEvent.asStateFlow()
33+
34+
fun onDeleteHabit() = viewModelScope.launch {
35+
/* the logic of deleting a habit */
36+
37+
_navigateBackEvent.update {
38+
object : UiEvent<Any> {
39+
override val data: Any = Unit
40+
override fun onConsumed() {
41+
_navigateBackEvent.update { null }
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
@Composable
49+
fun RoutineDetailsScreen(
50+
viewModel: RoutineDetailsScreenViewModel = koinViewModel(),
51+
navigateBack: () -> Unit,
52+
) {
53+
val navigateBackEvent by viewModel.navigateBackEvent.collectAsStateWithLifecycle()
54+
ObserveUiEvent(navigateBackEvent) {
55+
navigateBack()
56+
}
57+
}
58+
```
59+
60+
So here we have a `StateFlow` that is initially set to `null`. When the user wants to delete the habit, `navigateBackEvent` gets a value of the `UiEvent` object. There is an implementation of `onConsumed` defined that resets our `StateFlow` back to null.
61+
62+
Then we observe this event using `ObserveUiEvent` in the compose code. After the we’ve navigated back, the `navigateBackEvent` will be reset to null automatically.
63+
64+
> Now, in practice, it is rarely necessary to reset the state back and we could get away without doing it. However, we must follow this pattern for all one-off events to keep consistency inside the codebase and avoid unexpected behavior.

0 commit comments

Comments
 (0)