a
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,53 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.lowe.common"
|
||||
compileSdk = Version.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Version.minSdk
|
||||
targetSdk = Version.targetSdk
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
compileOptions {
|
||||
targetCompatibility(JavaVersion.VERSION_11)
|
||||
sourceCompatibility(JavaVersion.VERSION_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(mapOf("path" to ":resource")))
|
||||
implementation(Deps.lifecucleRuntimeKtx)
|
||||
|
||||
implementation(Deps.paging)
|
||||
implementation(Deps.pagingKtx)
|
||||
|
||||
implementation(Deps.retrofit)
|
||||
implementation(Deps.retrofitGsonConverter)
|
||||
implementation(Deps.okhttp)
|
||||
implementation(Deps.okhttpLoggingInterceptor)
|
||||
|
||||
implementation(Deps.preferences)
|
||||
implementation(Deps.hiltAndroid)
|
||||
kapt(Deps.kaptHiltAndroidCompiler)
|
||||
kapt(Deps.kaptHiltCompiler)
|
||||
|
||||
implementation(Deps.dataStore)
|
||||
implementation(Deps.kotlinSerial)
|
||||
|
||||
testImplementation(Deps.testJunit)
|
||||
androidTestImplementation(Deps.androidTestJunit)
|
||||
androidTestImplementation(Deps.androidTestEspresso)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# Model
|
||||
-keep class com.lowe.common.services.model.** {*;}
|
||||
-keep class com.lowe.common.model.** {*;}
|
||||
-keepclasseswithmembers class com.lowe.common.base.http.adapter.NetworkResponse {*;}
|
||||
-keepclasseswithmembers class * extends com.lowe.common.base.http.adapter.NetworkResponse {*;}
|
||||
-keepclasseswithmembers class com.lowe.common.account.AccountState {*;}
|
||||
-keepclasseswithmembers class com.lowe.common.account.LocalUserInfo {*;}
|
||||
-keepclasseswithmembers class com.lowe.common.account.RegisterInfo {*;}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.lowe.common
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.lowe.common.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.google.gson.Gson
|
||||
import com.lowe.common.base.AppLog
|
||||
import com.lowe.common.base.http.cookie.UserCookieJarImpl
|
||||
import com.lowe.common.di.ApplicationScope
|
||||
import com.lowe.common.di.IoDispatcher
|
||||
import com.lowe.common.services.model.User
|
||||
import com.lowe.common.services.model.UserBaseInfo
|
||||
import com.lowe.common.utils.fromJson
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountManager @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val cookieJar: UserCookieJarImpl,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val dispatcher: CoroutineDispatcher
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val PREFERENCE_KEY_ACCOUNT_USER_INFO = stringPreferencesKey("key_account_user_info")
|
||||
}
|
||||
|
||||
private val userBaseInfoStateFlow: MutableStateFlow<UserBaseInfo> =
|
||||
MutableStateFlow(UserBaseInfo())
|
||||
|
||||
private val accountStatusFlow: MutableStateFlow<AccountState> =
|
||||
MutableStateFlow(AccountState.LogOut)
|
||||
|
||||
init {
|
||||
applicationScope.launch(dispatcher) {
|
||||
launch {
|
||||
if (cookieJar.isLoginCookieValid()) {
|
||||
accountStatusFlow.tryEmit(AccountState.LogIn(true))
|
||||
}
|
||||
}
|
||||
launch {
|
||||
dataStore.data
|
||||
.onEach {
|
||||
AppLog.d("dataStore", it.toString())
|
||||
}
|
||||
.catch {
|
||||
AppLog.e(msg = "Error reading preferences.", throwable = it)
|
||||
emit(emptyPreferences())
|
||||
}.filter {
|
||||
it.contains(PREFERENCE_KEY_ACCOUNT_USER_INFO)
|
||||
}.map {
|
||||
AppLog.d(msg = "initUserDataFlow fromJson: ${it.asMap().keys.toString()} ")
|
||||
Gson().fromJson(it[PREFERENCE_KEY_ACCOUNT_USER_INFO]) ?: UserBaseInfo()
|
||||
}.collectLatest { userBaseInfo ->
|
||||
AppLog.d(msg = "${PREFERENCE_KEY_ACCOUNT_USER_INFO.name} initUserDataFlow : ${userBaseInfo.userInfo}")
|
||||
userBaseInfoStateFlow.emit(userBaseInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息,基于dataStore
|
||||
*/
|
||||
fun collectUserInfoFlow(): StateFlow<UserBaseInfo> = userBaseInfoStateFlow
|
||||
|
||||
/**
|
||||
* 用户状态信息
|
||||
*/
|
||||
fun accountStateFlow(): StateFlow<AccountState> = accountStatusFlow
|
||||
|
||||
fun cacheUserBaseInfo(userBaseInfo: UserBaseInfo) {
|
||||
applicationScope.launch(dispatcher) {
|
||||
dataStore.edit {
|
||||
it[PREFERENCE_KEY_ACCOUNT_USER_INFO] = Gson().toJson(userBaseInfo).apply {
|
||||
AppLog.d(msg = "${PREFERENCE_KEY_ACCOUNT_USER_INFO.name} cacheUserBaseInfo : $this}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUserBaseInfo() {
|
||||
applicationScope.launch(dispatcher) {
|
||||
dataStore.edit {
|
||||
it[PREFERENCE_KEY_ACCOUNT_USER_INFO] = ""
|
||||
AppLog.d(msg = "${PREFERENCE_KEY_ACCOUNT_USER_INFO.name} clearUserBaseInfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun peekUserBaseInfo(): UserBaseInfo = userBaseInfoStateFlow.value
|
||||
|
||||
fun logIn(user: User) {
|
||||
applicationScope.launch(dispatcher) {
|
||||
accountStatusFlow.emit(AccountState.LogIn(true, user))
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
applicationScope.launch(dispatcher) {
|
||||
clearUserBaseInfo()
|
||||
cookieJar.clear()
|
||||
accountStatusFlow.emit(AccountState.LogOut)
|
||||
}
|
||||
}
|
||||
|
||||
fun isMe(userId: String) = peekUserBaseInfo().userInfo.id == userId
|
||||
|
||||
fun isLogin() = accountStatusFlow.value.isLogin
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
import android.content.Context
|
||||
import com.lowe.common.services.model.User
|
||||
import com.lowe.common.utils.Activities.Login
|
||||
import com.lowe.common.utils.intentTo
|
||||
import com.lowe.common.utils.showShortToast
|
||||
|
||||
sealed interface AccountState {
|
||||
|
||||
object LogOut : AccountState
|
||||
|
||||
data class LogIn(val isFromCookie: Boolean, val user: User? = null) : AccountState
|
||||
}
|
||||
|
||||
inline val AccountState.isLogin: Boolean
|
||||
get() {
|
||||
return this is AccountState.LogIn
|
||||
}
|
||||
|
||||
inline fun AccountState.checkLogin(context: Context, action: (AccountState) -> Unit) {
|
||||
if (this.isLogin) {
|
||||
action(this)
|
||||
} else {
|
||||
context.getString(com.lowe.resource.R.string.account_need_login).showShortToast()
|
||||
context.startActivity(intentTo(Login))
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.base.http.adapter.isSuccess
|
||||
import com.lowe.common.base.http.adapter.whenSuccess
|
||||
import com.lowe.common.services.AccountService
|
||||
import com.lowe.common.services.model.User
|
||||
import com.lowe.common.services.model.UserBaseInfo
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
interface IAccountViewModelDelegate {
|
||||
|
||||
val accountState: StateFlow<AccountState>
|
||||
|
||||
val accountInfo: StateFlow<UserBaseInfo>
|
||||
|
||||
val isLogin: Boolean
|
||||
|
||||
val userId: String
|
||||
|
||||
suspend fun fetchUserInfo(): NetworkResponse<UserBaseInfo>
|
||||
|
||||
suspend fun login(localUserInfo: LocalUserInfo): NetworkResponse<User>
|
||||
|
||||
suspend fun logout(): NetworkResponse<Any>
|
||||
|
||||
suspend fun register(registerInfo: RegisterInfo): NetworkResponse<Any>
|
||||
}
|
||||
|
||||
internal class AccountViewModelDelegate @Inject constructor(
|
||||
private val service: AccountService,
|
||||
private val accountManager: AccountManager
|
||||
) : IAccountViewModelDelegate {
|
||||
|
||||
override val accountState: StateFlow<AccountState>
|
||||
get() = accountManager.accountStateFlow()
|
||||
|
||||
override val accountInfo: StateFlow<UserBaseInfo>
|
||||
get() = accountManager.collectUserInfoFlow()
|
||||
|
||||
override val isLogin: Boolean
|
||||
get() = accountManager.isLogin()
|
||||
|
||||
override val userId: String
|
||||
get() = accountInfo.value.userInfo.id
|
||||
|
||||
override suspend fun fetchUserInfo(): NetworkResponse<UserBaseInfo> {
|
||||
return service.getUserInfo().also {
|
||||
it.whenSuccess { userBaseInfo ->
|
||||
accountManager.cacheUserBaseInfo(userBaseInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun login(localUserInfo: LocalUserInfo): NetworkResponse<User> {
|
||||
val result = service.login(localUserInfo.username, localUserInfo.password)
|
||||
result.whenSuccess {
|
||||
accountManager.logIn(it)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun logout(): NetworkResponse<Any> {
|
||||
return service.logout().also {
|
||||
if (it.isSuccess) {
|
||||
accountManager.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun register(registerInfo: RegisterInfo): NetworkResponse<Any> {
|
||||
return service.register(
|
||||
registerInfo.username,
|
||||
registerInfo.password,
|
||||
registerInfo.confirmPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
import com.lowe.common.services.AccountService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class AccountViewModelDelegateModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAccountViewModelDelegate(
|
||||
service: AccountService,
|
||||
accountManager: AccountManager
|
||||
): IAccountViewModelDelegate {
|
||||
return AccountViewModelDelegate(service, accountManager)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
data class LocalUserInfo(
|
||||
val username: String = "",
|
||||
val password: String = ""
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.lowe.common.account
|
||||
|
||||
data class RegisterInfo(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val confirmPassword: String
|
||||
)
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import android.util.Log
|
||||
import com.lowe.resource.theme.ThemePrimaryKey
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
object ActivityConfigHelper {
|
||||
|
||||
private var configSets = setOf<Config>()
|
||||
|
||||
private val configChangeSharedFlow = MutableSharedFlow<Set<Config>>(extraBufferCapacity = 1)
|
||||
|
||||
fun collectConfigChange() =
|
||||
configChangeSharedFlow.asSharedFlow().distinctUntilChanged(this::areConfigsEquivalent)
|
||||
|
||||
fun updateConfig(config: Config, notify: Boolean = true) {
|
||||
configSets = setOf(config)
|
||||
if (notify) {
|
||||
configChangeSharedFlow.tryEmit(configSets)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConfig(config: Set<Config>, notify: Boolean = true) {
|
||||
configSets = config
|
||||
if (notify) {
|
||||
configChangeSharedFlow.tryEmit(configSets)
|
||||
}
|
||||
}
|
||||
|
||||
fun getConfigs() = configSets.toSet()
|
||||
|
||||
private fun areConfigsEquivalent(old: Set<Config>, new: Set<Config>): Boolean {
|
||||
return if (old.size != new.size) false else old.containsAll(new).apply {
|
||||
Log.d(
|
||||
"ActivityConfigHelper",
|
||||
"areConfigsEquivalent old: $old - new: $new - result: $this"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed interface Config {
|
||||
|
||||
data class ThemeConfig(val key: ThemePrimaryKey) : Config
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
@file:Suppress("UNUSED")
|
||||
package com.lowe.common.base
|
||||
|
||||
import android.util.Log
|
||||
import com.lowe.common.BuildConfig
|
||||
|
||||
/**
|
||||
* App 日志类
|
||||
*/
|
||||
object AppLog {
|
||||
|
||||
private const val DEFAULT_TAG = "WanAndroid"
|
||||
|
||||
/**
|
||||
* Debug 下开启
|
||||
*/
|
||||
private val isDebug
|
||||
get() = BuildConfig.DEBUG
|
||||
|
||||
private const val tag = DEFAULT_TAG
|
||||
|
||||
/**
|
||||
* [Log.VERBOSE]
|
||||
*/
|
||||
fun v(tag: String = DEFAULT_TAG, msg: String) {
|
||||
if (isDebug) {
|
||||
Log.v(tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Log.DEBUG]
|
||||
*/
|
||||
fun d(tag: String = DEFAULT_TAG, msg: String) {
|
||||
if (isDebug) {
|
||||
Log.d(tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Log.INFO]
|
||||
*/
|
||||
fun i(tag: String = DEFAULT_TAG, msg: String) {
|
||||
if (isDebug) {
|
||||
Log.i(tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Log.WARN]
|
||||
*/
|
||||
fun w(tag: String = DEFAULT_TAG, msg: String) {
|
||||
if (isDebug) {
|
||||
Log.w(tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Log.ERROR]
|
||||
*/
|
||||
fun e(tag: String = DEFAULT_TAG, msg: String) {
|
||||
if (isDebug) {
|
||||
Log.e(tag, msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Log.ERROR]
|
||||
*/
|
||||
fun e(tag: String = DEFAULT_TAG, msg: String = "", throwable: Throwable) {
|
||||
if (isDebug) {
|
||||
Log.e(tag, msg, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.lowe.resource.theme.ThemeHelper
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class BaseThemeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
initConfigs(ActivityConfigHelper.getConfigs())
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
ActivityConfigHelper.collectConfigChange().collect {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initConfigs(configs: Set<Config>) {
|
||||
configs.forEach {
|
||||
when (it) {
|
||||
is Config.ThemeConfig -> onThemeConfigChanged(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onThemeConfigChanged(config: Config.ThemeConfig) {
|
||||
setTheme(ThemeHelper.getThemeRes(config.key))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
|
||||
const val DEFAULT_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.lowe.common.services.BaseService
|
||||
|
||||
/**
|
||||
* PagingSource 通用封装类
|
||||
*/
|
||||
class IntKeyPagingSource<S : BaseService, V : Any>(
|
||||
private val pageStart: Int = BaseService.DEFAULT_PAGE_START_NO_1,
|
||||
private val service: S,
|
||||
private val load: suspend (S, Int, Int) -> List<V>
|
||||
) : PagingSource<Int, V>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, V>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, V> {
|
||||
val page = params.key ?: pageStart
|
||||
return try {
|
||||
val data = load(service, page, params.loadSize)
|
||||
LoadResult.Page(
|
||||
data = data,
|
||||
prevKey = if (page == pageStart) null else page - 1,
|
||||
nextKey = if (data.isEmpty()) null else page + 1
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
class SimpleDiffCallback(
|
||||
private val oldList: List<Any>,
|
||||
private val newList: List<Any>,
|
||||
private val areItemSame: (Any, Any) -> Boolean,
|
||||
private val areContentSame: (Any, Any) -> Boolean,
|
||||
private val getChangePayload: (Any, Any) -> Any? = { _: Any, _: Any -> null }
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
areItemSame(oldList[oldItemPosition], newList[newItemPosition])
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
areContentSame(oldList[oldItemPosition], newList[newItemPosition])
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
|
||||
getChangePayload(oldList[oldItemPosition], newList[newItemPosition])
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.lowe.common.base
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
class SimpleDiffItemCallback<T : Any>(
|
||||
private val areItemSame: (T, T) -> Boolean,
|
||||
private val areContentSame: (T, T) -> Boolean,
|
||||
private val changePayload: (T, T) -> Any? = { _: T, _: T -> null }
|
||||
) : DiffUtil.ItemCallback<T>() {
|
||||
|
||||
override fun getChangePayload(oldItem: T, newItem: T) = changePayload(oldItem, newItem)
|
||||
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = areItemSame(oldItem, newItem)
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T) = areContentSame(oldItem, newItem)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.lowe.common.base.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lowe.common.base.ActivityConfigHelper
|
||||
import com.lowe.common.base.Config
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.services.usecase.ArticleCollectUseCase
|
||||
import com.lowe.common.theme.ThemeViewModelDelegate
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [Application]生命周期内的[AndroidViewModel]
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AppViewModel @Inject constructor(
|
||||
application: Application,
|
||||
private val articleCollectUseCase: ArticleCollectUseCase,
|
||||
private val themeViewModelDelegate: ThemeViewModelDelegate
|
||||
) : AndroidViewModel(application), ThemeViewModelDelegate by themeViewModelDelegate {
|
||||
/**
|
||||
* 全局收藏事件
|
||||
*/
|
||||
private val _collectArticleLiveData = MutableLiveData<CollectEvent>()
|
||||
val collectArticleEvent: LiveData<CollectEvent> = _collectArticleLiveData
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
themeViewModelDelegate.themeState.collectLatest {
|
||||
ActivityConfigHelper.updateConfig(Config.ThemeConfig(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏文章
|
||||
*/
|
||||
fun articleCollectAction(event: CollectEvent) {
|
||||
this.viewModelScope.launch {
|
||||
articleCollectUseCase.articleCollectAction(event).getOrNull() ?: return@launch
|
||||
_collectArticleLiveData.value = event
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.lowe.common.base.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import com.lowe.common.services.usecase.ArticleCollectUseCase
|
||||
import com.lowe.common.theme.ThemeViewModelDelegate
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 用于创建[AppViewModel]实例
|
||||
*/
|
||||
class AppViewModelFactory @Inject constructor(
|
||||
private val application: Application,
|
||||
private val articleCollectUseCase: ArticleCollectUseCase,
|
||||
private val themeViewModelDelegate: ThemeViewModelDelegate
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
return when (modelClass) {
|
||||
AppViewModel::class.java -> AppViewModel(application, articleCollectUseCase, themeViewModelDelegate)
|
||||
else -> throw IllegalArgumentException("Unknown class $modelClass")
|
||||
} as T
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.lowe.common.base.app
|
||||
|
||||
import android.app.Application
|
||||
|
||||
interface ApplicationProxy {
|
||||
|
||||
fun onCreate(application: Application)
|
||||
|
||||
fun onTerminate()
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.base.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
|
||||
object CommonApplicationProxy : ApplicationProxy, ViewModelStoreOwner {
|
||||
|
||||
lateinit var application: Application
|
||||
|
||||
private val viewModelStore = ViewModelStore()
|
||||
|
||||
override fun onCreate(application: Application) {
|
||||
this.application = application
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
viewModelStore.clear()
|
||||
}
|
||||
|
||||
override fun getViewModelStore() = viewModelStore
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.lowe.common.base.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.lowe.common.base.datastore.DataStorePreference.DataStorePreferenceKeys.PREFERENCE_THEME
|
||||
import com.lowe.resource.theme.ThemeHelper
|
||||
import com.lowe.resource.theme.ThemePrimaryKey
|
||||
import com.lowe.resource.theme.fromStorageKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface PreferenceStorage {
|
||||
|
||||
suspend fun applyTheme(themeKey: String)
|
||||
val appliedTheme: Flow<ThemePrimaryKey>
|
||||
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class DataStorePreference @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>
|
||||
) : PreferenceStorage {
|
||||
|
||||
object DataStorePreferenceKeys {
|
||||
val PREFERENCE_THEME = stringPreferencesKey("setting_theme")
|
||||
}
|
||||
|
||||
override suspend fun applyTheme(themeKey: String) {
|
||||
dataStore.edit {
|
||||
it[PREFERENCE_THEME] = themeKey
|
||||
}
|
||||
}
|
||||
|
||||
override val appliedTheme: Flow<ThemePrimaryKey>
|
||||
get() = dataStore.data.filter { it.contains(PREFERENCE_THEME) }
|
||||
.map { fromStorageKey(it[PREFERENCE_THEME].orEmpty()) ?: ThemeHelper.defaultThemeKey }
|
||||
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.lowe.common.base.http
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.lowe.common.di.ApplicationCoroutineScope
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object SearchHistoryPreference {
|
||||
|
||||
private const val KEY_DATA_STORE_SEARCH_HISTORY = "key_data_store_search_history"
|
||||
|
||||
val searchHistoryPreferences = stringSetPreferencesKey(KEY_DATA_STORE_SEARCH_HISTORY)
|
||||
|
||||
}
|
||||
|
||||
object DataStoreFactory {
|
||||
|
||||
object Name {
|
||||
const val DATA_STORE_NAME_COOKIE = "data_store_cookie_name"
|
||||
}
|
||||
|
||||
private const val USER_PREFERENCES = "wan_android_preferences"
|
||||
|
||||
private lateinit var defaultDataStore: DataStore<Preferences>
|
||||
|
||||
private val dataStoreMaps = ConcurrentHashMap<String, DataStore<Preferences>>()
|
||||
|
||||
fun init(appContext: Context) {
|
||||
getDefaultPreferencesDataStore(appContext)
|
||||
}
|
||||
|
||||
private fun getDefaultPreferencesDataStore(appContext: Context): DataStore<Preferences> {
|
||||
if (this::defaultDataStore.isInitialized.not()) {
|
||||
defaultDataStore = createPreferencesDataStore(appContext, USER_PREFERENCES)
|
||||
}
|
||||
return defaultDataStore
|
||||
}
|
||||
|
||||
fun getDefaultPreferencesDataStore() = defaultDataStore
|
||||
|
||||
fun getPreferencesDataStore(appContext: Context, name: String): DataStore<Preferences> =
|
||||
dataStoreMaps.getOrPut(name) {
|
||||
createPreferencesDataStore(appContext, name)
|
||||
}
|
||||
|
||||
private fun createPreferencesDataStore(
|
||||
appContext: Context,
|
||||
name: String
|
||||
): DataStore<Preferences> {
|
||||
return PreferenceDataStoreFactory.create(
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(
|
||||
produceNewData = { emptyPreferences() }
|
||||
),
|
||||
migrations = listOf(
|
||||
SharedPreferencesMigration(
|
||||
appContext,
|
||||
name
|
||||
)
|
||||
),
|
||||
scope = ApplicationCoroutineScope.providesIOCoroutineScope(),
|
||||
produceFile = { appContext.preferencesDataStoreFile(name) }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.lowe.common.base.http
|
||||
|
||||
import com.lowe.common.base.app.CommonApplicationProxy
|
||||
import com.lowe.common.base.http.adapter.ErrorHandler
|
||||
import com.lowe.common.utils.NetWorkUtil
|
||||
import com.lowe.common.utils.showShortToast
|
||||
import java.io.IOException
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
/**
|
||||
* Error的Toast处理
|
||||
*/
|
||||
internal object ErrorToastHandler : ErrorHandler {
|
||||
|
||||
private const val ERROR_DEFAULT = "请求失败"
|
||||
private const val ERROR_CONNECTED_TIME_OUT = "请求链接超时"
|
||||
private const val ERROR_NET_WORK_DISCONNECTED = "网络连接异常"
|
||||
|
||||
private fun handle(throwable: Throwable): String =
|
||||
when (throwable) {
|
||||
is IOException -> {
|
||||
if (NetWorkUtil.isNetworkAvailable(CommonApplicationProxy.application).not()) {
|
||||
ERROR_NET_WORK_DISCONNECTED
|
||||
} else handIoException(throwable)
|
||||
}
|
||||
else -> ERROR_DEFAULT
|
||||
}
|
||||
|
||||
override fun bizError(code: Int, msg: String) {
|
||||
msg.showShortToast()
|
||||
}
|
||||
|
||||
override fun otherError(throwable: Throwable) {
|
||||
handle(throwable).showShortToast()
|
||||
}
|
||||
|
||||
private fun handIoException(ioException: IOException): String {
|
||||
return when (ioException) {
|
||||
is SocketTimeoutException -> {
|
||||
ERROR_CONNECTED_TIME_OUT
|
||||
}
|
||||
else -> ERROR_DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.lowe.common.base.http
|
||||
|
||||
import com.lowe.common.base.AppLog
|
||||
import com.lowe.common.base.http.adapter.ErrorHandler
|
||||
import com.lowe.common.base.http.adapter.NetworkResponseAdapterFactory
|
||||
import com.lowe.common.base.http.converter.GsonConverterFactory
|
||||
import com.lowe.common.base.http.cookie.UserCookieJarImpl
|
||||
import com.lowe.common.base.http.interceptor.logInterceptor
|
||||
import com.lowe.common.di.ApplicationCoroutineScope
|
||||
import com.lowe.common.di.CoroutinesModule
|
||||
import com.lowe.common.services.BaseService
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Retrofit管理类
|
||||
*/
|
||||
object RetrofitManager {
|
||||
|
||||
const val BASE_URL = "https://www.wanandroid.com"
|
||||
|
||||
private const val TIME_OUT_SECONDS = 10
|
||||
|
||||
private lateinit var cookieJarImpl: UserCookieJarImpl
|
||||
|
||||
/** OkHttpClient相关配置 */
|
||||
private val client: OkHttpClient
|
||||
get() = OkHttpClient.Builder()
|
||||
.addInterceptor(logInterceptor)
|
||||
.cookieJar(cookieJarImpl)
|
||||
.connectTimeout(TIME_OUT_SECONDS.toLong(), TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val servicesMap = ConcurrentHashMap<String, BaseService>()
|
||||
private val errorHandlers = mutableListOf<ErrorHandler>()
|
||||
|
||||
fun init(cookieJar: UserCookieJarImpl) {
|
||||
cookieJarImpl = cookieJar
|
||||
addErrorHandlerListener(ErrorToastHandler)
|
||||
}
|
||||
|
||||
fun addErrorHandlerListener(handler: ErrorHandler) {
|
||||
errorHandlers.add(handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo(Inject Implementation)
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : BaseService> getService(serviceClass: Class<T>, baseUrl: String? = null): T {
|
||||
return servicesMap.getOrPut(serviceClass.name) {
|
||||
Retrofit.Builder()
|
||||
.client(client)
|
||||
.addCallAdapterFactory(NetworkResponseAdapterFactory(object : ErrorHandler {
|
||||
override fun bizError(code: Int, msg: String) {
|
||||
ApplicationCoroutineScope.provideApplicationScope()
|
||||
.launch(CoroutinesModule.providesMainImmediateDispatcher()) {
|
||||
errorHandlers.forEach { it.bizError(code, msg) }
|
||||
}
|
||||
AppLog.d(msg = "bizError: code:$code - msg: $msg")
|
||||
}
|
||||
|
||||
override fun otherError(throwable: Throwable) {
|
||||
ApplicationCoroutineScope.provideApplicationScope()
|
||||
.launch(CoroutinesModule.providesMainImmediateDispatcher()) {
|
||||
errorHandlers.forEach { it.otherError(throwable) }
|
||||
}
|
||||
AppLog.e(msg = throwable.message.toString(), throwable = throwable)
|
||||
}
|
||||
}))
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.baseUrl(baseUrl ?: BASE_URL)
|
||||
.build()
|
||||
.create(serviceClass)
|
||||
} as T
|
||||
}
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.lowe.common.base.http.adapter
|
||||
|
||||
/**
|
||||
* 用于配置全局的异常处理逻辑
|
||||
*/
|
||||
interface ErrorHandler {
|
||||
|
||||
/**
|
||||
* 业务错误
|
||||
*/
|
||||
fun bizError(code: Int, msg: String)
|
||||
|
||||
/**
|
||||
* 其他错误
|
||||
*/
|
||||
fun otherError(throwable: Throwable)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.lowe.common.base.http.adapter
|
||||
|
||||
import com.lowe.common.base.http.exception.ApiException
|
||||
import com.lowe.common.result.Result
|
||||
|
||||
/**
|
||||
* 接口的返回类型包装类
|
||||
*/
|
||||
sealed class NetworkResponse<out T : Any> {
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
data class Success<T : Any>(val data: T) : NetworkResponse<T>()
|
||||
|
||||
/**
|
||||
* 业务错误
|
||||
*/
|
||||
data class BizError(val errorCode: Int = 0, val errorMessage: String = "") :
|
||||
NetworkResponse<Nothing>()
|
||||
|
||||
/**
|
||||
* 其他错误
|
||||
*/
|
||||
data class UnknownError(val throwable: Throwable) : NetworkResponse<Nothing>()
|
||||
}
|
||||
|
||||
inline val NetworkResponse<*>.isSuccess: Boolean
|
||||
get() {
|
||||
return this is NetworkResponse.Success
|
||||
}
|
||||
|
||||
fun <T : Any> NetworkResponse<T>.getOrNull(): T? =
|
||||
when (this) {
|
||||
is NetworkResponse.Success -> data
|
||||
is NetworkResponse.BizError -> null
|
||||
is NetworkResponse.UnknownError -> null
|
||||
}
|
||||
|
||||
fun <T : Any> NetworkResponse<T>.exceptionOrNull(): Throwable? =
|
||||
when (this) {
|
||||
is NetworkResponse.Success -> null
|
||||
is NetworkResponse.BizError -> ApiException(errorCode, errorMessage)
|
||||
is NetworkResponse.UnknownError -> throwable
|
||||
}
|
||||
|
||||
fun <T : Any> NetworkResponse<T>.getOrThrow(): T =
|
||||
when (this) {
|
||||
is NetworkResponse.Success -> data
|
||||
is NetworkResponse.BizError -> throw ApiException(errorCode, errorMessage)
|
||||
is NetworkResponse.UnknownError -> throw throwable
|
||||
}
|
||||
|
||||
inline fun <T : Any> NetworkResponse<T>.getOrElse(default: (NetworkResponse<T>) -> T): T =
|
||||
when (this) {
|
||||
is NetworkResponse.Success -> data
|
||||
else -> default(this)
|
||||
}
|
||||
|
||||
inline fun <T : Any> NetworkResponse<T>.whenSuccess(
|
||||
block: (T) -> Unit
|
||||
) {
|
||||
(this as? NetworkResponse.Success)?.data?.also(block)
|
||||
}
|
||||
|
||||
inline fun <T : Any> NetworkResponse<T>.guardSuccess(
|
||||
block: () -> Nothing
|
||||
): T {
|
||||
if (this !is NetworkResponse.Success) {
|
||||
block()
|
||||
}
|
||||
return this.data
|
||||
}
|
||||
|
||||
fun <T : Any> NetworkResponse<T>.toResult(): Result<T> {
|
||||
return if (this is NetworkResponse.Success<T>) {
|
||||
Result.Success(this.data)
|
||||
} else {
|
||||
Result.Error(exceptionOrNull())
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.lowe.common.base.http.adapter
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class NetworkResponseAdapter(
|
||||
private val successType: Type,
|
||||
private val errorHandler: ErrorHandler?
|
||||
) : CallAdapter<Any, Call<Any>> {
|
||||
|
||||
override fun responseType(): Type = successType
|
||||
|
||||
override fun adapt(call: Call<Any>): Call<Any> =
|
||||
NetworkResponseCall(call, successType as ParameterizedType, errorHandler)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.lowe.common.base.http.adapter
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class NetworkResponseAdapterFactory(
|
||||
private val errorHandler: ErrorHandler? = null
|
||||
) : CallAdapter.Factory() {
|
||||
|
||||
override fun get(
|
||||
returnType: Type,
|
||||
annotations: Array<Annotation>,
|
||||
retrofit: Retrofit
|
||||
): CallAdapter<*, *>? {
|
||||
|
||||
// suspend 函数在 Retrofit 中的返回值其实是 `Call`
|
||||
if (Call::class.java != getRawType(returnType)) return null
|
||||
|
||||
// 检查返回类型是否为 `ParameterizedType`
|
||||
check(returnType is ParameterizedType) {
|
||||
"return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
|
||||
}
|
||||
|
||||
// 获取Call内的一层泛型类型
|
||||
val responseType = getParameterUpperBound(0, returnType)
|
||||
|
||||
// 如果非NetworkResponse不处理
|
||||
if (getRawType(responseType) != NetworkResponse::class.java) return null
|
||||
|
||||
return NetworkResponseAdapter(responseType, errorHandler)
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.lowe.common.base.http.adapter
|
||||
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
internal class NetworkResponseCall(
|
||||
private val delegate: Call<Any>,
|
||||
private val wrapperType: ParameterizedType,
|
||||
private val errorHandler: ErrorHandler?
|
||||
) : Call<Any> {
|
||||
|
||||
override fun enqueue(callback: Callback<Any>): Unit =
|
||||
delegate.enqueue(object : Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
// 无论请求响应成功还是失败都回调 Response.success
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body()
|
||||
if (body is NetworkResponse.BizError) {
|
||||
errorHandler?.bizError(body.errorCode, body.errorMessage)
|
||||
}
|
||||
callback.onResponse(this@NetworkResponseCall, Response.success(body))
|
||||
} else {
|
||||
val exception = HttpException(response)
|
||||
errorHandler?.otherError(exception)
|
||||
callback.onResponse(
|
||||
this@NetworkResponseCall,
|
||||
Response.success(NetworkResponse.UnknownError(exception))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
if (call.isCanceled) {
|
||||
// 忽略请求被Canceled的情况
|
||||
return
|
||||
}
|
||||
errorHandler?.otherError(t)
|
||||
callback.onResponse(
|
||||
this@NetworkResponseCall,
|
||||
Response.success(NetworkResponse.UnknownError(t))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun clone(): Call<Any> =
|
||||
NetworkResponseCall(delegate, wrapperType, errorHandler)
|
||||
|
||||
override fun execute(): Response<Any> =
|
||||
throw UnsupportedOperationException("${this.javaClass.name} doesn't support execute")
|
||||
|
||||
override fun isExecuted(): Boolean = delegate.isExecuted
|
||||
override fun cancel(): Unit = delegate.cancel()
|
||||
override fun isCanceled(): Boolean = delegate.isCanceled
|
||||
override fun request(): Request = delegate.request()
|
||||
override fun timeout(): Timeout = delegate.timeout()
|
||||
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.lowe.common.base.http.converter
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class GsonConverterFactory(private val gson: Gson) : Converter.Factory() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun create(): GsonConverterFactory {
|
||||
return create(GsonBuilder().disableHtmlEscaping().create())
|
||||
}
|
||||
|
||||
private fun create(gson: Gson?): GsonConverterFactory {
|
||||
if (gson == null) throw NullPointerException("gson == null")
|
||||
return GsonConverterFactory(gson)
|
||||
}
|
||||
}
|
||||
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): GsonResponseBodyConverter<out Any> {
|
||||
check(type is ParameterizedType) {
|
||||
"type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
|
||||
}
|
||||
return GsonResponseBodyConverter(
|
||||
gson,
|
||||
/**
|
||||
* 获取NetWorkResponse包装内的第一个泛型,如NetWorkResponse<List<Article>>获取List<Article>以让Gson成功解析
|
||||
*/
|
||||
gson.getAdapter(TypeToken.get(getParameterUpperBound(0, type)))
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestBodyConverter(
|
||||
type: Type,
|
||||
parameterAnnotations: Array<out Annotation>,
|
||||
methodAnnotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
) = GsonRequestBodyConverter(gson, gson.getAdapter(TypeToken.get(type)))
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.lowe.common.base.http.converter
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.TypeAdapter
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okio.Buffer
|
||||
import retrofit2.Converter
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class GsonRequestBodyConverter<T>(
|
||||
private val gson: Gson,
|
||||
private val adapter: TypeAdapter<T>
|
||||
) : Converter<T, RequestBody> {
|
||||
|
||||
override fun convert(value: T): RequestBody {
|
||||
val buffer = Buffer()
|
||||
val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
|
||||
val jsonWriter = gson.newJsonWriter(writer)
|
||||
adapter.write(jsonWriter, value)
|
||||
jsonWriter.close()
|
||||
return buffer.readByteString().toRequestBody(MEDIA_TYPE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MEDIA_TYPE = "application/json; charset=UTF-8".toMediaTypeOrNull()
|
||||
private val UTF_8 = Charset.forName("UTF-8")
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.lowe.common.base.http.converter
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Converter
|
||||
|
||||
class GsonResponseBodyConverter<T : Any>(
|
||||
private val gson: Gson,
|
||||
private val adapter: TypeAdapter<T>
|
||||
) : Converter<ResponseBody, NetworkResponse<T>> {
|
||||
|
||||
override fun convert(value: ResponseBody): NetworkResponse<T> {
|
||||
val jsonReader = gson.newJsonReader(value.charStream())
|
||||
value.use {
|
||||
jsonReader.beginObject()
|
||||
var errorCode = 0
|
||||
var errorMsg = ""
|
||||
var data: T? = null
|
||||
while (jsonReader.hasNext()) {
|
||||
when (jsonReader.nextName()) {
|
||||
"errorCode" -> errorCode = jsonReader.nextInt()
|
||||
"errorMsg" -> errorMsg = jsonReader.nextString()
|
||||
"data" -> data = adapter.read(jsonReader)
|
||||
else -> jsonReader.skipValue()
|
||||
}
|
||||
}
|
||||
jsonReader.endObject()
|
||||
return if (errorCode != 0) {
|
||||
NetworkResponse.BizError(errorCode, errorMsg)
|
||||
} else if (data == null) {
|
||||
/**
|
||||
* 由于接口会有"data":null的情况,这里兜底替换为Any(),保证Success里data的非空性
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
NetworkResponse.Success(Any()) as NetworkResponse<T>
|
||||
} else {
|
||||
NetworkResponse.Success(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.lowe.common.base.http.cookie
|
||||
|
||||
import android.util.Log
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cookie
|
||||
import java.io.IOException
|
||||
|
||||
class CookieCacheHelper(
|
||||
private val cookieDataStore: DataStore<Preferences>,
|
||||
private val applicationScope: CoroutineScope,
|
||||
private val ioDispatcher: CoroutineDispatcher
|
||||
) : ICookieCache {
|
||||
|
||||
private val cookieCache = MutableStateFlow((emptyList<Cookie>()))
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
cookieDataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else throw it
|
||||
}
|
||||
.map { preferences ->
|
||||
Log.d("UserCookieJarImpl", "map preferences: ${preferences}")
|
||||
preferences.asMap().values.mapNotNull {
|
||||
if (it is String) Json.decodeFromString(CookieSerializer, it) else null
|
||||
}
|
||||
}.collectLatest {
|
||||
cookieCache.value = it
|
||||
Log.d("UserCookieJarImpl", "cookieDataStore: ${cookieCache.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCookieCache(): StateFlow<List<Cookie>> = cookieCache.asStateFlow()
|
||||
|
||||
override fun snapshot(): List<Cookie> = cookieCache.value
|
||||
|
||||
override fun saveAll(cookies: Collection<Cookie>) {
|
||||
// fast-path
|
||||
if (cookies.isEmpty()) return
|
||||
cookieCache.value = (cookieCache.value + cookies).distinctBy { it.key }
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
cookieDataStore.edit { preferences ->
|
||||
preferences.putAll(
|
||||
*cookies.map {
|
||||
stringPreferencesKey(it.key) to Json.encodeToString(CookieSerializer, it)
|
||||
}.toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAll(cookies: Collection<Cookie>) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
cookieDataStore.edit { preferences ->
|
||||
cookies.forEach {
|
||||
preferences.remove(stringPreferencesKey(it.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
// fast-path
|
||||
cookieCache.value = emptyList()
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
cookieDataStore.edit { preferences ->
|
||||
preferences.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.lowe.common.base.http.cookie
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.*
|
||||
import okhttp3.Cookie
|
||||
|
||||
object CookieSerializer : KSerializer<Cookie> {
|
||||
|
||||
override fun deserialize(decoder: Decoder): Cookie {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var name = ""
|
||||
var value = ""
|
||||
var expiresAt = 0L
|
||||
var domain = ""
|
||||
var path = "/"
|
||||
var secure = false
|
||||
var httpOnly = false
|
||||
var persistent = false
|
||||
var hostOnly = false
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> name = decodeStringElement(descriptor, index)
|
||||
1 -> value = decodeStringElement(descriptor, index)
|
||||
2 -> expiresAt = decodeLongElement(descriptor, index)
|
||||
3 -> domain = decodeStringElement(descriptor, index)
|
||||
4 -> path = decodeStringElement(descriptor, index)
|
||||
5 -> secure = decodeBooleanElement(descriptor, index)
|
||||
6 -> httpOnly = decodeBooleanElement(descriptor, index)
|
||||
7 -> persistent = decodeBooleanElement(descriptor, index)
|
||||
8 -> hostOnly = decodeBooleanElement(descriptor, index)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
Cookie.Builder().name(name).value(value).expiresAt(expiresAt).path(path)
|
||||
.apply {
|
||||
if (hostOnly) hostOnlyDomain(domain) else domain(domain)
|
||||
if (secure) secure()
|
||||
if (httpOnly) httpOnly()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = buildClassSerialDescriptor("Cookie") {
|
||||
element<String>("name")
|
||||
element<String>("value")
|
||||
element<Long>("expiresAt")
|
||||
element<String>("domain")
|
||||
element<String>("path")
|
||||
element<Boolean>("secure")
|
||||
element<Boolean>("httpOnly")
|
||||
element<Boolean>("persistent")
|
||||
element<Boolean>("hostOnly")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Cookie) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
encodeStringElement(descriptor, 0, value.name)
|
||||
encodeStringElement(descriptor, 1, value.value)
|
||||
encodeLongElement(descriptor, 2, value.expiresAt)
|
||||
encodeStringElement(descriptor, 3, value.domain)
|
||||
encodeStringElement(descriptor, 4, value.path)
|
||||
encodeBooleanElement(descriptor, 5, value.secure)
|
||||
encodeBooleanElement(descriptor, 6, value.httpOnly)
|
||||
encodeBooleanElement(descriptor, 7, value.persistent)
|
||||
encodeBooleanElement(descriptor, 8, value.hostOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.lowe.common.base.http.cookie
|
||||
|
||||
import okhttp3.Cookie
|
||||
|
||||
interface ICookieCache {
|
||||
|
||||
fun snapshot(): Collection<Cookie>
|
||||
|
||||
fun saveAll(cookies: Collection<Cookie>)
|
||||
|
||||
fun removeAll(cookies: Collection<Cookie>)
|
||||
|
||||
fun clear()
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.lowe.common.base.http.cookie
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
internal inline val Cookie.key: String
|
||||
get() = (if (secure) "https" else "http") + "://" + domain + path + "|" + name
|
||||
|
||||
internal fun Cookie.isExpired() = expiresAt < System.currentTimeMillis()
|
||||
|
||||
const val COOKIE_LOGIN_USER_NAME = "loginUserName_wanandroid_com"
|
||||
const val COOKIE_LOGIN_USER_TOKEN = "token_pass_wanandroid_com"
|
||||
|
||||
class UserCookieJarImpl(private val cookieCacheHelper: CookieCacheHelper) : CookieJar {
|
||||
|
||||
@Synchronized
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val (expiredCookies, validCookies) = cookieCacheHelper.snapshot()
|
||||
.partition { it.isExpired() }
|
||||
Log.d("UserCookieJarImpl", "loadForRequest: url:$url - expired: $expiredCookies - valid: $validCookies")
|
||||
cookieCacheHelper.removeAll(expiredCookies)
|
||||
return validCookies.filter { it.matches(url) }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val (expiredCookies, validCookies) = cookies.partition { it.isExpired() }
|
||||
Log.d("UserCookieJarImpl", "saveFromResponse: url:$url - expired: $expiredCookies - valid: $validCookies")
|
||||
cookieCacheHelper.removeAll(expiredCookies)
|
||||
cookieCacheHelper.saveAll(validCookies.filter { it.persistent })
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
cookieCacheHelper.clear()
|
||||
}
|
||||
|
||||
suspend fun isLoginCookieValid(): Boolean {
|
||||
var isUserNameValid = false
|
||||
var isUserTokenValid = false
|
||||
cookieCacheHelper.getCookieCache().first().forEach {
|
||||
if (it.name == COOKIE_LOGIN_USER_NAME) {
|
||||
isUserNameValid = it.value.isNotBlank()
|
||||
}
|
||||
if (it.name == COOKIE_LOGIN_USER_TOKEN) {
|
||||
isUserTokenValid = it.value.isNotBlank()
|
||||
}
|
||||
}
|
||||
return isUserNameValid && isUserTokenValid
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.lowe.common.base.http.exception
|
||||
|
||||
class ApiException(val code: Int, override val message: String?) : RuntimeException(message) {
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = -77705430766904704L
|
||||
|
||||
const val CODE_NOT_LOGGED_IN = -1001
|
||||
}
|
||||
|
||||
fun isNotLogged() = code == CODE_NOT_LOGGED_IN
|
||||
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.lowe.common.base.http.interceptor
|
||||
|
||||
import com.lowe.common.BuildConfig
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
val logInterceptor by lazy {
|
||||
HttpLoggingInterceptor { com.lowe.common.base.AppLog.d(msg = it) }.setLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.lowe.common.compat
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import com.lowe.common.utils.SDKUtils
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Bundle Compat类
|
||||
*/
|
||||
object BundleCompat {
|
||||
inline fun <reified T : Parcelable> getParcelable(bundle: Bundle?, key: String?) =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
bundle?.getParcelable(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
bundle?.getParcelable(key)
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> getSerializable(bundle: Bundle?, key: String?) =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
bundle?.getSerializable(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
bundle?.getSerializable(key) as T?
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> getParcelableArrayList(bundle: Bundle?, key: String?) =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
bundle?.getParcelableArrayList(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
bundle?.getParcelableArrayList(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.lowe.common.compat
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import com.lowe.common.utils.SDKUtils
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Intent Compat类
|
||||
*/
|
||||
object IntentCompat {
|
||||
inline fun <reified T : Parcelable> getParcelableExtra(intent: Intent, name: String): T? =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
intent.getParcelableExtra(name, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(name)
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> getParcelableArrayListExtra(
|
||||
intent: Intent,
|
||||
name: String
|
||||
): ArrayList<T>? =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
intent.getParcelableArrayListExtra(name, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra(name)
|
||||
}
|
||||
|
||||
inline fun <reified T : Serializable> getSerializableExtra(
|
||||
intent: Intent,
|
||||
name: String
|
||||
): Serializable? =
|
||||
if (SDKUtils.atLeast33()) {
|
||||
intent.getSerializableExtra(name, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getSerializableExtra(name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.lowe.common.constant
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lowe.common.base.app.CommonApplicationProxy
|
||||
|
||||
/**
|
||||
* 设置页常量
|
||||
*/
|
||||
object SettingConstants {
|
||||
const val PREFERENCE_KEY_NORMAL_CATEGORY_THEME = "normal_theme"
|
||||
const val PREFERENCE_KEY_NORMAL_CATEGORY_DARK_MODE = "normal_darkMode"
|
||||
private const val DARK_MODE_ON = "on"
|
||||
private const val DARK_MODE_OFF = "off"
|
||||
private const val DARK_MODE_FOLLOW_SYSTEM = "system"
|
||||
|
||||
const val PREFERENCE_KEY_OTHER_CATEGORY_ABOUT = "other_about"
|
||||
const val PREFERENCE_KEY_OTHER_CATEGORY_GITHUB = "other_github"
|
||||
const val PREFERENCE_KEY_OTHER_CATEGORY_LOGOUT = "other_logout"
|
||||
|
||||
/**
|
||||
* 是否开启深色模式
|
||||
*/
|
||||
@AppCompatDelegate.NightMode
|
||||
val preferenceDarkMode: Int
|
||||
get() {
|
||||
return getNightMode(
|
||||
PreferenceManager.getDefaultSharedPreferences(CommonApplicationProxy.application)
|
||||
.getString(PREFERENCE_KEY_NORMAL_CATEGORY_DARK_MODE, DARK_MODE_FOLLOW_SYSTEM)
|
||||
?: DARK_MODE_FOLLOW_SYSTEM
|
||||
)
|
||||
}
|
||||
|
||||
fun getNightMode(value: String) =
|
||||
when (value) {
|
||||
DARK_MODE_ON -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
DARK_MODE_OFF -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class ApplicationScope
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
@Deprecated(message = "use provideApplicationScope() instead", replaceWith = ReplaceWith(expression = "@ApplicationScope with @Dispatcher"))
|
||||
annotation class IoApplicationScope
|
||||
|
||||
/**
|
||||
* Application周期内的[CoroutineScope]提供者,当需要在页面生命周期之外开启协程时使用
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object ApplicationCoroutineScope {
|
||||
|
||||
/**
|
||||
* 默认[Dispatchers.IO]
|
||||
*/
|
||||
private val ioApplicationScope by lazy {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.IO + CoroutineExceptionHandler { _, throwable ->
|
||||
com.lowe.common.base.AppLog.e(
|
||||
"IOApplicationScope:\n${throwable.message.toString()}", throwable = throwable
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private val applicationScope =
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineExceptionHandler { _, throwable ->
|
||||
com.lowe.common.base.AppLog.e(
|
||||
"applicationScope:\n${throwable.message.toString()}", throwable = throwable
|
||||
)
|
||||
})
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@ApplicationScope
|
||||
fun provideApplicationScope() = applicationScope
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@IoApplicationScope
|
||||
@Deprecated(message = "use provideApplicationScope() instead", replaceWith = ReplaceWith(expression = "@ApplicationScope with @Dispatcher"))
|
||||
fun providesIOCoroutineScope() = ioApplicationScope
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.lowe.common.base.http.cookie.CookieCacheHelper
|
||||
import com.lowe.common.base.http.cookie.UserCookieJarImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object CookieJarModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideCookieJar(
|
||||
@CookieDataStore dataStore: DataStore<Preferences>,
|
||||
@ApplicationScope scope: CoroutineScope,
|
||||
@IoDispatcher dispatcher: CoroutineDispatcher
|
||||
) = UserCookieJarImpl(CookieCacheHelper(dataStore, scope, dispatcher))
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class MainDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class DefaultDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class IoDispatcher
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class MainImmediateDispatcher
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object CoroutinesModule {
|
||||
|
||||
@DefaultDispatcher
|
||||
@Provides
|
||||
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@IoDispatcher
|
||||
@Provides
|
||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@MainDispatcher
|
||||
@Provides
|
||||
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
@MainImmediateDispatcher
|
||||
@Provides
|
||||
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.lowe.common.base.datastore.DataStorePreference
|
||||
import com.lowe.common.base.datastore.PreferenceStorage
|
||||
import com.lowe.common.base.http.DataStoreFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Qualifier
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Qualifier
|
||||
annotation class CookieDataStore
|
||||
|
||||
/**
|
||||
* [DataStore] 提供者
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object DataStoreModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDefaultDataStore(): DataStore<Preferences> =
|
||||
DataStoreFactory.getDefaultPreferencesDataStore()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@CookieDataStore
|
||||
fun provideCookieDataStore(@ApplicationContext context: Context): DataStore<Preferences> =
|
||||
DataStoreFactory.getPreferencesDataStore(
|
||||
context,
|
||||
DataStoreFactory.Name.DATA_STORE_NAME_COOKIE
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideDataStorePreference(dataStore: DataStore<Preferences>): PreferenceStorage =
|
||||
DataStorePreference(dataStore)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import com.lowe.common.services.*
|
||||
import com.lowe.common.services.impl.*
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Service 实现提供者
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
abstract class ServiceImplModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getHomeServiceImpl(impl: HomeServiceImpl): HomeService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getProjectServiceImpl(impl: ProjectServiceImpl): ProjectService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getNavigatorServiceImpl(impl: NavigatorServiceImpl): NavigatorService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getGroupServiceImpl(impl: GroupServiceImpl): GroupService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getProfileServiceImpl(impl: ProfileServiceImpl): ProfileService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getSearchServiceImpl(impl: SearchServiceImpl): SearchService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getCollectServiceImpl(impl: CollectServiceImpl): CollectService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getAccountServiceImpl(impl: AccountServiceImpl): AccountService
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun getCoinServiceImpl(impl: CoinServiceImpl): CoinService
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.lowe.common.di
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.base.app.AppViewModelFactory
|
||||
import com.lowe.common.base.app.CommonApplicationProxy
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* [AppViewModel] 提供者
|
||||
*/
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object ViewModelModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAppViewModel(factory: AppViewModelFactory) = ViewModelProvider(CommonApplicationProxy.viewModelStore, factory)[AppViewModel::class.java]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.lowe.common.result
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
|
||||
open class Event<out T>(private val content: T) {
|
||||
|
||||
var hasBeenHandled = false
|
||||
private set // Allow external read but not write
|
||||
|
||||
fun getContentIfNotHandled(): T? {
|
||||
return if (hasBeenHandled) {
|
||||
null
|
||||
} else {
|
||||
hasBeenHandled = true
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
fun peekContent(): T = content
|
||||
}
|
||||
|
||||
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
|
||||
override fun onChanged(event: Event<T>?) {
|
||||
event?.getContentIfNotHandled()?.let { value ->
|
||||
onEventUnhandledContent(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.lowe.common.result
|
||||
|
||||
import com.lowe.common.result.Result.Success
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
sealed class Result<out R> {
|
||||
|
||||
data class Success<out T>(val data: T) : Result<T>()
|
||||
data class Error(val throwable: Throwable?) : Result<Nothing>()
|
||||
object Loading : Result<Nothing>()
|
||||
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
is Success<*> -> "Success[data=$data]"
|
||||
is Error -> "Error[throwable=$throwable]"
|
||||
Loading -> "Loading"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Result<*>.succeeded
|
||||
get() = this is Success && data != null
|
||||
|
||||
fun <T> Result<T>.successOr(fallback: T): T {
|
||||
return (this as? Success<T>)?.data ?: fallback
|
||||
}
|
||||
|
||||
val <T> Result<T>.data: T?
|
||||
get() = (this as? Success)?.data
|
||||
|
||||
inline fun <reified T> Result<T>.updateOnSuccess(stateFlow: MutableStateFlow<T>) {
|
||||
if (this is Success) {
|
||||
stateFlow.value = data
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.User
|
||||
import com.lowe.common.services.model.UserBaseInfo
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AccountService : BaseService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST("user/login")
|
||||
suspend fun login(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String
|
||||
): NetworkResponse<User>
|
||||
|
||||
@GET("user/logout/json")
|
||||
suspend fun logout(): NetworkResponse<Any>
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST("user/register")
|
||||
suspend fun register(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
@Field("repassword") confirmPassword: String
|
||||
): NetworkResponse<Any>
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
@GET("user/lg/userinfo/json")
|
||||
suspend fun getUserInfo(): NetworkResponse<UserBaseInfo>
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
interface BaseService {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 默认初始页数
|
||||
*/
|
||||
const val DEFAULT_PAGE_START_NO = 0
|
||||
|
||||
const val DEFAULT_PAGE_START_NO_1 = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.CoinHistory
|
||||
import com.lowe.common.services.model.CoinInfo
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface CoinService : BaseService {
|
||||
|
||||
@GET("/lg/coin/list/{page}/json")
|
||||
suspend fun getMyCoinList(@Path("page") page: Int): NetworkResponse<PageResponse<CoinHistory>>
|
||||
|
||||
@GET("coin/rank/{page}/json")
|
||||
suspend fun getCoinRanking(@Path("page") page: Int): NetworkResponse<PageResponse<CoinInfo>>
|
||||
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.CollectBean
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface CollectService : BaseService {
|
||||
|
||||
/**
|
||||
* 收藏列表
|
||||
*/
|
||||
@GET("lg/collect/list/{page}/json")
|
||||
suspend fun getCollectList(@Path("page") page: Int): NetworkResponse<PageResponse<CollectBean>>
|
||||
|
||||
/**
|
||||
* 收藏站内文章
|
||||
*/
|
||||
@POST("lg/collect/{id}/json")
|
||||
suspend fun collectArticle(@Path("id") id: Int): NetworkResponse<Any>
|
||||
|
||||
/**
|
||||
* 取消收藏站内文章
|
||||
*/
|
||||
@POST("lg/uncollect_originId/{id}/json")
|
||||
suspend fun unCollectArticle(@Path("id") id: Int): NetworkResponse<Any>
|
||||
|
||||
suspend fun isCollectArticle(collect: Boolean, id: Int) =
|
||||
if (collect) collectArticle(id) else unCollectArticle(id)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface GroupService : BaseService {
|
||||
|
||||
/**
|
||||
* 公众号作者列表
|
||||
*/
|
||||
@GET("wxarticle/chapters/json")
|
||||
suspend fun getAuthorTitleList(): NetworkResponse<List<Classify>>
|
||||
|
||||
/**
|
||||
* 对于id作者的文章
|
||||
*/
|
||||
@GET("wxarticle/list/{id}/{page}/json")
|
||||
suspend fun getAuthorArticles(
|
||||
@Path("id") id: Int,
|
||||
@Path("page") page: Int,
|
||||
@Query("page_size") pageSize: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Banner
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface HomeService : BaseService {
|
||||
|
||||
/**
|
||||
* 首页banner
|
||||
*/
|
||||
@GET("banner/json")
|
||||
suspend fun getBanner(): NetworkResponse<List<Banner>>
|
||||
|
||||
/**
|
||||
* 首页文章
|
||||
*/
|
||||
@GET("article/list/{pageNo}/json")
|
||||
suspend fun getArticlePageList(
|
||||
@Path("pageNo") pageNo: Int,
|
||||
@Query("page_size") pageSize: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
|
||||
/**
|
||||
* 首页置顶文章
|
||||
*/
|
||||
@GET("article/top/json")
|
||||
suspend fun getArticleTopList(): NetworkResponse<List<Article>>
|
||||
|
||||
/**
|
||||
* 广场文章
|
||||
*/
|
||||
@GET("user_article/list/{pageNo}/json")
|
||||
suspend fun getSquarePageList(
|
||||
@Path("pageNo") pageNo: Int,
|
||||
@Query("page_size") pageSize: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
|
||||
/**
|
||||
* 问答列表
|
||||
*/
|
||||
@GET("wenda/list/{pageNo}/json")
|
||||
suspend fun getAnswerPageList(@Path("pageNo") pageNo: Int): NetworkResponse<PageResponse<Article>>
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.*
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface NavigatorService : BaseService {
|
||||
|
||||
/**
|
||||
* 导航数据
|
||||
*/
|
||||
@GET("navi/json")
|
||||
suspend fun getNavigationList(): NetworkResponse<List<Navigation>>
|
||||
|
||||
/**
|
||||
* 体系数据
|
||||
*/
|
||||
@GET("tree/json")
|
||||
suspend fun getTreeList(): NetworkResponse<List<Series>>
|
||||
|
||||
/**
|
||||
* 教程列表
|
||||
*/
|
||||
@GET("chapter/547/sublist/json")
|
||||
suspend fun getTutorialList(): NetworkResponse<List<Classify>>
|
||||
|
||||
/**
|
||||
* 对应教程的章节列表
|
||||
*/
|
||||
@GET("article/list/0/json")
|
||||
suspend fun getTutorialChapterList(
|
||||
@Query("cid") id: Int,
|
||||
@Query("order_type") orderType: Int = 1
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
|
||||
/**
|
||||
* 系列对应Tag的文章列表
|
||||
*/
|
||||
@GET("article/list/{page}/json")
|
||||
suspend fun getSeriesDetailList(
|
||||
@Path("page") page: Int,
|
||||
@Query("cid") id: Int,
|
||||
@Query("page_size") size: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.MsgBean
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import com.lowe.common.services.model.ShareBean
|
||||
import com.lowe.common.services.model.ToolBean
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface ProfileService : BaseService {
|
||||
|
||||
/**
|
||||
* 已读消息列表
|
||||
*/
|
||||
@GET("message/lg/readed_list/{page}/json")
|
||||
suspend fun getReadiedMessageList(@Path("page") page: Int): NetworkResponse<PageResponse<MsgBean>>
|
||||
|
||||
/**
|
||||
* 未读消息列表
|
||||
*/
|
||||
@GET("message/lg/unread_list//{page}/json")
|
||||
suspend fun getUnReadMessageList(@Path("page") page: Int): NetworkResponse<PageResponse<MsgBean>>
|
||||
|
||||
/**
|
||||
* 我的分享文章列表
|
||||
*/
|
||||
@GET("user/lg/private_articles/{page}/json")
|
||||
suspend fun getMyShareList(@Path("page") page: Int): NetworkResponse<ShareBean>
|
||||
|
||||
/**
|
||||
* 对应用户的分享文章列表
|
||||
*/
|
||||
@GET("user/{userId}/share_articles/{page}/json")
|
||||
suspend fun getUserShareList(
|
||||
@Path("userId") userId: String,
|
||||
@Path("page") page: Int
|
||||
): NetworkResponse<ShareBean>
|
||||
|
||||
/**
|
||||
* 工具列表
|
||||
*/
|
||||
@GET("tools/list/json")
|
||||
suspend fun getToolList(): NetworkResponse<List<ToolBean>>
|
||||
|
||||
@GET("message/lg/count_unread/json")
|
||||
suspend fun getUnreadMessageCount(): NetworkResponse<Int>
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import com.lowe.common.services.model.ProjectTitle
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ProjectService : BaseService {
|
||||
|
||||
/**
|
||||
* 项目分类数据
|
||||
*/
|
||||
@GET("project/tree/json")
|
||||
suspend fun getProjectTitleList(): NetworkResponse<List<ProjectTitle>>
|
||||
|
||||
/**
|
||||
* 项目文章列表
|
||||
*/
|
||||
@GET("project/list/{pageNo}/json")
|
||||
suspend fun getProjectPageList(
|
||||
@Path("pageNo") pageNo: Int,
|
||||
@Query("page_size") pageSize: Int,
|
||||
@Query("cid") categoryId: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
|
||||
/**
|
||||
* 最新项目列表
|
||||
*/
|
||||
@GET("article/listproject/{pageNo}/json")
|
||||
suspend fun getNewProjectPageList(
|
||||
@Path("pageNo") pageNo: Int,
|
||||
@Query("page_size") pageSize: Int
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lowe.common.services
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.HotKeyBean
|
||||
import com.lowe.common.services.model.PageResponse
|
||||
import retrofit2.http.*
|
||||
|
||||
interface SearchService : BaseService {
|
||||
|
||||
/**
|
||||
* 热搜词
|
||||
*/
|
||||
@GET("hotkey/json")
|
||||
suspend fun getSearchHotKey(): NetworkResponse<List<HotKeyBean>>
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*/
|
||||
@POST("article/query/{page}/json")
|
||||
@FormUrlEncoded
|
||||
suspend fun queryBySearchKey(
|
||||
@Path("page") page: Int,
|
||||
@Field("k") key: String
|
||||
): NetworkResponse<PageResponse<Article>>
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.AccountService
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountServiceImpl @Inject constructor() : AccountService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(AccountService::class.java) }
|
||||
|
||||
override suspend fun login(username: String, password: String) =
|
||||
service.login(username, password)
|
||||
|
||||
override suspend fun logout() = service.logout()
|
||||
|
||||
override suspend fun register(
|
||||
username: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
) = service.register(username, password, confirmPassword)
|
||||
|
||||
override suspend fun getUserInfo() = service.getUserInfo()
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.CoinService
|
||||
import javax.inject.Inject
|
||||
|
||||
class CoinServiceImpl @Inject constructor() : CoinService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(CoinService::class.java) }
|
||||
|
||||
override suspend fun getMyCoinList(page: Int) = service.getMyCoinList(page)
|
||||
|
||||
override suspend fun getCoinRanking(page: Int) = service.getCoinRanking(page)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.CollectService
|
||||
import javax.inject.Inject
|
||||
|
||||
class CollectServiceImpl @Inject constructor() : CollectService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(CollectService::class.java) }
|
||||
|
||||
override suspend fun getCollectList(page: Int) = service.getCollectList(page)
|
||||
|
||||
override suspend fun collectArticle(id: Int) = service.collectArticle(id)
|
||||
|
||||
override suspend fun unCollectArticle(id: Int) = service.unCollectArticle(id)
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.GroupService
|
||||
import javax.inject.Inject
|
||||
|
||||
class GroupServiceImpl @Inject constructor() : GroupService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(GroupService::class.java) }
|
||||
|
||||
override suspend fun getAuthorTitleList() = service.getAuthorTitleList()
|
||||
|
||||
override suspend fun getAuthorArticles(id: Int, page: Int, pageSize: Int) =
|
||||
service.getAuthorArticles(id, page, pageSize)
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.HomeService
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeServiceImpl @Inject constructor() : HomeService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(HomeService::class.java) }
|
||||
|
||||
override suspend fun getBanner() = service.getBanner()
|
||||
|
||||
override suspend fun getArticleTopList() = service.getArticleTopList()
|
||||
|
||||
override suspend fun getArticlePageList(pageNo: Int, pageSize: Int) =
|
||||
service.getArticlePageList(pageNo, pageSize)
|
||||
|
||||
override suspend fun getSquarePageList(pageNo: Int, pageSize: Int) =
|
||||
service.getSquarePageList(pageNo, pageSize)
|
||||
|
||||
override suspend fun getAnswerPageList(pageNo: Int) = service.getAnswerPageList(pageNo)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.NavigatorService
|
||||
import javax.inject.Inject
|
||||
|
||||
class NavigatorServiceImpl @Inject constructor() : NavigatorService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(NavigatorService::class.java) }
|
||||
|
||||
override suspend fun getNavigationList() = service.getNavigationList()
|
||||
|
||||
override suspend fun getTreeList() = service.getTreeList()
|
||||
|
||||
override suspend fun getTutorialList() = service.getTutorialList()
|
||||
|
||||
override suspend fun getTutorialChapterList(id: Int, orderType: Int) =
|
||||
service.getTutorialChapterList(id, orderType)
|
||||
|
||||
override suspend fun getSeriesDetailList(page: Int, id: Int, size: Int) =
|
||||
service.getSeriesDetailList(page, id, size)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.ProfileService
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProfileServiceImpl @Inject constructor() : ProfileService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(ProfileService::class.java) }
|
||||
|
||||
override suspend fun getMyShareList(page: Int) = service.getMyShareList(page)
|
||||
|
||||
override suspend fun getUserShareList(userId: String, page: Int) =
|
||||
service.getUserShareList(userId, page)
|
||||
|
||||
override suspend fun getToolList() = service.getToolList()
|
||||
|
||||
override suspend fun getReadiedMessageList(page: Int) = service.getReadiedMessageList(page)
|
||||
|
||||
override suspend fun getUnReadMessageList(page: Int) = service.getUnReadMessageList(page)
|
||||
|
||||
override suspend fun getUnreadMessageCount() = service.getUnreadMessageCount()
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.ProjectService
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProjectServiceImpl @Inject constructor() : ProjectService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(ProjectService::class.java) }
|
||||
|
||||
override suspend fun getProjectTitleList() = service.getProjectTitleList()
|
||||
|
||||
override suspend fun getProjectPageList(pageNo: Int, pageSize: Int, categoryId: Int) =
|
||||
service.getProjectPageList(pageNo, pageSize, categoryId)
|
||||
|
||||
override suspend fun getNewProjectPageList(pageNo: Int, pageSize: Int) =
|
||||
service.getNewProjectPageList(pageNo, pageSize)
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.lowe.common.services.impl
|
||||
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.services.SearchService
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchServiceImpl @Inject constructor() : SearchService {
|
||||
|
||||
private val service by lazy { RetrofitManager.getService(SearchService::class.java) }
|
||||
|
||||
override suspend fun getSearchHotKey() = service.getSearchHotKey()
|
||||
|
||||
override suspend fun queryBySearchKey(page: Int, key: String) =
|
||||
service.queryBySearchKey(page, key)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 文章
|
||||
*/
|
||||
data class Article(
|
||||
var apkLink: String,
|
||||
var audit: Int,
|
||||
var author: String,
|
||||
var canEdit: Boolean,
|
||||
var chapterId: Int,
|
||||
var chapterName: String,
|
||||
var collect: Boolean,
|
||||
var courseId: Int,
|
||||
var desc: String,
|
||||
var descMd: String,
|
||||
var envelopePic: String,
|
||||
var fresh: Boolean,
|
||||
var host: String,
|
||||
var id: Int,
|
||||
var link: String,
|
||||
var niceDate: String,
|
||||
var niceShareDate: String,
|
||||
var origin: String,
|
||||
var prefix: String,
|
||||
var projectLink: String,
|
||||
var publishTime: Long,
|
||||
var realSuperChapterId: Int,
|
||||
var selfVisible: Int,
|
||||
var shareDate: Long,
|
||||
var shareUser: String,
|
||||
var superChapterId: Int,
|
||||
var superChapterName: String,
|
||||
var tags: List<Tag>,
|
||||
var title: String,
|
||||
var type: Int,
|
||||
var userId: Int,
|
||||
var visible: Int,
|
||||
var zan: Int
|
||||
){
|
||||
|
||||
/**
|
||||
* 获取文章作者
|
||||
*/
|
||||
fun getArticleAuthor(): String = author.ifEmpty { shareUser }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 轮播图
|
||||
*/
|
||||
data class Banner(
|
||||
var desc: String = "",
|
||||
var id: Int = 0,
|
||||
var imagePath: String = "",
|
||||
var isVisible: Int = 0,
|
||||
var order: Int = 0,
|
||||
var title: String = "",
|
||||
var type: Int = 0,
|
||||
var url: String = ""
|
||||
)
|
||||
|
||||
data class Banners(
|
||||
val banners: List<Banner>
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is Banners) {
|
||||
if (this.banners.size == other.banners.size) {
|
||||
this.banners.forEachIndexed { index, banner ->
|
||||
if (banner != other.banners[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return super.equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = banners.hashCode()
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
|
||||
@Parcelize
|
||||
data class Classify(
|
||||
val author: String = "",
|
||||
val children: List<@RawValue Any> = emptyList(),
|
||||
val courseId: Int = 0,
|
||||
val cover: String = "",
|
||||
val desc: String = "",
|
||||
val id: Int = 0,
|
||||
val lisense: String = "",
|
||||
val lisenseLink: String = "",
|
||||
val name: String = "",
|
||||
val order: Int = 0,
|
||||
val parentChapterId: Int = 0,
|
||||
val userControlSetTop: Boolean = false,
|
||||
val visible: Int = 0
|
||||
) : Parcelable
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 获取过的积分记录
|
||||
*/
|
||||
data class CoinHistory(
|
||||
var coinCount: Int,
|
||||
var date: Long,
|
||||
var desc: String,
|
||||
var id: Int,
|
||||
var type: Int,
|
||||
var reason: String,
|
||||
var userId: Int,
|
||||
var userName: String
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 积分信息
|
||||
*/
|
||||
data class CoinInfo(
|
||||
val coinCount: Int = 0,
|
||||
val rank: String = "",
|
||||
val level: Int = 0,
|
||||
val userId: Int = 0,
|
||||
val nickname: String = "",
|
||||
val username: String = ""
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 收藏文章信息
|
||||
*/
|
||||
data class CollectArticleInfo(
|
||||
val count: Int = 0
|
||||
)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 收藏
|
||||
*/
|
||||
data class CollectBean(
|
||||
val author: String = "",
|
||||
val chapterId: Int = 0,
|
||||
val chapterName: String = "",
|
||||
val courseId: Int = 0,
|
||||
val desc: String = "",
|
||||
val envelopePic: String = "",
|
||||
val id: Int = 0,
|
||||
val link: String = "",
|
||||
val niceDate: String = "",
|
||||
val origin: String = "",
|
||||
val originId: Int = 0,
|
||||
val publishTime: Long = 0,
|
||||
val title: String = "",
|
||||
val userId: Int = 0,
|
||||
val visible: Int = 0,
|
||||
val zan: Int = 0,
|
||||
){
|
||||
/**
|
||||
* 是否收藏状态
|
||||
*/
|
||||
var collect: Boolean = true
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 收藏事件
|
||||
*/
|
||||
data class CollectEvent(
|
||||
val id: Int,
|
||||
val link: String,
|
||||
val isCollected: Boolean
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 热搜词
|
||||
*/
|
||||
data class HotKeyBean(
|
||||
val id: Int,
|
||||
val link: String,
|
||||
val name: String,
|
||||
val order: Int,
|
||||
val visible: Int
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
data class MsgBean(
|
||||
val category: Int,
|
||||
val date: Long,
|
||||
val fromUser: String,
|
||||
val fromUserId: Int,
|
||||
val fullLink: String,
|
||||
val id: Int,
|
||||
val isRead: Int,
|
||||
val link: String,
|
||||
val message: String,
|
||||
val niceDate: String,
|
||||
val tag: String,
|
||||
val title: String,
|
||||
val userId: Int
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 导航
|
||||
*/
|
||||
data class Navigation(
|
||||
var articles: List<Article>,
|
||||
var cid: Int,
|
||||
var name: String
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
data class PageResponse<T>(
|
||||
val curPage: Int,
|
||||
val datas: List<T>,
|
||||
val offset: Int,
|
||||
val over: Boolean,
|
||||
val pageCount: Int,
|
||||
val size: Int,
|
||||
val total: Int
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 项目标题
|
||||
*/
|
||||
data class ProjectTitle(
|
||||
val author: String = "",
|
||||
val children: List<Any> = emptyList(),
|
||||
val courseId: Int = 0,
|
||||
val cover: String = "",
|
||||
val desc: String = "",
|
||||
val id: Int = 0,
|
||||
val lisense: String = "",
|
||||
val lisenseLink: String = "",
|
||||
val name: String = "",
|
||||
val order: Int = 0,
|
||||
val parentChapterId: Int = 0,
|
||||
val userControlSetTop: Boolean = false,
|
||||
val visible: Int = 0
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 体系
|
||||
*/
|
||||
data class Series(
|
||||
val author: String,
|
||||
val children: List<Classify>,
|
||||
val courseId: Int,
|
||||
val cover: String,
|
||||
val desc: String,
|
||||
val id: Int,
|
||||
val lisense: String,
|
||||
val lisenseLink: String,
|
||||
val name: String,
|
||||
val order: Int,
|
||||
val parentChapterId: Int,
|
||||
val userControlSetTop: Boolean,
|
||||
val visible: Int
|
||||
)
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 分享数据
|
||||
*/
|
||||
data class ShareBean(
|
||||
val coinInfo: CoinInfo,
|
||||
val shareArticles: PageResponse<Article>
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* [Article] Tag
|
||||
*/
|
||||
data class Tag(
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* Tool工具
|
||||
*/
|
||||
data class ToolBean(
|
||||
val desc: String = "",
|
||||
val icon: String = "",
|
||||
val id: Int = 0,
|
||||
val isNew: Int = 0,
|
||||
val link: String = "",
|
||||
val name: String = "",
|
||||
val order: Int = 0,
|
||||
val showInTab: Int = 0,
|
||||
val tabName: String = "",
|
||||
val visible: Int = 1
|
||||
) {
|
||||
fun getIconUrl() = "https://www.wanandroid.com/resources/image/pc/tools/$icon"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
@Parcelize
|
||||
data class User(
|
||||
val admin: Boolean = false,
|
||||
val chapterTops: List<String> = emptyList(),
|
||||
val collectIds: MutableList<String> = mutableListOf(),
|
||||
val email: String = "",
|
||||
val icon: String = "",
|
||||
val id: String = "",
|
||||
val nickname: String = "",
|
||||
val password: String = "",
|
||||
val token: String = "",
|
||||
val type: Int = 0,
|
||||
val username: String = ""
|
||||
) : Parcelable
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.lowe.common.services.model
|
||||
|
||||
/**
|
||||
* 个人基本信息
|
||||
*/
|
||||
data class UserBaseInfo(
|
||||
val coinInfo: CoinInfo = CoinInfo(),
|
||||
val collectArticleInfo: CollectArticleInfo = CollectArticleInfo(),
|
||||
val userInfo: User = User()
|
||||
)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.lowe.common.services.repository
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.BaseService
|
||||
import com.lowe.common.services.CollectService
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 收藏Repository
|
||||
*/
|
||||
class CollectRepository @Inject constructor(private val service: CollectService) {
|
||||
|
||||
fun getCollectFlow() = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO,
|
||||
service = service
|
||||
) { profileService, page, _ ->
|
||||
profileService.getCollectList(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
suspend fun articleCollectAction(event: CollectEvent) =
|
||||
service.isCollectArticle(event.isCollected, event.id)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.lowe.common.services.repository
|
||||
|
||||
import com.lowe.common.services.ProfileService
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageRepository @Inject constructor(private val profileService: ProfileService) {
|
||||
|
||||
suspend fun getUnreadMessageCount() = profileService.getUnreadMessageCount()
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.lowe.common.services.usecase
|
||||
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.services.repository.CollectRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 收藏操作UseCase
|
||||
*/
|
||||
class ArticleCollectUseCase @Inject constructor(private val collectRepository: CollectRepository) {
|
||||
|
||||
suspend fun articleCollectAction(event: CollectEvent) =
|
||||
collectRepository.articleCollectAction(event)
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.lowe.common.services.usecase
|
||||
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.result.Result
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
abstract class UseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
|
||||
|
||||
suspend operator fun invoke(parameters: P): Result<R> {
|
||||
return try {
|
||||
withContext(coroutineDispatcher) {
|
||||
execute(parameters).let {
|
||||
Result.Success(it)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RuntimeException::class)
|
||||
protected abstract suspend fun execute(parameters: P): R
|
||||
}
|
||||
|
||||
abstract class FlowUseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
|
||||
operator fun invoke(parameters: P): Flow<Result<R>> = execute(parameters)
|
||||
.catch { e -> emit(Result.Error(Exception(e))) }
|
||||
.flowOn(coroutineDispatcher)
|
||||
|
||||
protected abstract fun execute(parameters: P): Flow<Result<R>>
|
||||
}
|
||||
|
||||
|
||||
abstract class NetWorkUseCase<in P, R : Any> {
|
||||
|
||||
suspend operator fun invoke(parameters: P): NetworkResponse<R> {
|
||||
return try {
|
||||
execute(parameters)
|
||||
} catch (e: Exception) {
|
||||
NetworkResponse.UnknownError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RuntimeException::class)
|
||||
protected abstract suspend fun execute(parameters: P): NetworkResponse<R>
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.lowe.common.services.usecase
|
||||
|
||||
import com.lowe.common.services.repository.MessageRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class UnreadMessageCountUseCase @Inject constructor(
|
||||
private val messageRepository: MessageRepository,
|
||||
) : NetWorkUseCase<Unit, Int>() {
|
||||
|
||||
override suspend fun execute(parameters: Unit) = messageRepository.getUnreadMessageCount()
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.lowe.common.services.usecase.setting
|
||||
|
||||
import com.lowe.common.base.datastore.PreferenceStorage
|
||||
import com.lowe.common.di.IoDispatcher
|
||||
import com.lowe.common.services.usecase.UseCase
|
||||
import com.lowe.resource.theme.ThemePrimaryKey
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import javax.inject.Inject
|
||||
|
||||
class ApplyThemeUseCase @Inject constructor(
|
||||
private val preferenceStorage: PreferenceStorage,
|
||||
@IoDispatcher dispatcher: CoroutineDispatcher
|
||||
) : UseCase<ThemePrimaryKey, Unit>(dispatcher) {
|
||||
override suspend fun execute(parameters: ThemePrimaryKey) {
|
||||
preferenceStorage.applyTheme(parameters.storageKey)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.common.services.usecase.setting
|
||||
|
||||
import com.lowe.common.base.datastore.PreferenceStorage
|
||||
import com.lowe.common.di.DefaultDispatcher
|
||||
import com.lowe.common.result.Result
|
||||
import com.lowe.common.services.usecase.FlowUseCase
|
||||
import com.lowe.resource.theme.ThemePrimaryKey
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class ThemeChangeFlowUseCase @Inject constructor(
|
||||
private val preferenceStorage: PreferenceStorage,
|
||||
@DefaultDispatcher dispatcher: CoroutineDispatcher
|
||||
) : FlowUseCase<Unit, ThemePrimaryKey>(dispatcher) {
|
||||
override fun execute(parameters: Unit): Flow<Result<ThemePrimaryKey>> {
|
||||
return preferenceStorage.appliedTheme.map {
|
||||
Result.Success(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.lowe.common.theme
|
||||
|
||||
import com.lowe.common.di.ApplicationScope
|
||||
import com.lowe.common.result.successOr
|
||||
import com.lowe.common.services.usecase.setting.ApplyThemeUseCase
|
||||
import com.lowe.common.services.usecase.setting.ThemeChangeFlowUseCase
|
||||
import com.lowe.resource.theme.ThemeHelper
|
||||
import com.lowe.resource.theme.ThemePrimaryKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ThemeViewModelDelegate {
|
||||
|
||||
val themeState: StateFlow<ThemePrimaryKey>
|
||||
|
||||
val currentTheme: ThemePrimaryKey
|
||||
|
||||
suspend fun applyTheme(key: ThemePrimaryKey)
|
||||
|
||||
}
|
||||
|
||||
class ThemeActivityDelegateImpl @Inject constructor(
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
themeChangeFlowUseCase: ThemeChangeFlowUseCase,
|
||||
private val applyThemeUseCase: ApplyThemeUseCase
|
||||
) : ThemeViewModelDelegate {
|
||||
|
||||
override val themeState: StateFlow<ThemePrimaryKey> = themeChangeFlowUseCase(Unit).map {
|
||||
it.successOr(ThemeHelper.defaultThemeKey)
|
||||
}.stateIn(applicationScope, SharingStarted.Eagerly, ThemeHelper.defaultThemeKey)
|
||||
|
||||
override val currentTheme: ThemePrimaryKey
|
||||
get() = themeState.value
|
||||
|
||||
override suspend fun applyTheme(key: ThemePrimaryKey) {
|
||||
applyThemeUseCase(key)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.lowe.common.theme
|
||||
|
||||
import com.lowe.common.di.ApplicationScope
|
||||
import com.lowe.common.services.usecase.setting.ApplyThemeUseCase
|
||||
import com.lowe.common.services.usecase.setting.ThemeChangeFlowUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class ThemeViewModelDelegateModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAccountViewModelDelegate(
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
themeChangeFlowUseCase: ThemeChangeFlowUseCase,
|
||||
applyThemeUseCase: ApplyThemeUseCase,
|
||||
): ThemeViewModelDelegate =
|
||||
ThemeActivityDelegateImpl(
|
||||
applicationScope,
|
||||
themeChangeFlowUseCase,
|
||||
applyThemeUseCase
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.lowe.common.utils
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.bundleOf
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
private const val PACKAGE_NAME = "com.lowe.wanandroid"
|
||||
|
||||
interface AddressableActivity {
|
||||
|
||||
val className: String
|
||||
|
||||
val bundle: Bundle
|
||||
}
|
||||
|
||||
fun intentTo(addressable: AddressableActivity) =
|
||||
Intent().setComponent(ComponentName(PACKAGE_NAME, addressable.className))
|
||||
.putExtras(addressable.bundle)
|
||||
|
||||
object Activities {
|
||||
|
||||
object Setting : AddressableActivity {
|
||||
override val className: String
|
||||
get() = "$PACKAGE_NAME.ui.setting.SettingActivity"
|
||||
override val bundle: Bundle
|
||||
get() = bundleOf()
|
||||
}
|
||||
|
||||
object Login : AddressableActivity {
|
||||
|
||||
override val className: String
|
||||
get() = "$PACKAGE_NAME.ui.login.LoginActivity"
|
||||
|
||||
override val bundle: Bundle
|
||||
get() = bundleOf()
|
||||
}
|
||||
|
||||
class ShareList(
|
||||
override val className: String = "$PACKAGE_NAME.ui.share.ShareListActivity",
|
||||
override val bundle: Bundle
|
||||
) : AddressableActivity {
|
||||
|
||||
companion object {
|
||||
const val KEY_SHARE_LIST_USER_ID = "key_share_list_user_id"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Web(
|
||||
override val className: String = "$PACKAGE_NAME.ui.web.WebActivity",
|
||||
override val bundle: Bundle
|
||||
) : AddressableActivity {
|
||||
|
||||
@Parcelize
|
||||
data class WebIntent(
|
||||
val url: String,
|
||||
val id: Int = 0,
|
||||
var isCollected: Boolean = false,
|
||||
) : Parcelable {
|
||||
fun isNeedShowCollectIcon() = id != 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_WEB_VIEW_Intent_bundle = "key_web_view_intent_bundle"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user