a
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk compile_sdk_version
|
||||
|
||||
defaultConfig {
|
||||
minSdk min_sdk_version
|
||||
targetSdk target_sdk_version
|
||||
versionCode version_code
|
||||
versionName version_name
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility java_version
|
||||
targetCompatibility java_version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.core:core-ktx:$core_ktx_version"
|
||||
implementation "androidx.appcompat:appcompat:$appcompat_version"
|
||||
|
||||
//retrofit
|
||||
api "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
api "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
|
||||
//okHttp
|
||||
api "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
|
||||
//paging3
|
||||
api "androidx.paging:paging-runtime:$paging_version"
|
||||
api "androidx.paging:paging-compose:$paging_compose_version"
|
||||
|
||||
//数据保存,这里用于cookie持久化
|
||||
api "androidx.datastore:datastore-preferences:$datastore_version"
|
||||
}
|
||||
Vendored
+21
@@ -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,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.linx.net">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.linx.net.base
|
||||
|
||||
object NetUrl {
|
||||
|
||||
const val BASE_URL = "https://www.wanandroid.com"
|
||||
|
||||
//首页轮播图
|
||||
const val HOME_BANNER = "/banner/json"
|
||||
|
||||
//首页列表
|
||||
const val ARTICLE_LIST = "/article/list/{page}/json"
|
||||
|
||||
//置顶文章
|
||||
const val ARTICLE_TOP = "/article/top/json"
|
||||
|
||||
//项目分类(指示器)
|
||||
const val PROJECT_TREE = "/project/tree/json"
|
||||
|
||||
//项目列表数据
|
||||
const val PROJECT_LIST = "/project/list/{page}/json"
|
||||
|
||||
//广场
|
||||
const val USER_ARTICLE_LIST = "/user_article/list/{page}/json"
|
||||
|
||||
//问答
|
||||
const val WEN_DA = "/wenda/list/{page}/json"
|
||||
|
||||
//体系
|
||||
const val SYSTEM = "/tree/json"
|
||||
|
||||
//导航数据
|
||||
const val NAVI = "/navi/json"
|
||||
|
||||
//公众号列表
|
||||
const val WXARTICLE_CHAPTERS = "/wxarticle/chapters/json"
|
||||
|
||||
//某个公众号历史数据列表
|
||||
const val WXARTICLE_LIST = "/wxarticle/list/{id}/{page}/json"
|
||||
|
||||
//个人信息
|
||||
const val LG_COIN_USERINFO = "/lg/coin/userinfo/json"
|
||||
|
||||
//登录
|
||||
const val USER_LOGIN = "/user/login"
|
||||
|
||||
//注册
|
||||
const val USER_REGISTER = "/user/register"
|
||||
|
||||
//积分排行
|
||||
const val COIN_RANK = "/coin/rank/{page}/json"
|
||||
|
||||
//我的收藏
|
||||
const val MY_COLLECT = "/lg/collect/list/{page}/json"
|
||||
|
||||
//我分享的文章
|
||||
const val MY_SHARE_ARTICLES = "/user/lg/private_articles/{page}/json"
|
||||
|
||||
//搜索热词
|
||||
const val HOTKEY = "/hotkey/json"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.linx.net.base
|
||||
|
||||
import com.linx.net.widget.DataStoreUtils
|
||||
import com.linx.net.widget.KtHttpLogInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* retrofit2请求
|
||||
*/
|
||||
object ServiceCreator {
|
||||
|
||||
private const val SAVE_USER_LOGIN_KEY = "user/login"
|
||||
private const val SAVE_USER_REGISTER_KEY = "user/register"
|
||||
private const val SET_COOKIE_KEY = "set-cookie"
|
||||
private const val COOKIE_NAME = "Cookie"
|
||||
|
||||
private val mOkClient: OkHttpClient = OkHttpClient.Builder()
|
||||
//完整请求超时时长,从发起到接受返回数据,默认0s
|
||||
.callTimeout(10, TimeUnit.SECONDS)
|
||||
//与服务器建立链接的时长,默认10s
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
//读取服务器返回数据的时长
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
//向服务器写入数据的时长,默认10s
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
//是否重连
|
||||
.retryOnConnectionFailure(true)
|
||||
//是否重定向
|
||||
.followRedirects(false)
|
||||
//保存cookie
|
||||
.addInterceptor {
|
||||
val request = it.request()
|
||||
val response = it.proceed(request)
|
||||
val requestUrl = request.url.toString()
|
||||
val domain = request.url.host
|
||||
//cookie可能有多个,都保存下来
|
||||
if ((requestUrl.contains(SAVE_USER_LOGIN_KEY) || requestUrl.contains(
|
||||
SAVE_USER_REGISTER_KEY
|
||||
)) && response.headers(SET_COOKIE_KEY).isNotEmpty()
|
||||
) {
|
||||
val cookies = response.headers(SET_COOKIE_KEY)
|
||||
val cookie = encodeCookie(cookies)
|
||||
saveCookie(requestUrl, domain, cookie)
|
||||
}
|
||||
response
|
||||
}
|
||||
//请求时设置cookie
|
||||
.addInterceptor {
|
||||
val request = it.request()
|
||||
val builder = request.newBuilder()
|
||||
val domain = request.url.host
|
||||
//获取domain内的cookie
|
||||
if (domain.isNotEmpty()) {
|
||||
val sqDomain: String = DataStoreUtils.readStringData(domain, "")
|
||||
val cookie: String = if (sqDomain.isNotEmpty()) sqDomain else ""
|
||||
if (cookie.isNotEmpty()) {
|
||||
builder.addHeader(COOKIE_NAME, cookie)
|
||||
}
|
||||
}
|
||||
it.proceed(builder.build())
|
||||
}
|
||||
//添加网络拦截器,打印日志
|
||||
.addNetworkInterceptor(KtHttpLogInterceptor {
|
||||
logLevel(KtHttpLogInterceptor.LogLevel.BODY)
|
||||
})
|
||||
.build()
|
||||
|
||||
/**
|
||||
* 保存cookie
|
||||
*/
|
||||
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
|
||||
private fun saveCookie(url: String?, domain: String?, cookies: String) {
|
||||
url ?: return
|
||||
DataStoreUtils.putSyncData(url, cookies)
|
||||
domain ?: return
|
||||
DataStoreUtils.putSyncData(domain, cookies)
|
||||
}
|
||||
|
||||
private var retrofit: Retrofit? = null
|
||||
|
||||
private val retrofitBuilder = Retrofit.Builder()
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(mOkClient)
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
*/
|
||||
fun initConfig(baseUrl: String, okClient: OkHttpClient = mOkClient): ServiceCreator {
|
||||
retrofit = retrofitBuilder.baseUrl(baseUrl).client(mOkClient).build()
|
||||
return this
|
||||
}
|
||||
|
||||
//inline内联修饰符可以使用reified来修饰函数的泛型,让泛型具体化
|
||||
inline fun <reified T> getService(): T = create(T::class.java)
|
||||
|
||||
/**
|
||||
* 获取retrofit的service对象
|
||||
*/
|
||||
fun <T> create(serviceClazz: Class<T>): T {
|
||||
if (retrofit == null) {
|
||||
throw UninitializedPropertyAccessException("Retrofit必须初始化,需要配置baseUrl")
|
||||
} else {
|
||||
return retrofit!!.create(serviceClazz)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理cookie
|
||||
*/
|
||||
fun encodeCookie(cookies: List<String>): String {
|
||||
val sb = StringBuilder()
|
||||
val set = HashSet<String>()
|
||||
cookies
|
||||
.map { cookie ->
|
||||
cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
}
|
||||
.forEach { it ->
|
||||
it.filterNot { set.contains(it) }.forEach { set.add(it) }
|
||||
}
|
||||
|
||||
val ite = set.iterator()
|
||||
while (ite.hasNext()) {
|
||||
val cookie = ite.next()
|
||||
sb.append(cookie).append(";")
|
||||
}
|
||||
|
||||
val last = sb.lastIndexOf(";")
|
||||
if (sb.length - 1 == last) {
|
||||
sb.deleteCharAt(last)
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.linx.net.cookie
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
/**
|
||||
* 用于持久化的cookieJar实现类
|
||||
*/
|
||||
internal class LocalCookieJar : CookieJar {
|
||||
|
||||
//cookie的本地化存储
|
||||
private val cache = mutableListOf<Cookie>()
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
//过期的Cookie
|
||||
val invalidCookies: MutableList<Cookie> = ArrayList()
|
||||
//有效的Cookie
|
||||
val validCookies: MutableList<Cookie> = ArrayList()
|
||||
|
||||
for (cookie in cache) {
|
||||
if (cookie.expiresAt < System.currentTimeMillis()) {
|
||||
//判断是否过期
|
||||
invalidCookies.add(cookie)
|
||||
} else if (cookie.matches(url)) {
|
||||
//匹配Cookie对应url
|
||||
validCookies.add(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
//缓存中移除过期的Cookie
|
||||
cache.removeAll(invalidCookies)
|
||||
|
||||
//返回List<Cookie>让Request进行设置
|
||||
return validCookies
|
||||
}
|
||||
|
||||
/**
|
||||
* 将cookie保存
|
||||
*/
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
cache.addAll(cookies)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.linx.net.ext
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.linx.net.model.BaseResponse
|
||||
import com.linx.net.model.DataResult
|
||||
import com.linx.net.model.succeeded
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* 这里表示网络请求成功,并得到业务服务器的响应
|
||||
* 将BaseCniaoRsp的对象,转化为需要的对象类型,也就是将body.string转为entity
|
||||
* @return 返回需要类型对象,可能为null,如果json解析失败的话
|
||||
* 加了reified才可以T::class.java
|
||||
*/
|
||||
inline fun <reified T> BaseResponse.toEntity(): T? {
|
||||
|
||||
if (data == null) {
|
||||
Log.e("DataResultExt", "server Response Json Ok,But data==null,$errorCode,$errorMsg")
|
||||
return null
|
||||
}
|
||||
|
||||
//todo 如果有需要的话在这里对返回的data数据进行解密然后再tojson fromjson
|
||||
|
||||
// val decodeData = CnUtils.decodeData(data.toString())
|
||||
//gson不允许我们将json对象采用String,所以单独处理
|
||||
// if (T::class.java.isAssignableFrom(String::class.java)) {
|
||||
// return decodeData as T
|
||||
// }
|
||||
|
||||
//如果data不为空,先进行tojson处理再转化为T对象类型的entity string
|
||||
//传入LoginRsp就是LoginRsp 传入RegisterRsp就是RegisterRsp
|
||||
return kotlin.runCatching {
|
||||
//Gson不能直接解析泛型 如果遇到List<*>会报错 这里需要借助TypeToken类传递泛型
|
||||
val type = object : TypeToken<T>() {}.type
|
||||
return Gson().fromJson(Gson().toJson(data), type)
|
||||
}.onFailure { e ->
|
||||
e.printStackTrace() //Catch出错,报错
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口成功,但是业务返回code不是0的情况
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun BaseResponse.onBizError(crossinline block: (code: Int, message: String?) -> Unit): BaseResponse {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
//同时不等于0的时候执行if
|
||||
if (errorCode != BaseResponse.SERVER_CODE_SUCCESS) {
|
||||
//返回错误码和错误信息
|
||||
block.invoke(errorCode, errorMsg ?: "Error Message Null")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 接口成功且业务成功code==0的情况
|
||||
* crossinline关键字 只要标志了就不能进入return true函数快
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <reified T> BaseResponse.onBizOK(crossinline action: (code: Int, data: T?, message: String?) -> Unit): BaseResponse {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
//code == 0,成功
|
||||
if (errorCode == BaseResponse.SERVER_CODE_SUCCESS) {
|
||||
//返回成功码和解密之后的序列化对象
|
||||
action.invoke(errorCode, this.toEntity<T>(), errorMsg)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* 扩展用于处理网络返回数据结果 网络接口请求成功,但是业务成功与否不一定
|
||||
* 注解 使用的是一个实验性的特性 需要在.gradel编译器中添加参数注明
|
||||
* */
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <R> DataResult<R>.onSuccess(action: R.() -> Unit): DataResult<R> {
|
||||
//契约关系
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.AT_MOST_ONCE) //最多走一次
|
||||
}
|
||||
|
||||
if (succeeded) action.invoke((this as DataResult.Success).data)
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* 扩展用于处理网络返回数据结果 网络请求出现错误的时候的回调
|
||||
* */
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <R> DataResult<R>.onFailure(action: (exception: Throwable) -> Unit): DataResult<R> {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
|
||||
}
|
||||
if (this is DataResult.Error) action.invoke(exception)
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.linx.net.ext
|
||||
|
||||
import android.util.Log
|
||||
import com.linx.net.model.DataResult
|
||||
import retrofit2.Call
|
||||
import retrofit2.await
|
||||
|
||||
/**
|
||||
* 扩展retrofit的返回数据,调用await,并catch超时等异常
|
||||
* @receiver Call<T>
|
||||
* @return DataResult<T> 返回格式为ApiResponse封装
|
||||
*/
|
||||
suspend fun <T : Any> Call<T>.serverData(): DataResult<T> {
|
||||
var result: DataResult<T> = DataResult.Loading
|
||||
kotlin.runCatching {
|
||||
this.await()
|
||||
}.onFailure {
|
||||
result = DataResult.Error(RuntimeException(it))
|
||||
}.onSuccess {
|
||||
result = DataResult.Success(it)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.linx.net.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* 服务器返回的数据基类
|
||||
*/
|
||||
@Keep
|
||||
data class BaseResponse(
|
||||
|
||||
//接口数据
|
||||
val data: Any?,
|
||||
|
||||
//接口状态码
|
||||
val errorCode: Int,
|
||||
|
||||
//接口出错消息
|
||||
val errorMsg: String = ""
|
||||
) {
|
||||
companion object {
|
||||
const val SERVER_CODE_SUCCESS = 0 //接口请求响应业务处理 成功
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.linx.net.model
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* 数据请求返回格式
|
||||
*/
|
||||
sealed class DataResult <out R> {
|
||||
|
||||
//成功
|
||||
data class Success<out T>(val data: T): DataResult<T>()
|
||||
|
||||
//请求失败
|
||||
data class Error(val exception: Exception): DataResult<Nothing>()
|
||||
|
||||
//加载数据中
|
||||
object Loading: DataResult<Nothing>()
|
||||
|
||||
override fun toString(): String {
|
||||
return when(this) {
|
||||
is Success<*> -> "Success[data=$data]"
|
||||
is Error -> "Error[exception=$exception]"
|
||||
is Loading -> "Loading"
|
||||
else -> "Error"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回结果如果是Success类,并且data非null才认为是成功的
|
||||
*/
|
||||
val DataResult<*>.succeeded: Boolean
|
||||
get() = this is DataResult.Success && data != null
|
||||
|
||||
/**
|
||||
* 获取资源的状态
|
||||
*/
|
||||
enum class Status{
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
LOADING
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据封装
|
||||
*/
|
||||
data class ReSource<out T>(val status: Status, val data: T?, val message: String?) {
|
||||
|
||||
companion object {
|
||||
fun <T> success(data: T?): ReSource<T> = ReSource(Status.SUCCESS, data, "Resource Success")
|
||||
fun <T> error(msg: String, data: T?): ReSource<T> = ReSource(Status.ERROR, data, msg)
|
||||
fun <T> loading(data: T?): ReSource<T> = ReSource(Status.LOADING, data, null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.linx.net.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* 公共的Paging数据源
|
||||
*/
|
||||
class CommonPagingSource <T: Any> (private val block: suspend (nextPage: Int) -> CommonalityPageModel<T>): PagingSource<Int, T>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, T>): Int? = null
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
|
||||
return try {
|
||||
//params.key为当前页码 页码从0开始
|
||||
val nextPage = params.key ?: 0
|
||||
//更新页码后请求数据
|
||||
val response = block.invoke(nextPage)
|
||||
LoadResult.Page(
|
||||
data = response.data.datas,
|
||||
//前一页页码
|
||||
prevKey = if (nextPage == 0) null else nextPage - 1,
|
||||
//后一页页码
|
||||
nextKey = if (nextPage < response.data.pageCount) nextPage + 1 else null
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
LoadResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.linx.net.paging
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
/**
|
||||
* page公共的Model
|
||||
*/
|
||||
@Keep
|
||||
data class CommonalityPageModel<T>(
|
||||
val `data`: Data<T>,
|
||||
) {
|
||||
@Keep
|
||||
data class Data<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,276 @@
|
||||
package com.linx.net.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.*
|
||||
import androidx.datastore.preferences.createDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* 版权:Zhujiang 个人版权
|
||||
*
|
||||
* @author zhujiang
|
||||
* 创建日期:12/3/20
|
||||
*
|
||||
* 异步获取数据
|
||||
* [getData] [readBooleanFlow] [readFloatFlow] [readIntFlow] [readLongFlow] [readStringFlow]
|
||||
* 同步获取数据
|
||||
* [getSyncData] [readBooleanData] [readFloatData] [readIntData] [readLongData] [readStringData]
|
||||
*
|
||||
* 异步写入数据
|
||||
* [putData] [saveBooleanData] [saveFloatData] [saveIntData] [saveLongData] [saveStringData]
|
||||
* 同步写入数据
|
||||
* [putSyncData] [saveSyncBooleanData] [saveSyncFloatData] [saveSyncIntData] [saveSyncLongData] [saveSyncStringData]
|
||||
*
|
||||
* 异步清除数据
|
||||
* [clear]
|
||||
* 同步清除数据
|
||||
* [clearSync]
|
||||
*
|
||||
* 描述:DataStore 工具类
|
||||
*
|
||||
*/
|
||||
object DataStoreUtils {
|
||||
|
||||
private lateinit var dataStore: DataStore<Preferences>
|
||||
private const val preferenceName = "PlayAndroidDataStore"
|
||||
|
||||
/**
|
||||
* init Context
|
||||
* @param context Context
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
dataStore = context.createDataStore(preferenceName)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <U> getSyncData(key: String, default: U): U {
|
||||
val res = when (default) {
|
||||
is Long -> readLongData(key, default)
|
||||
is String -> readStringData(key, default)
|
||||
is Int -> readIntData(key, default)
|
||||
is Boolean -> readBooleanData(key, default)
|
||||
is Float -> readFloatData(key, default)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
return res as U
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <U> getData(key: String, default: U): Flow<U> {
|
||||
val data = when (default) {
|
||||
is Long -> readLongFlow(key, default)
|
||||
is String -> readStringFlow(key, default)
|
||||
is Int -> readIntFlow(key, default)
|
||||
is Boolean -> readBooleanFlow(key, default)
|
||||
is Float -> readFloatFlow(key, default)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
return data as Flow<U>
|
||||
}
|
||||
|
||||
suspend fun <U> putData(key: String, value: U) {
|
||||
when (value) {
|
||||
is Long -> saveLongData(key, value)
|
||||
is String -> saveStringData(key, value)
|
||||
is Int -> saveIntData(key, value)
|
||||
is Boolean -> saveBooleanData(key, value)
|
||||
is Float -> saveFloatData(key, value)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
}
|
||||
|
||||
fun <U> putSyncData(key: String, value: U) {
|
||||
when (value) {
|
||||
is Long -> saveSyncLongData(key, value)
|
||||
is String -> saveSyncStringData(key, value)
|
||||
is Int -> saveSyncIntData(key, value)
|
||||
is Boolean -> saveSyncBooleanData(key, value)
|
||||
is Float -> saveSyncFloatData(key, value)
|
||||
else -> throw IllegalArgumentException("This type can be saved into DataStore")
|
||||
}
|
||||
}
|
||||
|
||||
fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
//当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
|
||||
//但是如果是其他的异常,最好将它抛出去,不要隐藏问题
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[preferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readBooleanData(key: String, default: Boolean = false): Boolean {
|
||||
var value = false
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[preferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[preferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readIntData(key: String, default: Int = 0): Int {
|
||||
var value = 0
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[preferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readStringFlow(key: String, default: String = ""): Flow<String> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[preferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readStringData(key: String, default: String = ""): String {
|
||||
var value = ""
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[preferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[preferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readFloatData(key: String, default: Float = 0f): Float {
|
||||
var value = 0f
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[preferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
|
||||
dataStore.data
|
||||
.catch {
|
||||
if (it is IOException) {
|
||||
it.printStackTrace()
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}.map {
|
||||
it[preferencesKey(key)] ?: default
|
||||
}
|
||||
|
||||
fun readLongData(key: String, default: Long = 0L): Long {
|
||||
var value = 0L
|
||||
runBlocking {
|
||||
dataStore.data.first {
|
||||
value = it[preferencesKey(key)] ?: default
|
||||
true
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
suspend fun saveBooleanData(key: String, value: Boolean) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[preferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncBooleanData(key: String, value: Boolean) =
|
||||
runBlocking { saveBooleanData(key, value) }
|
||||
|
||||
suspend fun saveIntData(key: String, value: Int) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[preferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncIntData(key: String, value: Int) = runBlocking { saveIntData(key, value) }
|
||||
|
||||
suspend fun saveStringData(key: String, value: String) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[preferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncStringData(key: String, value: String) = runBlocking { saveStringData(key, value) }
|
||||
|
||||
suspend fun saveFloatData(key: String, value: Float) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[preferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncFloatData(key: String, value: Float) = runBlocking { saveFloatData(key, value) }
|
||||
|
||||
suspend fun saveLongData(key: String, value: Long) {
|
||||
dataStore.edit { mutablePreferences ->
|
||||
mutablePreferences[preferencesKey(key)] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSyncLongData(key: String, value: Long) = runBlocking { saveLongData(key, value) }
|
||||
|
||||
suspend fun clear() {
|
||||
dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSync() {
|
||||
runBlocking {
|
||||
dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.linx.net.widget
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.*
|
||||
import java.net.URLDecoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 日志拦截器
|
||||
*/
|
||||
class KtHttpLogInterceptor(block: (KtHttpLogInterceptor.() -> Unit)? = null) : Interceptor {
|
||||
|
||||
private var logLevel: LogLevel = LogLevel.NONE //打印日期的标记
|
||||
private var colorLevel: ColorLevel = ColorLevel.DEBUG //默认是debug级别的logcat
|
||||
private var logTag: String = TAG //日志的logcat的Tag
|
||||
private var MILLIS_PATTERN:String = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
init {
|
||||
block?.invoke(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置logLevel
|
||||
*/
|
||||
fun logLevel(level: LogLevel): KtHttpLogInterceptor {
|
||||
logLevel = level
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置colorLevel
|
||||
*/
|
||||
fun colorLevel(level: ColorLevel): KtHttpLogInterceptor {
|
||||
colorLevel = level
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Log的Tag
|
||||
*/
|
||||
fun logTag(tag: String): KtHttpLogInterceptor {
|
||||
logTag = tag
|
||||
return this
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
//请求
|
||||
val request: Request = chain.request()
|
||||
//响应
|
||||
return kotlin.runCatching { chain.proceed(request) }
|
||||
.onFailure {
|
||||
it.printStackTrace()
|
||||
logIt(
|
||||
it.message.toString(),
|
||||
ColorLevel.ERROR
|
||||
)
|
||||
}.onSuccess { response ->
|
||||
if (logLevel == LogLevel.NONE) {
|
||||
return response
|
||||
}
|
||||
//记录请求日志
|
||||
logRequest(request, chain.connection())
|
||||
//记录响应日志
|
||||
logResponse(response)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*/
|
||||
private fun logRequest(request: Request, connection: Connection?) {
|
||||
val strb = StringBuilder()
|
||||
strb.appendLine("\r\n")
|
||||
strb.appendLine(
|
||||
"->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->"
|
||||
)
|
||||
when (logLevel) {
|
||||
LogLevel.NONE -> {
|
||||
|
||||
}
|
||||
LogLevel.BASIC -> {
|
||||
logBasicReq(strb, request, connection)
|
||||
}
|
||||
LogLevel.HEADERS -> {
|
||||
logHeadersReq(strb, request, connection)
|
||||
}
|
||||
LogLevel.BODY -> {
|
||||
logBodyReq(strb, request, connection)
|
||||
}
|
||||
}
|
||||
strb.appendLine(
|
||||
"->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->->"
|
||||
)
|
||||
logIt(strb)
|
||||
}
|
||||
|
||||
private fun logBodyReq(strb: StringBuilder, request: Request, connection: Connection?) {
|
||||
logHeadersReq(strb, request, connection)
|
||||
strb.appendLine("RequestBody:${request.body.toString()}")
|
||||
}
|
||||
|
||||
private fun logHeadersReq(strb: StringBuilder, request: Request, connection: Connection?) {
|
||||
logBasicReq(strb, request, connection)
|
||||
val headerStr: String = request.headers.joinToString { header ->
|
||||
"请求 Header:{${header.first}=${header.second}}\n"
|
||||
}
|
||||
strb.appendln(headerStr)
|
||||
}
|
||||
|
||||
private fun logBasicReq(strb: StringBuilder, request: Request, connection: Connection?) {
|
||||
strb.appendLine(
|
||||
"请求 method:${request.method} url:${decodeUrlStr(request.url.toString())} tag:" +
|
||||
"${request.tag()} protocol:${connection?.protocol() ?: Protocol.HTTP_1_1}"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录响应日志
|
||||
*/
|
||||
private fun logResponse(response: Response) {
|
||||
val strb = StringBuffer()
|
||||
strb.appendLine("\r\n")
|
||||
strb.appendLine("<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-")
|
||||
when (logLevel) {
|
||||
LogLevel.NONE -> {
|
||||
/*do nothing*/
|
||||
}
|
||||
LogLevel.BASIC -> {
|
||||
logBasicRep(strb,response)
|
||||
}
|
||||
LogLevel.HEADERS -> {
|
||||
logHeadersRep(response, strb)
|
||||
}
|
||||
LogLevel.BODY -> {
|
||||
logHeadersRep(response, strb)
|
||||
kotlin.runCatching {
|
||||
//peek类似于clone数据流,监视,窥探,不能直接用原来的body的string流数据作为日志,会消费掉io,所有这里是peek,监测
|
||||
val peekBody: ResponseBody = response.peekBody(1024 * 1024)
|
||||
strb.appendln(peekBody.string())
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
strb.appendLine(
|
||||
"<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<" +
|
||||
"-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-<<-"
|
||||
)
|
||||
logIt(strb, ColorLevel.INFO)
|
||||
}
|
||||
|
||||
private fun logHeadersRep(response: Response, strb: StringBuffer) {
|
||||
logBasicRep(strb,response)
|
||||
val headerStr:String=response.headers.joinToString {
|
||||
header->"响应 Header:{${header.first}=${header.second}}\n"
|
||||
}
|
||||
strb.appendLine(headerStr)
|
||||
}
|
||||
|
||||
private fun logBasicRep(strb: StringBuffer, response: Response) {
|
||||
strb.appendLine("响应 protocol:${response.protocol} code:${response.cacheControl} message:${response.message}")
|
||||
.appendLine("响应 request Url: ${decodeUrlStr(response.request.url.toString())}")
|
||||
.appendLine("响应 sentRequestTime:${
|
||||
toDateTimeStr(
|
||||
response.sentRequestAtMillis,
|
||||
MILLIS_PATTERN
|
||||
)
|
||||
} receivedResponseTime:${
|
||||
toDateTimeStr(
|
||||
response.receivedResponseAtMillis,
|
||||
MILLIS_PATTERN
|
||||
)
|
||||
}")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对于url编码的string解码
|
||||
*/
|
||||
private fun decodeUrlStr(url: String): String? {
|
||||
return kotlin.runCatching {
|
||||
URLDecoder.decode(url, "utf-8")
|
||||
}.onFailure { it.printStackTrace() }.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印日志
|
||||
*/
|
||||
private fun logIt(any: Any, tempLevel: ColorLevel? = null) {
|
||||
when (tempLevel ?: colorLevel) {
|
||||
ColorLevel.VERBOSE -> Log.v(logTag, any.toString())
|
||||
ColorLevel.DEBUG -> Log.d(logTag, any.toString())
|
||||
ColorLevel.INFO -> Log.i(logTag, any.toString())
|
||||
ColorLevel.WARN -> Log.w(logTag, any.toString())
|
||||
ColorLevel.ERROR -> Log.e(logTag, any.toString())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "<KtHttp>" //默认的TAG
|
||||
|
||||
//时间格式化
|
||||
fun toDateTimeStr(millis: Long, pattern: String): String {
|
||||
return SimpleDateFormat(pattern, Locale.getDefault()).format(millis)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印日志范围
|
||||
*/
|
||||
enum class LogLevel {
|
||||
NONE,//不打印
|
||||
BASIC,//只打印行首,请求/响应
|
||||
HEADERS, //打印请求和响应的所有header
|
||||
BODY //打印所有
|
||||
}
|
||||
|
||||
/**
|
||||
* Log颜色等级,应用于android Logcat分为 v、d、i、w、e
|
||||
*/
|
||||
enum class ColorLevel {
|
||||
VERBOSE,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user