🛠️ Fix not thread-safe _state.update() calls

* Fixed multiple issues with update calls being executed simultaneously
This commit is contained in:
Acclorite 2024-12-28 16:02:16 +02:00
parent a25cb8f3eb
commit bc9c378e43
9 changed files with 344 additions and 226 deletions

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates
@ -25,6 +26,8 @@ class AboutModel @Inject constructor(
private val checkForUpdates: CheckForUpdates private val checkForUpdates: CheckForUpdates
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(AboutState()) private val _state = MutableStateFlow(AboutState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -100,6 +103,7 @@ class AboutModel @Inject constructor(
} }
is AboutEvent.OnDismissDialog -> { is AboutEvent.OnDismissDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy(dialog = null) it.copy(dialog = null)
} }
@ -107,3 +111,10 @@ class AboutModel @Inject constructor(
} }
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
}

View file

@ -18,8 +18,9 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -55,6 +56,8 @@ class BookInfoModel @Inject constructor(
private val updateBookWithText: UpdateBookWithText private val updateBookWithText: UpdateBookWithText
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(BookInfoState()) private val _state = MutableStateFlow(BookInfoState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -384,15 +387,16 @@ class BookInfoModel @Inject constructor(
is BookInfoEvent.OnCheckCoverReset -> { is BookInfoEvent.OnCheckCoverReset -> {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
if (_state.value.book.id == -1) return@launch
canResetCover.execute(_state.value.book.id).apply {
_state.update { _state.update {
it.copy( it.copy(
canResetCover = canResetCover.execute( canResetCover = this
_state.value.book.id
)
) )
} }
} }
} }
}
is BookInfoEvent.OnShowMoreBottomSheet -> { is BookInfoEvent.OnShowMoreBottomSheet -> {
_state.update { _state.update {
@ -780,16 +784,16 @@ class BookInfoModel @Inject constructor(
return@launch return@launch
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
_state.update { _state.update {
BookInfoState( BookInfoState(
book = book book = book
) )
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
if (startUpdate) { if (startUpdate) {
onEvent( onEvent(
BookInfoEvent.OnCheckForTextUpdate( BookInfoEvent.OnCheckForTextUpdate(
@ -804,13 +808,14 @@ class BookInfoModel @Inject constructor(
fun resetScreen() { fun resetScreen() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
_state.update {
BookInfoState()
}
eventJob.cancel() eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob() eventJob = SupervisorJob()
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -18,8 +18,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -47,6 +48,8 @@ class BrowseModel @Inject constructor(
private val insertBook: InsertBook private val insertBook: InsertBook
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(BrowseState()) private val _state = MutableStateFlow(BrowseState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -119,6 +122,7 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnRequestFocus -> { is BrowseEvent.OnRequestFocus -> {
viewModelScope.launch(Dispatchers.Main) {
if (!_state.value.hasFocused) { if (!_state.value.hasFocused) {
event.focusRequester.requestFocus() event.focusRequester.requestFocus()
_state.update { _state.update {
@ -128,20 +132,23 @@ class BrowseModel @Inject constructor(
} }
} }
} }
}
is BrowseEvent.OnSearchQueryChange -> { is BrowseEvent.OnSearchQueryChange -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
searchQuery = event.query searchQuery = event.query
) )
} }
changeSearchQueryJob?.cancel() changeSearchQueryJob?.cancel()
changeSearchQueryJob = viewModelScope.launch(Dispatchers.IO) { changeSearchQueryJob = launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(BrowseEvent.OnSearch) onEvent(BrowseEvent.OnSearch)
} }
} }
}
is BrowseEvent.OnSearch -> { is BrowseEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -292,20 +299,24 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnShowFilterBottomSheet -> { is BrowseEvent.OnShowFilterBottomSheet -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
) )
} }
} }
}
is BrowseEvent.OnDismissBottomSheet -> { is BrowseEvent.OnDismissBottomSheet -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
bottomSheet = null bottomSheet = null
) )
} }
} }
}
is BrowseEvent.OnPermissionCheck -> { is BrowseEvent.OnPermissionCheck -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -413,6 +424,7 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissPermissionDialog -> { is BrowseEvent.OnDismissPermissionDialog -> {
viewModelScope.launch {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted = if (!legacyPermission) { val isPermissionGranted = if (!legacyPermission) {
Environment.isExternalStorageManager() Environment.isExternalStorageManager()
@ -429,6 +441,7 @@ class BrowseModel @Inject constructor(
_state.update { it.copy(isError = true) } _state.update { it.copy(isError = true) }
} }
} }
}
is BrowseEvent.OnShowAddDialog -> { is BrowseEvent.OnShowAddDialog -> {
viewModelScope.launch { viewModelScope.launch {
@ -484,6 +497,7 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissAddDialog -> { is BrowseEvent.OnDismissAddDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = null dialog = null
@ -491,6 +505,7 @@ class BrowseModel @Inject constructor(
} }
getAddDialogBooksJob?.cancel() getAddDialogBooksJob?.cancel()
} }
}
is BrowseEvent.OnActionAddDialog -> { is BrowseEvent.OnActionAddDialog -> {
viewModelScope.launch { viewModelScope.launch {
@ -574,6 +589,7 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissDialog -> { is BrowseEvent.OnDismissDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = null dialog = null
@ -582,6 +598,7 @@ class BrowseModel @Inject constructor(
} }
} }
} }
}
private suspend fun getFilesFromDownloads( private suspend fun getFilesFromDownloads(
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
@ -601,9 +618,11 @@ class BrowseModel @Inject constructor(
} }
fun resetScreen() { fun resetScreen() {
viewModelScope.launch {
storagePermissionJob?.cancel() storagePermissionJob?.cancel()
_state.update { it.copy(isError = false) } _state.update { it.copy(isError = false) }
} }
}
fun filterList( fun filterList(
files: List<SelectableFile>, files: List<SelectableFile>,
@ -710,4 +729,10 @@ class BrowseModel @Inject constructor(
) )
) )
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -42,6 +43,8 @@ class HistoryModel @Inject constructor(
private val deleteWholeHistory: DeleteWholeHistory private val deleteWholeHistory: DeleteWholeHistory
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(HistoryState()) private val _state = MutableStateFlow(HistoryState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -136,6 +139,7 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnRequestFocus -> { is HistoryEvent.OnRequestFocus -> {
viewModelScope.launch(Dispatchers.Main) {
if (!_state.value.hasFocused) { if (!_state.value.hasFocused) {
event.focusRequester.requestFocus() event.focusRequester.requestFocus()
_state.update { _state.update {
@ -145,20 +149,23 @@ class HistoryModel @Inject constructor(
} }
} }
} }
}
is HistoryEvent.OnSearchQueryChange -> { is HistoryEvent.OnSearchQueryChange -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
searchQuery = event.query searchQuery = event.query
) )
} }
searchQueryChange?.cancel() searchQueryChange?.cancel()
searchQueryChange = viewModelScope.launch(Dispatchers.IO) { searchQueryChange = launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(HistoryEvent.OnSearch) onEvent(HistoryEvent.OnSearch)
} }
} }
}
is HistoryEvent.OnSearch -> { is HistoryEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -209,12 +216,14 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { is HistoryEvent.OnShowDeleteWholeHistoryDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG
) )
} }
} }
}
is HistoryEvent.OnActionDeleteWholeHistoryDialog -> { is HistoryEvent.OnActionDeleteWholeHistoryDialog -> {
viewModelScope.launch { viewModelScope.launch {
@ -238,6 +247,7 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnDismissDialog -> { is HistoryEvent.OnDismissDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = null dialog = null
@ -246,6 +256,7 @@ class HistoryModel @Inject constructor(
} }
} }
} }
}
private suspend fun getHistoryFromDatabase( private suspend fun getHistoryFromDatabase(
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
@ -314,4 +325,10 @@ class HistoryModel @Inject constructor(
) )
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -32,6 +33,8 @@ class LibraryModel @Inject constructor(
private val moveBooks: UpdateBook private val moveBooks: UpdateBook
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(LibraryState()) private val _state = MutableStateFlow(LibraryState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -104,18 +107,20 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnSearchQueryChange -> { is LibraryEvent.OnSearchQueryChange -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
searchQuery = event.query searchQuery = event.query
) )
} }
searchQueryChange?.cancel() searchQueryChange?.cancel()
searchQueryChange = viewModelScope.launch(Dispatchers.IO) { searchQueryChange = launch(Dispatchers.IO) {
delay(500) delay(500)
yield() yield()
onEvent(LibraryEvent.OnSearch) onEvent(LibraryEvent.OnSearch)
} }
} }
}
is LibraryEvent.OnSearch -> { is LibraryEvent.OnSearch -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -124,6 +129,7 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnRequestFocus -> { is LibraryEvent.OnRequestFocus -> {
viewModelScope.launch(Dispatchers.Main) {
if (!_state.value.hasFocused) { if (!_state.value.hasFocused) {
event.focusRequester.requestFocus() event.focusRequester.requestFocus()
_state.update { _state.update {
@ -133,6 +139,7 @@ class LibraryModel @Inject constructor(
} }
} }
} }
}
is LibraryEvent.OnClearSelectedBooks -> { is LibraryEvent.OnClearSelectedBooks -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -163,12 +170,14 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnShowMoveDialog -> { is LibraryEvent.OnShowMoveDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = LibraryScreen.MOVE_DIALOG dialog = LibraryScreen.MOVE_DIALOG
) )
} }
} }
}
is LibraryEvent.OnActionMoveDialog -> { is LibraryEvent.OnActionMoveDialog -> {
viewModelScope.launch { viewModelScope.launch {
@ -213,12 +222,14 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnShowDeleteDialog -> { is LibraryEvent.OnShowDeleteDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = LibraryScreen.DELETE_DIALOG dialog = LibraryScreen.DELETE_DIALOG
) )
} }
} }
}
is LibraryEvent.OnActionDeleteDialog -> { is LibraryEvent.OnActionDeleteDialog -> {
viewModelScope.launch { viewModelScope.launch {
@ -249,6 +260,7 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnDismissDialog -> { is LibraryEvent.OnDismissDialog -> {
viewModelScope.launch {
_state.update { _state.update {
it.copy( it.copy(
dialog = null dialog = null
@ -257,6 +269,7 @@ class LibraryModel @Inject constructor(
} }
} }
} }
}
private suspend fun getBooksFromDatabase( private suspend fun getBooksFromDatabase(
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
@ -273,4 +286,10 @@ class LibraryModel @Inject constructor(
) )
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
import ua.acclorite.book_story.domain.browse.toBrowseLayout import ua.acclorite.book_story.domain.browse.toBrowseLayout
@ -45,6 +46,8 @@ class MainModel @Inject constructor(
private val getAllSettings: GetAllSettings private val getAllSettings: GetAllSettings
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _isReady = MutableStateFlow(false) private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow() val isReady = _isReady.asStateFlow()
@ -506,4 +509,10 @@ class MainModel @Inject constructor(
} }
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -54,6 +55,8 @@ class ReaderModel @Inject constructor(
private val checkForTextUpdate: CheckForTextUpdate private val checkForTextUpdate: CheckForTextUpdate
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(ReaderState()) private val _state = MutableStateFlow(ReaderState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -550,13 +553,14 @@ class ReaderModel @Inject constructor(
return@launch return@launch
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
_state.update { _state.update {
ReaderState(book = book) ReaderState(book = book)
} }
eventJob.cancel()
eventJob = SupervisorJob()
onEvent( onEvent(
ReaderEvent.OnMenuVisibility( ReaderEvent.OnMenuVisibility(
show = false, show = false,
@ -606,6 +610,7 @@ class ReaderModel @Inject constructor(
} }
private fun updateChapter(index: Int) { private fun updateChapter(index: Int) {
viewModelScope.launch {
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index) val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
_state.update { _state.update {
it.copy( it.copy(
@ -614,6 +619,7 @@ class ReaderModel @Inject constructor(
) )
} }
} }
}
private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> { private fun calculateCurrentChapter(index: Int): Pair<Chapter?, Float> {
val currentChapter = _state.value.book.chapters.find { chapter -> val currentChapter = _state.value.book.chapters.find { chapter ->
@ -672,12 +678,14 @@ class ReaderModel @Inject constructor(
fun resetScreen() { fun resetScreen() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
_state.update {
ReaderState()
}
eventJob.cancel() eventJob.cancel()
eventJob = SupervisorJob() eventJob = SupervisorJob()
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -15,8 +15,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -42,6 +43,8 @@ class SettingsModel @Inject constructor(
private val deleteColorPreset: DeleteColorPreset private val deleteColorPreset: DeleteColorPreset
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(SettingsState()) private val _state = MutableStateFlow(SettingsState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -536,4 +539,10 @@ class SettingsModel @Inject constructor(
updateColorColorPresetJob?.cancel() updateColorColorPresetJob?.cancel()
deleteColorPresetJob?.cancel() deleteColorPresetJob?.cancel()
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -16,8 +16,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.presentation.core.util.launchActivity import ua.acclorite.book_story.presentation.core.util.launchActivity
import javax.inject.Inject import javax.inject.Inject
@ -27,6 +28,8 @@ class StartModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(StartState()) private val _state = MutableStateFlow(StartState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -59,6 +62,7 @@ class StartModel @Inject constructor(
} }
is StartEvent.OnStoragePermissionRequest -> { is StartEvent.OnStoragePermissionRequest -> {
viewModelScope.launch {
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted = if (legacyStoragePermission) { val isPermissionGranted = if (legacyStoragePermission) {
@ -71,7 +75,7 @@ class StartModel @Inject constructor(
storagePermissionGranted = true storagePermissionGranted = true
) )
} }
return return@launch
} }
if (legacyStoragePermission) { if (legacyStoragePermission) {
@ -82,7 +86,7 @@ class StartModel @Inject constructor(
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
intent.launchActivity(event.activity) { intent.launchActivity(event.activity) {
return return@launch
} }
} }
} }
@ -95,7 +99,7 @@ class StartModel @Inject constructor(
) )
intent.launchActivity(event.activity) { intent.launchActivity(event.activity) {
return return@launch
} }
} }
@ -123,15 +127,17 @@ class StartModel @Inject constructor(
} }
} }
} }
}
is StartEvent.OnNotificationsPermissionRequest -> { is StartEvent.OnNotificationsPermissionRequest -> {
viewModelScope.launch {
if (event.notificationsPermissionState.status.isGranted) { if (event.notificationsPermissionState.status.isGranted) {
_state.update { _state.update {
it.copy( it.copy(
notificationsPermissionGranted = true notificationsPermissionGranted = true
) )
} }
return return@launch
} }
if (!event.notificationsPermissionState.status.shouldShowRationale) { if (!event.notificationsPermissionState.status.shouldShowRationale) {
@ -141,7 +147,7 @@ class StartModel @Inject constructor(
intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName) intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
intent.launchActivity(event.activity) { intent.launchActivity(event.activity) {
return return@launch
} }
} }
@ -168,8 +174,10 @@ class StartModel @Inject constructor(
} }
} }
} }
}
fun resetScreen() { fun resetScreen() {
viewModelScope.launch {
_state.update { _state.update {
storagePermissionJob?.cancel() storagePermissionJob?.cancel()
notificationsPermissionJob?.cancel() notificationsPermissionJob?.cancel()
@ -178,3 +186,10 @@ class StartModel @Inject constructor(
} }
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
}