Skip to content

Commit

Permalink
[feature|build] Change the homepage style; optimize the use experienc…
Browse files Browse the repository at this point in the history
…e; change the MVI implementation; update the dependencies version
  • Loading branch information
SkyD666 committed Nov 30, 2023
1 parent dc59baa commit dc843a9
Show file tree
Hide file tree
Showing 31 changed files with 1,017 additions and 463 deletions.
12 changes: 6 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ android {
applicationId "com.skyd.rays"
minSdk 24
targetSdk 34
versionCode 46
versionName "1.6-rc05"
versionCode 47
versionName "2.0-alpha01"
flavorDimensions = ["versionName"]

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -133,7 +133,7 @@ dependencies {
implementation "com.google.android.material:material:1.10.0"
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation "androidx.activity:activity-compose:1.8.0"
implementation "androidx.activity:activity-compose:1.8.1"
implementation "androidx.palette:palette-ktx:1.0.0"
implementation "com.google.dagger:hilt-android:2.48.1"
kapt "com.google.dagger:hilt-android-compiler:2.48.1"
Expand All @@ -145,9 +145,9 @@ dependencies {
implementation "io.coil-kt:coil-gif:2.5.0"
implementation "androidx.profileinstaller:profileinstaller:1.3.1"
implementation "androidx.core:core-splashscreen:1.0.1"
implementation "androidx.room:room-runtime:2.6.0"
implementation "androidx.room:room-ktx:2.6.0"
ksp "androidx.room:room-compiler:2.6.0"
implementation "androidx.room:room-runtime:2.6.1"
implementation "androidx.room:room-ktx:2.6.1"
ksp "androidx.room:room-compiler:2.6.1"
implementation "com.github.thegrizzlylabs:sardine-android:0.8"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
implementation "com.github.Kyant0:Monet:0.1.0-alpha03"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class SearchStickersStrategy : IApiStrategy {
val requestPackage = data.getStringExtra("requestPackage")
val result = hiltEntryPoint.homeRepository()
.requestStickerWithTagsList(data.getStringExtra("keyword").orEmpty())
.first().data.orEmpty()
.first()
.map {
val uri = FileProvider.getUriForFile(
appContext,
Expand Down
17 changes: 9 additions & 8 deletions app/src/main/java/com/skyd/rays/base/BaseViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@ import kotlinx.coroutines.launch
abstract class BaseViewModel<UiState : IUiState, UiEvent : IUiEvent, UiIntent : IUiIntent> :
ViewModel() {

private val _uiIntentFlow: MutableSharedFlow<UiIntent> = MutableSharedFlow()
private val _uiIntentFlow: MutableSharedFlow<UiIntent> = MutableSharedFlow(extraBufferCapacity = 10)

protected abstract fun initUiState(): UiState

/**
* 若 IUIChange 是 Event,则发送出去,不纳入 UiState
*/
private fun Flow<IUIChange>.sendEvent(): Flow<UiState> = transform { value ->
Log.e("TAG", "sendEvent: $value")
val (state, event) = value.checkStateOrEvent()
if (event != null) {
uiEventChannel.send(event) // 此时 state 为 null
Expand Down Expand Up @@ -65,7 +64,9 @@ abstract class BaseViewModel<UiState : IUiState, UiEvent : IUiEvent, UiIntent :
if (uiIntent.showLoading) {
sendLoadUiIntent(LoadUiIntent.Loading(true))
}
Log.e("TAG", "bf sendUiIntent: $uiIntent", )
_uiIntentFlow.emit(uiIntent)
Log.e("TAG", "sendUiIntent: $uiIntent", )
}
}

Expand Down Expand Up @@ -199,10 +200,10 @@ abstract class BaseViewModel<UiState : IUiState, UiEvent : IUiEvent, UiIntent :
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly, initUiState())

val uiStateFlow2: StateFlow<UiState> = _uiIntentFlow
.handleIntent2()
.scan(initUiState()) { oldState, partialChange -> partialChange.reduce(oldState) }
.sendEvent()
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly, initUiState())
// val uiStateFlow2: StateFlow<UiState> = _uiIntentFlow
// .handleIntent2()
// .scan(initUiState()) { oldState, partialChange -> partialChange.reduce(oldState) }
// .sendEvent()
// .flowOn(Dispatchers.IO)
// .stateIn(viewModelScope, SharingStarted.Eagerly, initUiState())
}
159 changes: 159 additions & 0 deletions app/src/main/java/com/skyd/rays/base/mvi/AbstractMviViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.skyd.rays.base.mvi

import android.os.Build
import android.os.Looper
import android.util.Log
import androidx.annotation.CallSuper
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.skyd.rays.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.channels.onSuccess
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
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.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
import kotlin.coroutines.ContinuationInterceptor

private fun debugCheckMainThread() {
if (BuildConfig.DEBUG) {
check(Looper.getMainLooper() === Looper.myLooper()) {
"Expected to be called on the main thread but was " + Thread.currentThread().name
}
}
}

suspend fun debugCheckImmediateMainDispatcher() {
if (BuildConfig.DEBUG) {
val interceptor = currentCoroutineContext()[ContinuationInterceptor]
Log.d(
"###",
"debugCheckImmediateMainDispatcher: $interceptor, ${Dispatchers.Main.immediate}, ${Dispatchers.Main}"
)

check(interceptor === Dispatchers.Main.immediate) {
"Expected ContinuationInterceptor to be Dispatchers.Main.immediate but was $interceptor"
}
}
}

abstract class AbstractMviViewModel<I : MviIntent, S : MviViewState, E : MviSingleEvent> :
MviViewModel<I, S, E>, ViewModel() {
protected open val rawLogTag: String? = null

protected val logTag by lazy(PUBLICATION) {
(rawLogTag ?: this::class.java.simpleName).let { tag: String ->
// Tag length limit was removed in API 26.
if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
tag
} else {
tag.take(MAX_TAG_LENGTH)
}
}
}

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

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

@MainThread
final override suspend fun processIntent(intent: I) {
debugCheckMainThread()
debugCheckImmediateMainDispatcher()

check(intentMutableFlow.tryEmit(intent)) { "Failed to emit intent: $intent" }
}

@CallSuper
override fun onCleared() {
super.onCleared()
eventChannel.close()
}

// Send event and access intent flow.

/**
* Must be called in [kotlinx.coroutines.Dispatchers.Main.immediate],
* otherwise it will throw an exception.
*
* If you want to send an event from other [kotlinx.coroutines.CoroutineDispatcher],
* use `withContext(Dispatchers.Main.immediate) { sendEvent(event) }`.
*/
protected suspend fun sendEvent(event: E) {
debugCheckMainThread()
debugCheckImmediateMainDispatcher()

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

protected val intentSharedFlow: SharedFlow<I> get() = intentMutableFlow

// Extensions on Flow using viewModelScope.

protected fun <T> Flow<T>.debugLog(subject: String): Flow<T> =
if (BuildConfig.DEBUG) {
onEach { Log.e(logTag, ">>> $subject: $it") }
} else {
this
}

protected fun <T> SharedFlow<T>.debugLog(subject: String): SharedFlow<T> =
if (BuildConfig.DEBUG) {
val self = this

object : SharedFlow<T> by self {
val subscriberCount = AtomicInteger(0)

override suspend fun collect(collector: FlowCollector<T>): Nothing {
val count = subscriberCount.getAndIncrement()

self.collect {
Log.e(logTag, ">>> $subject ~ $count: $it")
collector.emit(it)
}
}
}
} else {
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
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/skyd/rays/base/mvi/MviIntent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.skyd.rays.base.mvi

/**
* Immutable object which represent an view's intent.
*/
interface MviIntent
6 changes: 6 additions & 0 deletions app/src/main/java/com/skyd/rays/base/mvi/MviSingleEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.skyd.rays.base.mvi
/**
* Immutable object which represents a single event
* like snack bar message, navigation event, a dialog trigger, etc...
*/
interface MviSingleEvent
29 changes: 29 additions & 0 deletions app/src/main/java/com/skyd/rays/base/mvi/MviViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.skyd.rays.base.mvi

import androidx.annotation.MainThread
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

/**
* Object that will subscribes to a MviView's [MviIntent]s,
* process it and emit a [MviViewState] back.
*
* @param I Top class of the [MviIntent] that the [MviViewModel] will be subscribing to.
* @param S Top class of the [MviViewState] the [MviViewModel] will be emitting.
* @param E Top class of the [MviSingleEvent] that the [MviViewModel] will be emitting.
*/
interface MviViewModel<I : MviIntent, S : MviViewState, E : MviSingleEvent> {
val viewState: StateFlow<S>

val singleEvent: Flow<E>

/**
* Must be called in [kotlinx.coroutines.Dispatchers.Main.immediate],
* otherwise it will throw an exception.
*
* If you want to process an intent from other [kotlinx.coroutines.CoroutineDispatcher],
* use `withContext(Dispatchers.Main.immediate) { processIntent(intent) }`.
*/
@MainThread
suspend fun processIntent(intent: I)
}
16 changes: 16 additions & 0 deletions app/src/main/java/com/skyd/rays/base/mvi/MviViewState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.skyd.rays.base.mvi

import android.os.Bundle

/**
* Immutable object which contains all the required information to render a [MviView].
*/
interface MviViewState

/**
* An interface that converts a [MviViewState] to a [Bundle] and vice versa.
*/
interface MviViewStateSaver<S : MviViewState> {
fun S.toBundle(): Bundle
fun restore(bundle: Bundle?): S
}
60 changes: 60 additions & 0 deletions app/src/main/java/com/skyd/rays/ext/FlowExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.skyd.rays.ext

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.produceIn
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean

fun <T> concat(flow1: Flow<T>, flow2: Flow<T>): Flow<T> = flow {
emitAll(flow1)
emitAll(flow2)
}

fun <T> Flow<T>.startWith(item: T): Flow<T> = concat(flowOf(item), this)


/**
* Projects each source value to a [Flow] which is merged in the output [Flow] only if the previous projected [Flow] has completed.
* If value is received while there is some projected [Flow] sequence being merged, it will simply be ignored.
*
* This method is a shortcut for `map(transform).flattenFirst()`. See [flattenFirst].
*
* ### Operator fusion
*
* Applications of [flowOn], [buffer], and [produceIn] _after_ this operator are fused with
* its concurrent merging so that only one properly configured channel is used for execution of merging logic.
*
* @param transform A transform function to apply to value that was observed while no Flow is executing in parallel.
*/
fun <T, R> Flow<T>.flatMapFirst(transform: suspend (value: T) -> Flow<R>): Flow<R> =
map(transform).flattenFirst()

/**
* Converts a higher-order [Flow] into a first-order [Flow] by dropping inner [Flow] while the previous inner [Flow] has not yet completed.
*/
fun <T> Flow<Flow<T>>.flattenFirst(): Flow<T> = channelFlow {
val busy = AtomicBoolean(false)

collect { inner ->
if (busy.compareAndSet(false, true)) {
// Do not pay for dispatch here, it's never necessary
launch(start = CoroutineStart.UNDISPATCHED) {
try {
inner.collect { send(it) }
busy.set(false)
} catch (e: CancellationException) {
busy.set(false)
}
}
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/java/com/skyd/rays/model/db/dao/sticker/StickerDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ interface StickerDao {
)
fun getRecentCreateStickersList(count: Int): Flow<List<StickerWithTags>>

@Transaction
@Query(
"""SELECT *
FROM $STICKER_TABLE_NAME
ORDER BY $SHARE_COUNT_COLUMN DESC
LIMIT :count
"""
)
fun getMostSharedStickersList(count: Int): Flow<List<StickerWithTags>>

@Transaction
@Query("SELECT $UUID_COLUMN FROM $STICKER_TABLE_NAME WHERE $STICKER_MD5_COLUMN LIKE :stickerMd5")
fun containsByMd5(stickerMd5: String): String?
Expand Down
Loading

0 comments on commit dc843a9

Please sign in to comment.