From cefe2909425b2de6ca5de3931ba860c0eee37683 Mon Sep 17 00:00:00 2001 From: Paul Woitaschek <woitaschek@gmail.com> Date: Mon, 4 Jul 2022 08:50:12 +0200 Subject: [PATCH] Refactor the logic out of the SleepTimerDialogController into it's own viewmodel. Ensure that the creation of the bookmark is not cancelled through the lifecycle scope moving the coroutine in a dedicated scope. --- .../sleepTimer/SleepTimerDialogController.kt | 107 ++++++------------ .../sleepTimer/SleepTimerDialogViewModel.kt | 79 +++++++++++++ 2 files changed, 111 insertions(+), 75 deletions(-) create mode 100644 sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogViewModel.kt diff --git a/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogController.kt b/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogController.kt index 72e9b0f6f5..594aefb90a 100644 --- a/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogController.kt +++ b/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogController.kt @@ -1,6 +1,5 @@ package voice.sleepTimer -import android.annotation.SuppressLint import android.app.Dialog import android.os.Bundle import android.view.View @@ -8,27 +7,19 @@ import android.widget.FrameLayout import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.squareup.anvil.annotations.ContributesTo -import de.paulwoitaschek.flowpref.Pref +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import voice.common.conductor.DialogController -import voice.common.pref.PrefKeys import voice.common.AppScope +import voice.common.conductor.DialogController import voice.common.rootComponentAs import voice.data.Book import voice.data.getBookId import voice.data.putBookId -import voice.data.repo.BookRepository -import voice.data.repo.BookmarkRepo import voice.sleepTimer.databinding.DialogSleepBinding import javax.inject.Inject -import javax.inject.Named private const val NI_BOOK_ID = "ni#bookId" -private const val SI_MINUTES = "si#time" -/** - * Simple dialog for activating the sleep timer - */ class SleepTimerDialogController(bundle: Bundle) : DialogController(bundle) { constructor(bookId: Book.Id) : this( @@ -38,93 +29,59 @@ class SleepTimerDialogController(bundle: Bundle) : DialogController(bundle) { ) @Inject - lateinit var bookmarkRepo: BookmarkRepo - - @Inject - lateinit var sleepTimer: SleepTimer - - @Inject - lateinit var bookRepo: BookRepository + lateinit var viewModel: SleepTimerDialogViewModel init { rootComponentAs<Component>().inject(this) } - @field:[Inject Named(PrefKeys.SLEEP_TIME)] - lateinit var sleepTimePref: Pref<Int> - - private var selectedMinutes = 0 - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putInt(SI_MINUTES, selectedMinutes) - } - override fun onCreateDialog(savedViewState: Bundle?): Dialog { val binding = DialogSleepBinding.inflate(activity!!.layoutInflater) - fun updateTimeState() { - binding.time.text = activity!!.getString(R.string.min, selectedMinutes.toString()) - - if (selectedMinutes > 0) binding.fab.show() - else binding.fab.hide() - } - - @SuppressLint("SetTextI18n") - fun appendNumber(number: Int) { - val newNumber = selectedMinutes * 10 + number - if (newNumber > 999) return - selectedMinutes = newNumber - updateTimeState() + listOf( + binding.zero, + binding.one, + binding.two, + binding.three, + binding.four, + binding.five, + binding.six, + binding.seven, + binding.eight, + binding.nine, + ).forEachIndexed { index, textView -> + textView.setOnClickListener { + viewModel.onNumberClicked(index) + } } - selectedMinutes = savedViewState?.getInt(SI_MINUTES) ?: sleepTimePref.value - updateTimeState() - - // find views and prepare clicks - binding.one.setOnClickListener { appendNumber(1) } - binding.two.setOnClickListener { appendNumber(2) } - binding.three.setOnClickListener { appendNumber(3) } - binding.four.setOnClickListener { appendNumber(4) } - binding.five.setOnClickListener { appendNumber(5) } - binding.six.setOnClickListener { appendNumber(6) } - binding.seven.setOnClickListener { appendNumber(7) } - binding.eight.setOnClickListener { appendNumber(8) } - binding.nine.setOnClickListener { appendNumber(9) } - binding.zero.setOnClickListener { appendNumber(0) } - // upon delete remove the last number binding.delete.setOnClickListener { - selectedMinutes /= 10 - updateTimeState() + viewModel.onNumberDeleteClicked() } - // upon long click remove all numbers binding.delete.setOnLongClickListener { - selectedMinutes = 0 - updateTimeState() + viewModel.onNumberDeleteLongClicked() true } + lifecycleScope.launch { + viewModel.viewState().collectLatest { viewState -> + binding.time.text = activity!!.getString(R.string.min, viewState.selectedMinutes.toString()) - binding.fab.setOnClickListener { - require(selectedMinutes > 0) { "fab should be hidden when time is invalid" } - sleepTimePref.value = selectedMinutes - - lifecycleScope.launch { - val book = bookRepo.get(args.getBookId(NI_BOOK_ID)!!) ?: return@launch - bookmarkRepo.addBookmarkAtBookPosition( - book = book, - setBySleepTimer = true, - title = null - ) + if (viewState.showFab) { + binding.fab.show() + } else { + binding.fab.hide() + } } - - sleepTimer.setActive(true) + } + binding.fab.setOnClickListener { + viewModel.onConfirmButtonClicked(args.getBookId(NI_BOOK_ID)!!) dismissDialog() } return BottomSheetDialog(activity!!).apply { setContentView(binding.root) - // hide the background so the fab looks overlapping + // hide the background so the fab appears overlapping setOnShowListener { val parentView = binding.root.parent as View parentView.background = null diff --git a/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogViewModel.kt b/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogViewModel.kt new file mode 100644 index 0000000000..ff66533cd3 --- /dev/null +++ b/sleepTimer/src/main/kotlin/voice/sleepTimer/SleepTimerDialogViewModel.kt @@ -0,0 +1,79 @@ +package voice.sleepTimer + +import de.paulwoitaschek.flowpref.Pref +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import voice.common.pref.PrefKeys +import voice.data.Book +import voice.data.repo.BookRepository +import voice.data.repo.BookmarkRepo +import javax.inject.Inject +import javax.inject.Named + +class SleepTimerDialogViewModel +@Inject constructor( + private val bookmarkRepo: BookmarkRepo, + private val sleepTimer: SleepTimer, + private val bookRepo: BookRepository, + @Named(PrefKeys.SLEEP_TIME) + private val sleepTimePref: Pref<Int> +) { + + private val scope = MainScope() + + private val selectedMinutes = MutableStateFlow(sleepTimePref.value) + + fun viewState(): Flow<SleepTimerDialogViewState> { + return selectedMinutes + .map { selectedMinutes -> + SleepTimerDialogViewState( + selectedMinutes = selectedMinutes, + showFab = selectedMinutes > 0 + ) + } + } + + fun onNumberClicked(number: Int) { + require(number in 0..9) + selectedMinutes.update { oldValue -> + val newValue = (oldValue * 10 + number) + if (newValue > 999) { + oldValue + } else { + sleepTimePref.value = newValue + newValue + } + } + } + + fun onNumberDeleteClicked() { + selectedMinutes.update { it / 10 } + } + + fun onNumberDeleteLongClicked() { + selectedMinutes.update { 0 } + } + + fun onConfirmButtonClicked(bookId: Book.Id) { + check(selectedMinutes.value > 0) + + scope.launch { + val book = bookRepo.get(bookId) ?: return@launch + bookmarkRepo.addBookmarkAtBookPosition( + book = book, + setBySleepTimer = true, + title = null + ) + } + sleepTimer.setActive(true) + } +} + +data class SleepTimerDialogViewState( + val selectedMinutes: Int, + val showFab: Boolean, +)