The UI layer follows the MVVM (Model-View-ViewModel) pattern with reactive state management. ViewModels expose state and actions to UI components, which observe data changes and update accordingly.
ViewModels
MainViewModel
The MainViewModel coordinates the main activity and manages the alarm detail editor:
class MainViewModel (
private val uiStore: UiStore ,
private val alarms: IAlarmsManager ,
private val bugReporter: BugReporter ,
) : ViewModel () {
var openDrawerOnCreate: Boolean = false
fun editing (): StateFlow < EditedAlarm ?> {
return uiStore. editing ()
}
fun hideDetails () {
uiStore. hideDetails ()
}
fun deleteEdited () {
uiStore. editing (). value ?. value ?.id?. let {
alarms. getAlarm (it)?. delete ()
}
uiStore. hideDetails ()
}
fun edit (restored: EditedAlarm ) {
uiStore. edit (restored. value , restored.isNew)
}
fun sendUserReport () {
bugReporter. sendUserReport ()
}
}
The MainViewModel delegates UI state management to UiStore and domain operations to IAlarmsManager, keeping concerns separated.
ListViewModel
The ListViewModel manages the alarm list screen:
class ListViewModel (
private val uiStore: UiStore ,
) : ViewModel () {
fun edit (alarmValue: AlarmValue ) {
uiStore. edit (alarmValue)
}
fun createNewAlarm () {
uiStore. createNewAlarm ()
}
}
This ViewModel is minimal because list data comes directly from the domain Store:
// In the UI component
store. alarms ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { alarms ->
// Update RecyclerView adapter
}
AlarmDetailsViewModel
The AlarmDetailsViewModel handles the alarm detail/editor screen:
class AlarmDetailsViewModel (
private val uiStore: UiStore
) : ViewModel () {
fun editor (): StateFlow < EditedAlarm ?> {
return uiStore. editing ()
}
fun hideDetails () {
uiStore. hideDetails ()
}
fun modify (reason: String , function: ( AlarmValue ) -> AlarmValue ) {
uiStore. editing (). value ?. let { prev ->
uiStore. edit ( function (prev. value ), prev.isNew)
}
}
var newAlarmPopupSeen: Boolean
get () = uiStore.newAlarmPopupSeen
set ( value ) {
uiStore.newAlarmPopupSeen = value
}
}
The modify() function enables functional updates:
// In the UI
viewModel. modify ( "hour changed" ) {
copy (hour = selectedHour)
}
viewModel. modify ( "toggle repeat day" ) {
copy (daysOfWeek = daysOfWeek. withDay (dayIndex, enabled))
}
The modify() function’s reason parameter aids debugging by logging why the alarm was modified.
UI state management
UiStore
The UiStore manages transient UI state like the currently edited alarm:
class UiStore {
private val _editing = MutableStateFlow < EditedAlarm ?>( null )
fun editing (): StateFlow < EditedAlarm ?> = _editing. asStateFlow ()
fun edit ( value : AlarmValue , isNew: Boolean = false ) {
_editing. value = EditedAlarm ( value , isNew)
}
fun hideDetails () {
_editing. value = null
}
fun createNewAlarm () {
// Creates a new alarm and opens editor
}
var newAlarmPopupSeen: Boolean = false
}
EditedAlarm
The EditedAlarm data class wraps an alarm being edited:
data class EditedAlarm (
val value : AlarmValue ,
val isNew: Boolean
)
The isNew flag indicates whether the alarm was just created, enabling different UI behaviors (e.g., showing onboarding tips).
Reactive data flow
Observing alarm list
UI components observe the domain Store for alarm list updates:
// In Fragment or Activity
store. alarms ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { alarms ->
adapter. submitList (alarms)
}
. let { disposable. add (it) }
RxJava Observable Domain Store exposes RxJava observables for reactive updates
StateFlow ViewModels use Kotlin StateFlow for UI state
Observing next alarm
The next scheduled alarm is also observable:
store. next ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { nextOptional ->
if (nextOptional. isPresent ()) {
val next = nextOptional. get ()
updateNextAlarmUI (next.alarm, next.nextNonPrealarmTime)
} else {
hideNextAlarmUI ()
}
}
Alarm set events
The UI listens for alarm set events to show notifications:
store. sets ()
. observeOn (AndroidSchedulers. mainThread ())
. filter { it.fromUserInteraction }
. subscribe { alarmSet ->
showToast ( "Alarm set for ${ formatTime (alarmSet.millis) } " )
}
The fromUserInteraction flag distinguishes user-initiated changes from system events (like reboot), so toasts only appear for user actions.
Activities and Fragments
ViewModel injection
ViewModels are injected using Koin:
class AlarmListFragment : Fragment () {
private val listViewModel: ListViewModel by viewModel ()
private val store: Store by inject ()
override fun onViewCreated (view: View , savedInstanceState: Bundle ?) {
super . onViewCreated (view, savedInstanceState)
// Observe alarm list
store. alarms ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { alarms ->
adapter. submitList (alarms)
}
. let { disposable. add (it) }
}
}
User interactions
User interactions flow through ViewModels to the domain layer:
// Toggle alarm enabled/disabled
alarmSwitch. setOnCheckedChangeListener { _, isChecked ->
alarms. enable (alarmValue, isChecked)
}
// Open alarm editor
alarmItem. setOnClickListener {
listViewModel. edit (alarmValue)
}
// Create new alarm
fabButton. setOnClickListener {
listViewModel. createNewAlarm ()
}
Saving changes
Alarm changes are committed through the domain layer:
// In AlarmDetailsFragment
saveButton. setOnClickListener {
val editedAlarm = viewModel. editor (). value ?. value
if (editedAlarm != null ) {
alarms. getAlarm (editedAlarm.id)?. edit { editedAlarm }
viewModel. hideDetails ()
}
}
The Alarm.edit() function immediately persists changes to the repository, so no separate save call is needed.
Background press handling
The app uses a shared BackPresses component to coordinate back navigation:
class BackPresses {
private val subject = PublishSubject. create < Unit >()
fun onBackPressed () {
subject. onNext (Unit)
}
fun observable (): Observable < Unit > = subject
}
Components can observe back presses:
backPresses. observable ()
. subscribe {
if (isEditing) {
viewModel. hideDetails ()
} else {
requireActivity (). finish ()
}
}
Lifecycle management
Disposables
RxJava subscriptions are managed with CompositeDisposable:
class AlarmListFragment : Fragment () {
private val disposable = CompositeDisposable ()
override fun onViewCreated (view: View , savedInstanceState: Bundle ?) {
store. alarms ()
. subscribe { alarms -> /* ... */ }
. let { disposable. add (it) }
}
override fun onDestroyView () {
super . onDestroyView ()
disposable. clear ()
}
}
Persisting data
Critical activities await data persistence before pausing:
override fun onPause () {
super . onPause ()
alarmsRepository. awaitStored ()
}
Calling awaitStored() ensures alarm changes are durably persisted before the system can destroy the app.
Theme handling
The app supports dynamic theming:
single < DynamicThemeHandler > {
DynamicThemeHandler ( get ())
}
UI components apply themes reactively:
themeHandler. theme ()
. observeOn (AndroidSchedulers. mainThread ())
. subscribe { theme ->
requireActivity (). setTheme (theme.styleRes)
requireActivity (). recreate ()
}