Skip to content

Commit

Permalink
[optimize|fix|build] Optimize use of Flow in MVI mode; fix an issue t…
Browse files Browse the repository at this point in the history
…hat player progress value is Nan; update dependencies
  • Loading branch information
SkyD666 committed Sep 14, 2024
1 parent 54f9f1c commit c9a5a93
Show file tree
Hide file tree
Showing 21 changed files with 87 additions and 229 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ android {
minSdk = 24
targetSdk = 35
versionCode = 24
versionName = "2.1-beta03"
versionName = "2.1-beta04"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -211,7 +211,7 @@ dependencies {

implementation(libs.rome)
implementation(libs.rome.modules)
implementation("be.ceau:opml-parser:3.1.0") {
implementation(libs.ceau.opmlparser) {
exclude(group = "net.sf.kxml", module = "kxml2")
}

Expand Down
38 changes: 12 additions & 26 deletions app/src/main/java/com/skyd/anivu/base/mvi/AbstractMviViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
import com.skyd.anivu.BuildConfig
import com.skyd.anivu.util.debug
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.onSuccess
Expand All @@ -19,10 +20,9 @@ import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import java.util.concurrent.atomic.AtomicInteger
import kotlin.LazyThreadSafetyMode.PUBLICATION
Expand Down Expand Up @@ -66,7 +66,7 @@ abstract class AbstractMviViewModel<I : MviIntent, S : MviViewState, E : MviSing
}

private val eventChannel = Channel<E>(Channel.UNLIMITED)
private val intentMutableFlow = MutableSharedFlow<I>(extraBufferCapacity = 2)
private val intentMutableFlow = MutableSharedFlow<I>(extraBufferCapacity = Int.MAX_VALUE)

final override val singleEvent: Flow<E> = eventChannel.receiveAsFlow()

Expand All @@ -88,7 +88,7 @@ abstract class AbstractMviViewModel<I : MviIntent, S : MviViewState, E : MviSing
// Send event and access intent flow.

/**
* Must be called in [kotlinx.coroutines.Dispatchers.Main.immediate],
* Must be called in [MainCoroutineDispatcher.immediate],
* otherwise it will throw an exception.
*
* If you want to send an event from other [kotlinx.coroutines.CoroutineDispatcher],
Expand All @@ -100,16 +100,20 @@ abstract class AbstractMviViewModel<I : MviIntent, S : MviViewState, E : MviSing

eventChannel.trySend(event)
.onSuccess { debug { Log.i(logTag, "sendEvent: event=$event") } }
.onFailure {
debug { Log.e(logTag, "$it. Failed to send event: $event") }
}
.onFailure { debug { Log.e(logTag, "$it. Failed to send event: $event") } }
.getOrThrow()
}

protected val intentSharedFlow: SharedFlow<I> get() = intentMutableFlow
protected val intentFlow: Flow<I> get() = intentMutableFlow.asSharedFlow()

// Extensions on Flow using viewModelScope.

protected fun Flow<S>.toState(initialValue: S) = stateIn(
viewModelScope,
SharingStarted.Eagerly,
initialValue
)

protected fun <T> Flow<T>.debugLog(subject: String): Flow<T> =
if (BuildConfig.DEBUG) {
onEach { Log.i(logTag, ">>> $subject: $it") }
Expand Down Expand Up @@ -137,24 +141,6 @@ abstract class AbstractMviViewModel<I : MviIntent, S : MviViewState, E : MviSing
this
}

/**
* Share the flow in [viewModelScope],
* start when the first subscriber arrives,
* and stop when the last subscriber leaves.
*/
protected fun <T> Flow<T>.shareWhileSubscribed(): SharedFlow<T> =
shareIn(viewModelScope, SharingStarted.WhileSubscribed())

protected fun <T> Flow<T>.stateWithInitialNullWhileSubscribed(): StateFlow<T?> =
stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)

@Deprecated(
message = "This Flow is already shared in viewModelScope, so you don't need to share it again.",
replaceWith = ReplaceWith("this"),
level = DeprecationLevel.ERROR
)
protected fun <T> SharedFlow<T>.shareWhileSubscribed(): SharedFlow<T> = this

private companion object {
private const val MAX_TAG_LENGTH = 23
}
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/skyd/anivu/base/mvi/MviIntent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ fun <I : MviIntent, S : MviViewState, E : MviSingleEvent>
intentChannel
.consumeAsFlow()
.run { if (startWith == null) this else startWith(startWith) }
.onEach(this@getDispatcher::processIntent)
.collect()
.collect(this@getDispatcher::processIntent)
}
}
return remember(*keys, intentChannel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import com.skyd.anivu.ui.mpv.controller.state.PlayState
@Composable
fun ProgressIndicator(modifier: Modifier = Modifier, playState: () -> PlayState) {
val animatedProgress by animateFloatAsState(
targetValue = playState().run { currentPosition.toFloat() / duration },
targetValue = playState().run {
if (duration == 0) 0f else currentPosition.toFloat() / duration
},
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "playerProgressIndicatorAnimate"
)
Expand Down
17 changes: 4 additions & 13 deletions app/src/main/java/com/skyd/anivu/ui/mpv/mvi/PlayerViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.skyd.anivu.ui.mpv.mvi

import androidx.lifecycle.viewModelScope
import com.skyd.anivu.base.mvi.AbstractMviViewModel
import com.skyd.anivu.ext.catchMap
import com.skyd.anivu.model.bean.MediaPlayHistoryBean
Expand All @@ -9,8 +8,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
Expand All @@ -20,7 +17,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand All @@ -46,20 +42,15 @@ class PlayerViewModel @Inject constructor(
val initialVS = PlayerState.initial()

viewState = merge(
intentSharedFlow.filterIsInstance<PlayerIntent.TrySeekToLast>().take(1),
intentSharedFlow.filterNot { it is PlayerIntent.TrySeekToLast }
intentFlow.filterIsInstance<PlayerIntent.TrySeekToLast>().take(1),
intentFlow.filterNot { it is PlayerIntent.TrySeekToLast }
)
.shareWhileSubscribed()
.toPlayerPartialStateChangeFlow()
.debugLog("PlayerPartialStateChange")
.sendSingleEvent()
.scan(initialVS) { vs, change -> change.reduce(vs) }
.debugLog("ViewState")
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
initialVS
)
.toState(initialVS)
}

private fun Flow<PlayerPartialStateChange>.sendSingleEvent(): Flow<PlayerPartialStateChange> {
Expand All @@ -79,7 +70,7 @@ class PlayerViewModel @Inject constructor(
}
}

private fun SharedFlow<PlayerIntent>.toPlayerPartialStateChangeFlow(): Flow<PlayerPartialStateChange> {
private fun Flow<PlayerIntent>.toPlayerPartialStateChangeFlow(): Flow<PlayerPartialStateChange> {
return merge(
filterIsInstance<PlayerIntent.TrySeekToLast>().flatMapConcat { intent ->
playerRepo.requestLastPlayPosition(intent.path).map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,10 @@ private fun getLicenseList(): List<LicenseBean> {
license = "Apache-2.0",
link = "https://github.com/Calvin-LL/Reorderable",
),
LicenseBean(
name = "OPML Parser",
license = "Apache-2.0",
link = "https://github.com/mdewilde/opml-parser",
),
).sortedBy { it.name }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.skyd.anivu.ui.screen.about.update

import androidx.lifecycle.viewModelScope
import com.skyd.anivu.appContext
import com.skyd.anivu.base.mvi.AbstractMviViewModel
import com.skyd.anivu.config.Const
Expand All @@ -12,8 +11,6 @@ import com.skyd.anivu.ext.toDateTimeString
import com.skyd.anivu.model.repository.UpdateRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
Expand All @@ -23,7 +20,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import okhttp3.internal.toLongOrDefault
import java.text.SimpleDateFormat
Expand All @@ -41,21 +37,16 @@ class UpdateViewModel @Inject constructor(private var updateRepo: UpdateReposito
val initialVS = UpdateState.initial()

viewState = merge(
intentSharedFlow.filter { it is UpdateIntent.CheckUpdate && !it.isRetry }.take(1),
intentSharedFlow.filter { it is UpdateIntent.CheckUpdate && it.isRetry },
intentSharedFlow.filterNot { it is UpdateIntent.CheckUpdate }
intentFlow.filter { it is UpdateIntent.CheckUpdate && !it.isRetry }.take(1),
intentFlow.filter { it is UpdateIntent.CheckUpdate && it.isRetry },
intentFlow.filterNot { it is UpdateIntent.CheckUpdate }
)
.shareWhileSubscribed()
.toUpdatePartialStateChangeFlow()
.debugLog("UpdatePartialStateChange")
.sendSingleEvent()
.scan(initialVS) { vs, change -> change.reduce(vs) }
.debugLog("ViewState")
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
initialVS
)
.toState(initialVS)
}

private fun Flow<UpdatePartialStateChange>.sendSingleEvent(): Flow<UpdatePartialStateChange> {
Expand All @@ -71,7 +62,7 @@ class UpdateViewModel @Inject constructor(private var updateRepo: UpdateReposito
}
}

private fun SharedFlow<UpdateIntent>.toUpdatePartialStateChangeFlow(): Flow<UpdatePartialStateChange> {
private fun Flow<UpdateIntent>.toUpdatePartialStateChangeFlow(): Flow<UpdatePartialStateChange> {
return merge(
filterIsInstance<UpdateIntent.CheckUpdate>().flatMapConcat {
updateRepo.checkUpdate().map { data ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import com.skyd.anivu.ext.startWith
import com.skyd.anivu.model.repository.ArticleRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
Expand All @@ -20,7 +18,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject

@HiltViewModel
Expand All @@ -34,20 +31,15 @@ class ArticleViewModel @Inject constructor(
val initialVS = ArticleState.initial()

viewState = merge(
intentSharedFlow.filterIsInstance<ArticleIntent.Init>().distinctUntilChanged(),
intentSharedFlow.filterNot { it is ArticleIntent.Init }
intentFlow.filterIsInstance<ArticleIntent.Init>().distinctUntilChanged(),
intentFlow.filterNot { it is ArticleIntent.Init }
)
.shareWhileSubscribed()
.toArticlePartialStateChangeFlow()
.debugLog("ArticlePartialStateChange")
.sendSingleEvent()
.scan(initialVS) { vs, change -> change.reduce(vs) }
.debugLog("ViewState")
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
initialVS
)
.toState(initialVS)
}

private fun Flow<ArticlePartialStateChange>.sendSingleEvent(): Flow<ArticlePartialStateChange> {
Expand Down Expand Up @@ -75,7 +67,7 @@ class ArticleViewModel @Inject constructor(
}
}

private fun SharedFlow<ArticleIntent>.toArticlePartialStateChangeFlow(): Flow<ArticlePartialStateChange> {
private fun Flow<ArticleIntent>.toArticlePartialStateChangeFlow(): Flow<ArticlePartialStateChange> {
return merge(
filterIsInstance<ArticleIntent.Init>().flatMapConcat { intent ->
flowOf(articleRepo.requestArticleList(intent.urls).cachedIn(viewModelScope)).map {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
package com.skyd.anivu.ui.screen.download

import androidx.lifecycle.viewModelScope
import com.skyd.anivu.appContext
import com.skyd.anivu.base.mvi.AbstractMviViewModel
import com.skyd.anivu.base.mvi.MviSingleEvent
import com.skyd.anivu.ext.catchMap
import com.skyd.anivu.ext.startWith
import com.skyd.anivu.model.repository.download.DownloadRepository
import com.skyd.anivu.model.worker.download.DownloadTorrentWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.take
import javax.inject.Inject

Expand All @@ -35,22 +28,17 @@ class DownloadViewModel @Inject constructor(
val initialVS = DownloadState.initial()

viewState = merge(
intentSharedFlow.filterIsInstance<DownloadIntent.Init>().take(1),
intentSharedFlow.filterNot { it is DownloadIntent.Init }
intentFlow.filterIsInstance<DownloadIntent.Init>().take(1),
intentFlow.filterNot { it is DownloadIntent.Init }
)
.shareWhileSubscribed()
.toReadPartialStateChangeFlow()
.debugLog("DownloadPartialStateChange")
.scan(initialVS) { vs, change -> change.reduce(vs) }
.debugLog("ViewState")
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
initialVS
)
.toState(initialVS)
}

private fun SharedFlow<DownloadIntent>.toReadPartialStateChangeFlow(): Flow<DownloadPartialStateChange> {
private fun Flow<DownloadIntent>.toReadPartialStateChangeFlow(): Flow<DownloadPartialStateChange> {
return merge(
filterIsInstance<DownloadIntent.Init>().flatMapConcat {
downloadRepo.requestDownloadingVideos().map {
Expand Down
Loading

0 comments on commit c9a5a93

Please sign in to comment.