This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
+1
View File
@@ -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 {*;}
+21
View File
@@ -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
@@ -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>
@@ -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))
}
}
@@ -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
)
}
}
@@ -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
)
@@ -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
}
}
@@ -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)
}
}
}
@@ -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])
}
@@ -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
}
}
}
@@ -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
}
}
@@ -0,0 +1,11 @@
package com.lowe.common.base.app
import android.app.Application
interface ApplicationProxy {
fun onCreate(application: Application)
fun onTerminate()
}
@@ -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
}
@@ -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 }
}
@@ -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) }
)
}
}
@@ -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
}
}
}
@@ -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
}
}
@@ -0,0 +1,17 @@
package com.lowe.common.base.http.adapter
/**
* 用于配置全局的异常处理逻辑
*/
interface ErrorHandler {
/**
* 业务错误
*/
fun bizError(code: Int, msg: String)
/**
* 其他错误
*/
fun otherError(throwable: Throwable)
}
@@ -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())
}
}
@@ -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)
}
@@ -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)
}
}
@@ -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()
}
@@ -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)))
}
@@ -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")
}
}
@@ -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)
}
}
}
}
@@ -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()
}
}
}
}
@@ -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)
}
}
}
@@ -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()
}
@@ -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
}
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
@@ -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>>
}
@@ -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>>
}
@@ -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>>
}
@@ -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>
}
@@ -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>>
}
@@ -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()
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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()
}
@@ -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)
}
@@ -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()
}
@@ -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
@@ -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
)
@@ -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 = ""
)
@@ -0,0 +1,8 @@
package com.lowe.common.services.model
/**
* 收藏文章信息
*/
data class CollectArticleInfo(
val count: Int = 0
)
@@ -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
}
@@ -0,0 +1,10 @@
package com.lowe.common.services.model
/**
* 收藏事件
*/
data class CollectEvent(
val id: Int,
val link: String,
val isCollected: Boolean
)
@@ -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
)
@@ -0,0 +1,10 @@
package com.lowe.common.services.model
/**
* 导航
*/
data class Navigation(
var articles: List<Article>,
var cid: Int,
var name: String
)
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
@@ -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()
)
@@ -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)
}
@@ -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()
}
@@ -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)
}
@@ -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>
}
@@ -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()
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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