Browse Source

+ 缓存模式
| Interval 重置bug

drake 5 years ago
parent
commit
18772620ef

+ 86 - 11
README.md

@@ -9,7 +9,7 @@
 
 
 1.0+ 版本为RxKotlin实现
-2.0+ 版本开始引入Kotlin协程特性, 开发者无需掌握协程也可以使用, 两个版本存在Api冲突需要手动迁移
+2.0+ 版本开始引入Kotlin协程特性, 开发者无需掌握协程也可以使用, 两个版本存在Api变动需要手动迁移
 
 
 
@@ -20,6 +20,7 @@
 - 串行网络请求
 - 切换线程
 - DSL编程
+- 方便的缓存处理
 - 自动错误信息吐司
 - 自动异常捕获
 - 自动日志打印异常
@@ -36,6 +37,7 @@
 同时完全不影响[Kalle](https://github.com/yanzhenjie/Kalle)的特性
 
 - 九种缓存模式
+- 数据库缓存
 - 缓存加密
 - 上传进度监听
 - 下载进度监听
@@ -77,7 +79,7 @@ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
 // 支持自动下拉刷新和缺省页的, 可选
 implementation 'com.github.liangjingkanji:BRV:1.1.7'
 
-implementation 'com.github.liangjingkanji:Net:2.0.2'
+implementation 'com.github.liangjingkanji:Net:2.0.3'
 ```
 
 
@@ -90,6 +92,12 @@ implementation 'com.github.liangjingkanji:Net:2.0.2'
 
 ![image-20191223150901891](https://tva1.sinaimg.cn/large/006tNbRwgy1ga6o9s47lsj30dg0ca0tz.jpg)
 
+
+
+请求方式全部属于协程中的`async`异步函数, 运行在IO线程中. 函数返回`Deferred<T>`对象, 该对象通过`await()`函数获取结果. 
+
+执行await时会阻塞代码执行注意, 所以建议await在作用域最后一起执行, 保证请求全部发送出去然后统一获取结果.
+
 ### Post
 
 ```kotlin
@@ -136,7 +144,7 @@ scopeLife {
     absolutePath = true
   ){
     file("file", File())
-  }
+  }.await()
 
   textView.text = data.await()
 }
@@ -157,7 +165,7 @@ scopeLife {
 
                }
 
-  }
+  }.await()
 }
 ```
 
@@ -181,15 +189,11 @@ scopeLife {
   val data = downImage(
     "https://cdn.sspai.com/article/ebe361e4-c891-3afd-8680-e4bad609723e.jpg?imageMogr2/quality/95/thumbnail/!2880x620r/gravity/Center/crop/2880x620/interlace/1".
     200,200
-  )
+  ).await()
 
 }
 ```
 
-
-
-
-
 ## 初始化
 
 ```kotlin
@@ -440,7 +444,9 @@ StateLayout使用`scope`函数开启作用域
 - fragment
 - view
 
-### 自动下拉刷新|上拉加载|分页|缺省页
+### 自动下拉刷新
+
+兼具功能 下拉刷新|上拉加载|分页|缺省页
 
 需要引入第三方库: [BRV](https://github.com/liangjingkanji/BRV)
 
@@ -496,6 +502,8 @@ pageRefreshLayout.scopeRefresh{
 
 
 
+Tip: PageRefreshLayout只要加载成功后即使后续请求失败也不会显示错误缺省页
+
 ### 错误处理
 
 所有作用域都支持`catch|finally`函数回调
@@ -570,6 +578,72 @@ abstract class DefaultConverter(
 
 
 
+## 缓存和网络请求
+
+很多App要求秒启动展示首页数据, 然后断网以后也可以展示缓存数据, 这种需求需要做到刷新UI数据两遍, 本框架同样方便实现
+
+
+
+首先在初始化的时候启用缓存功能
+
+```kotlin
+initNet("http://localhost.com") {
+	cacheEnabled()
+}
+```
+
+
+
+可配置参数
+
+```kotlin
+fun KalleConfig.Builder.openCache(
+    path: String = NetConfig.app.cacheDir.absolutePath, // 缓存保存位置, 默认应用缓存目录
+    password: String = "cache" // 缓存密码, 默认cache
+) 
+```
+
+
+
+发起缓存请求
+
+```kotlin
+scopeNetLife {
+
+  Log.d("日志", "网络请求")
+
+  val data = get<String>(
+    "https://raw.githubusercontent.com/liangjingkanji/BRV/master/README.md",
+    CacheMode.NETWORK_YES_THEN_WRITE_CACHE,
+    true
+  )
+
+  textView.text = data.await()
+
+}.cache {
+
+  Log.d("日志", "读取缓存")
+
+  val data = get<String>(
+    "https://raw.githubusercontent.com/liangjingkanji/BRV/master/README.md",
+    CacheMode.READ_CACHE,
+    true
+  )
+
+  textView.text = data.await()
+}
+```
+
+上面示例代码这种属于: 先加载缓存(没有缓存不会报异常), 后网络请求(缓存和网络请求都失败报异常信息), 网络请求成功缓存到本地并刷新界面UI.
+
+
+
+注意事项
+
+1. CacheMode: 属于Kalle的缓存模式, 共有九种缓存模式适用于不同的业务场景: [文档](https://yanzhenjie.com/Kalle/cache/)
+2. cache: 该作用域内部允许抛出任何异常都不算错误, 这里的`cache`会比`scopeNetLife`先执行.
+3. 当缓存读取成功视为作用域执行成功, 默认情况即使后续的网络请求失败也不会提示错误信息(cache函数参数指定true则提示)
+
 ## 轮循器
 
 本框架附带一个超级强大的轮循器`Interval`, 基本上包含轮循器所需要到所有功能
@@ -597,11 +671,12 @@ Interval(1, TimeUnit.SECONDS).subscribe {
 
 ```
 subscribe() // 即开启定时器, 订阅多个也会监听同一个计数器
+start() // 开始
 stop() // 结束
 pause() // 暂停
 resume() // 继续
 reset // 重置轮循器 (包含计数器count和计时period) 不会停止轮循器
 switch //  切换轮循器开关
 
-state // 轮循器的状态
+state // 得到当前轮循器的状态
 ```

+ 1 - 1
net/build.gradle

@@ -40,7 +40,7 @@ dependencies {
 
     api 'com.yanzhenjie:okalle:0.1.7'
 
-    compileOnly 'com.github.liangjingkanji:BRV:1.1.7'
+    compileOnly 'com.github.liangjingkanji:BRV:1.2.0'
     compileOnly 'com.github.bumptech.glide:glide:4.9.0'
 
     implementation 'com.github.liangjingkanji:Tooltip:1.0.3'

+ 36 - 20
net/src/main/java/com/drake/net/Net.kt

@@ -16,6 +16,7 @@ import com.yanzhenjie.kalle.Kalle
 import com.yanzhenjie.kalle.download.UrlDownload
 import com.yanzhenjie.kalle.simple.SimpleBodyRequest
 import com.yanzhenjie.kalle.simple.SimpleUrlRequest
+import com.yanzhenjie.kalle.simple.cache.CacheMode
 import kotlinx.coroutines.*
 import java.io.File
 
@@ -33,17 +34,21 @@ import java.io.File
 
 inline fun <reified M> CoroutineScope.get(
     path: String,
-    tag: Any? = null,
+    cache: CacheMode = CacheMode.HTTP,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     noinline block: (SimpleUrlRequest.Api.() -> Unit)? = null
 ): Deferred<M> = async(Dispatchers.IO) {
 
-    val get = Kalle.get(if (absolutePath) path else (NetConfig.host + path)).tag(tag)
+    val request = Kalle.get(if (absolutePath) path else (NetConfig.host + path))
+        .tag(tag)
+        .cacheKey(path)
+        .cacheMode(cache)
 
     val response = if (block == null) {
-        get.perform(M::class.java, String::class.java)
+        request.perform(M::class.java, String::class.java)
     } else {
-        get.apply(block).perform<M, String>(M::class.java, String::class.java)
+        request.apply(block).perform<M, String>(M::class.java, String::class.java)
     }
 
     if (isActive) {
@@ -60,16 +65,22 @@ inline fun <reified M> CoroutineScope.get(
 
 inline fun <reified M> CoroutineScope.post(
     path: String,
-    tag: Any? = null,
+    cache: CacheMode = CacheMode.HTTP,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     noinline block: (SimpleBodyRequest.Api.() -> Unit)? = null
 ): Deferred<M> = async(Dispatchers.IO) {
 
-    val post = Kalle.post(if (absolutePath) path else (NetConfig.host + path)).tag(tag)
+    val request =
+        Kalle.post(if (absolutePath) path else (NetConfig.host + path))
+            .tag(tag)
+            .cacheKey(path)
+            .cacheMode(cache)
+
     val response = if (block == null) {
-        post.perform<M, String>(M::class.java, String::class.java)
+        request.perform<M, String>(M::class.java, String::class.java)
     } else {
-        post.apply(block).perform<M, String>(M::class.java, String::class.java)
+        request.apply(block).perform<M, String>(M::class.java, String::class.java)
     }
 
     if (isActive) {
@@ -87,8 +98,7 @@ inline fun <reified M> CoroutineScope.post(
 /**
  * 下载文件
  *
- * @param path String 网络路径, 非绝对路径会加上HOST为前缀
- * @see NetConfig.host
+ * @param path String 网络路径, 非绝对路径会加上HOST[NetConfig.host]为前缀
  * @param directory String 下载文件存放目录 {默认存在android/data/packageName/cache目录}
  * @param tag 可以传递对象给Request请求
  * @param absolutePath Boolean 下载链接是否是绝对路径
@@ -98,8 +108,8 @@ inline fun <reified M> CoroutineScope.post(
 fun CoroutineScope.download(
     path: String,
     directory: String = NetConfig.app.externalCacheDir!!.absolutePath,
-    tag: Any? = null,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     block: (UrlDownload.Api.() -> Unit)? = null
 ): Deferred<String> = async(Dispatchers.IO) {
     val realPath = if (absolutePath) path else (NetConfig.host + path)
@@ -153,16 +163,19 @@ fun CoroutineScope.downImage(
 
 inline fun <reified M> syncGet(
     path: String,
-    tag: Any? = null,
+    cache: CacheMode = CacheMode.HTTP,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     noinline block: (SimpleUrlRequest.Api.() -> Unit)? = null
 ): M {
 
-    val get = Kalle.get(if (absolutePath) path else (NetConfig.host + path)).tag(tag)
+    val request =
+        Kalle.get(if (absolutePath) path else (NetConfig.host + path)).tag(tag).cacheKey(path)
+            .cacheMode(cache)
     val response = if (block == null) {
-        get.perform(M::class.java, String::class.java)
+        request.perform(M::class.java, String::class.java)
     } else {
-        get.apply(block).perform<M, String>(M::class.java, String::class.java)
+        request.apply(block).perform<M, String>(M::class.java, String::class.java)
     }
 
     return if (response.isSucceed) {
@@ -174,16 +187,19 @@ inline fun <reified M> syncGet(
 
 inline fun <reified M> syncPost(
     path: String,
-    tag: Any? = null,
+    cache: CacheMode = CacheMode.HTTP,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     noinline block: (SimpleBodyRequest.Api.() -> Unit)? = null
 ): M {
 
-    val post = Kalle.post(if (absolutePath) path else (NetConfig.host + path)).tag(tag)
+    val request =
+        Kalle.post(if (absolutePath) path else (NetConfig.host + path)).tag(tag).cacheKey(path)
+            .cacheMode(cache)
     val response = if (block == null) {
-        post.perform<M, String>(M::class.java, String::class.java)
+        request.perform<M, String>(M::class.java, String::class.java)
     } else {
-        post.apply(block).perform<M, String>(M::class.java, String::class.java)
+        request.apply(block).perform<M, String>(M::class.java, String::class.java)
     }
 
     return if (response.isSucceed) {
@@ -196,8 +212,8 @@ inline fun <reified M> syncPost(
 fun syncDownload(
     path: String,
     directory: String = NetConfig.app.externalCacheDir!!.absolutePath,
-    tag: Any? = null,
     absolutePath: Boolean = false,
+    tag: Any? = null,
     block: (UrlDownload.Api.() -> Unit)? = null
 ): String {
 

+ 14 - 7
net/src/main/java/com/drake/net/NetConfig.kt

@@ -10,7 +10,6 @@ package com.drake.net
 import android.app.Application
 import android.app.Dialog
 import android.view.View
-import android.widget.Toast
 import androidx.fragment.app.FragmentActivity
 import com.drake.net.error.RequestParamsException
 import com.drake.net.error.ResponseException
@@ -20,6 +19,7 @@ import com.drake.tooltip.toast
 import com.yanzhenjie.kalle.Kalle
 import com.yanzhenjie.kalle.KalleConfig
 import com.yanzhenjie.kalle.exception.*
+import com.yanzhenjie.kalle.simple.cache.DiskCacheStore
 import java.util.concurrent.ExecutionException
 
 
@@ -28,7 +28,6 @@ object NetConfig {
     lateinit var host: String
     lateinit var app: Application
 
-    internal var defaultToast: Toast? = null
     internal var defaultDialog: (DialogCoroutineScope.(FragmentActivity) -> Dialog)? = null
     internal var onError: Throwable.() -> Unit = {
 
@@ -93,7 +92,8 @@ object NetConfig {
  * @param config 进行配置网络请求
  *
  * 如果想要自动解析数据模型请配置转换器, 可以继承或者参考默认转换器
- * @see DefaultConverter
+ *
+ * @see com.drake.net.convert.DefaultConvert
  */
 fun Application.initNet(host: String, config: KalleConfig.Builder.() -> Unit = {}) {
     NetConfig.host = host
@@ -106,8 +106,6 @@ fun Application.initNet(host: String, config: KalleConfig.Builder.() -> Unit = {
 
 /**
  * 该函数指定某些Observer的onError中的默认错误信息处理
- * @see NetObserver
- * @see DialogObserver
  *
  * @see NetConfig.onError
  */
@@ -117,8 +115,6 @@ fun KalleConfig.Builder.onError(block: Throwable.() -> Unit) {
 
 /**
  * 该函数指定某些Observer的onError中的默认错误信息处理
- * @see PageObserver
- * @see StateObserver
  *
  * 如果不设置默认只有 解析数据错误 | 后台自定义错误 会显示吐司
  * @see NetConfig.onStateError
@@ -136,3 +132,14 @@ fun KalleConfig.Builder.onDialog(block: (DialogCoroutineScope.(context: Fragment
     NetConfig.defaultDialog = block
 }
 
+fun KalleConfig.Builder.cacheEnabled(
+    path: String = NetConfig.app.cacheDir.absolutePath,
+    password: String = "cache"
+) {
+    val cacheStore = DiskCacheStore.newBuilder(path)
+        .password(password)
+        .build()
+
+    cacheStore(cacheStore)
+}
+

+ 30 - 19
net/src/main/java/com/drake/net/scope/AndroidScope.kt

@@ -7,19 +7,16 @@
 
 package com.drake.net.scope
 
-import androidx.annotation.CallSuper
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
 import kotlinx.coroutines.*
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-
 
 /**
  * 异步协程作用域
  */
-open class AndroidScope() : CoroutineScope {
+@Suppress("unused", "MemberVisibilityCanBePrivate", "NAME_SHADOWING")
+open class AndroidScope() {
 
     constructor(
         lifecycleOwner: LifecycleOwner,
@@ -34,29 +31,34 @@ open class AndroidScope() : CoroutineScope {
         })
     }
 
-    private var catch: (AndroidScope.(Throwable) -> Unit)? = null
-    private var finally: (AndroidScope.(Throwable?) -> Unit)? = null
+    protected var catch: (AndroidScope.(Throwable) -> Unit)? = null
+    protected var finally: (AndroidScope.(Throwable?) -> Unit)? = null
+    protected var auto = true
+
     private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
         catch(throwable)
     }
 
-    final override val coroutineContext: CoroutineContext =
-        Dispatchers.Main.immediate + exceptionHandler + Job()
+    protected val scope
+        get() = CoroutineScope(Dispatchers.Main + exceptionHandler + SupervisorJob())
+
 
-    fun launch(
+    open fun launch(
         block: suspend CoroutineScope.() -> Unit
     ): AndroidScope {
-        val job: Job = launch(EmptyCoroutineContext, CoroutineStart.DEFAULT, block)
-        job.invokeOnCompletion { finally(it) }
+        start()
+        scope.launch(block = block).invokeOnCompletion { finally(it) }
         return this
     }
 
-    @CallSuper
+    protected open fun start() {
+
+    }
+
     protected open fun catch(e: Throwable) {
         catch?.invoke(this, e) ?: handleError(e)
     }
 
-    @CallSuper
     protected open fun finally(e: Throwable?) {
         finally?.invoke(this, e)
     }
@@ -64,17 +66,15 @@ open class AndroidScope() : CoroutineScope {
     /**
      * 当作用域内发生异常时回调
      */
-    fun catch(block: AndroidScope.(Throwable) -> Unit): AndroidScope {
+    open fun catch(block: AndroidScope.(Throwable) -> Unit = {}): AndroidScope {
         this.catch = block
         return this
     }
 
     /**
      * 无论正常或者异常结束都将最终执行
-     * @param block 如果是发生异常[Throwable]为该异常对象, 正常结束为NULL
-     * @see launch 只有通过该函数开启的作用域才有效
      */
-    fun finally(block: AndroidScope.(Throwable?) -> Unit): AndroidScope {
+    open fun finally(block: AndroidScope.(Throwable?) -> Unit = {}): AndroidScope {
         this.finally = block
         return this
     }
@@ -82,11 +82,22 @@ open class AndroidScope() : CoroutineScope {
 
     /**
      * 错误处理
-     * 一般用于被子类重写, 当前默认为打印异常信息到LogCat
      */
     open fun handleError(e: Throwable) {
         e.printStackTrace()
     }
 
+    fun cancel(cause: CancellationException? = null) {
+        scope.cancel(cause)
+    }
+
+    fun cancel(message: String, cause: Throwable? = null) {
+        scope.cancel(message, cause)
+    }
+
+    fun autoOff() {
+        auto = false
+    }
+
 }
 

+ 9 - 2
net/src/main/java/com/drake/net/scope/DialogCoroutineScope.kt

@@ -34,11 +34,13 @@ class DialogCoroutineScope(
     val activity: FragmentActivity,
     var dialog: Dialog? = null,
     val cancelable: Boolean = true
-) : AndroidScope(), LifecycleObserver {
+) : NetCoroutineScope(), LifecycleObserver {
 
     init {
         activity.lifecycle.addObserver(this)
+    }
 
+    override fun start() {
         dialog = when {
             dialog != null -> dialog
             defaultDialog != null -> defaultDialog?.invoke(this, activity)
@@ -48,12 +50,17 @@ class DialogCoroutineScope(
                 progress
             }
         }
-
         dialog?.setOnDismissListener { }
         dialog?.setCancelable(cancelable)
         dialog?.show()
     }
 
+    override fun readCache(succeed: Boolean) {
+        if (succeed) {
+            dismiss()
+        }
+    }
+
     override fun handleError(e: Throwable) {
         NetConfig.onError(e)
     }

+ 64 - 2
net/src/main/java/com/drake/net/scope/NetCoroutineScope.kt

@@ -11,13 +11,26 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
 import com.drake.net.NetConfig
-import kotlinx.coroutines.cancel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.supervisorScope
 
 
 /**
  * 自动显示网络错误信息协程作用域
  */
-class NetCoroutineScope() : AndroidScope() {
+@Suppress("unused", "MemberVisibilityCanBePrivate", "NAME_SHADOWING")
+open class NetCoroutineScope() : AndroidScope() {
+
+    protected var readCache = true
+    protected var onCache: (suspend CoroutineScope.() -> Unit)? = null
+
+    protected var cacheSucceed = false
+        get() = if (onCache != null) field else false
+
+    protected var error = true
+        get() = if (cacheSucceed) field else true
+
 
     constructor(
         lifecycleOwner: LifecycleOwner,
@@ -32,8 +45,57 @@ class NetCoroutineScope() : AndroidScope() {
         })
     }
 
+    override fun launch(
+        block: suspend CoroutineScope.() -> Unit
+    ): NetCoroutineScope {
+        scope.launch {
+            start()
+            if (onCache != null && readCache) {
+                supervisorScope {
+                    cacheSucceed = try {
+                        onCache?.invoke(this)
+                        true
+                    } catch (e: Exception) {
+                        false
+                    }
+                    readCache(cacheSucceed)
+                }
+            }
+
+            block()
+        }.invokeOnCompletion {
+            finally(it)
+        }
+
+        return this
+    }
+
+    protected open fun readCache(succeed: Boolean) {
+
+    }
+
     override fun handleError(e: Throwable) {
         NetConfig.onError(e)
     }
 
+    override fun catch(e: Throwable) {
+        catch?.invoke(this, e) ?: if (error) handleError(e)
+    }
+
+
+    /**
+     * 该函数一般用于缓存读取
+     * 只在第一次启动作用域时回调
+     * 该函数在作用域[launch]之前执行
+     * 函数内部所有的异常都不会被抛出, 也不会终止作用域执行
+     */
+    fun cache(
+        error: Boolean = false,
+        onCache: suspend CoroutineScope.() -> Unit
+    ): AndroidScope {
+        this.error = error
+        this.onCache = onCache
+        return this
+    }
+
 }

+ 40 - 15
net/src/main/java/com/drake/net/scope/PageCoroutineScope.kt

@@ -11,15 +11,18 @@ import android.view.View
 import com.drake.brv.BindingAdapter
 import com.drake.brv.PageRefreshLayout
 import com.drake.net.NetConfig
-import kotlinx.coroutines.cancel
+import com.drake.net.R
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "NAME_SHADOWING")
 class PageCoroutineScope(
     val page: PageRefreshLayout
-) : AndroidScope() {
+) : NetCoroutineScope() {
 
     val index get() = page.index
-    var manual = false
+
+    private var load
+        get() = page.getTag(R.id.load) as? Boolean ?: false
+        set(value) = page.setTag(R.id.load, value)
 
     init {
         page.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
@@ -33,26 +36,50 @@ class PageCoroutineScope(
         })
     }
 
+    override fun start() {
+        (page.getTag(R.id.cache_succeed) as? Boolean)?.let {
+            readCache = false
+            cacheSucceed = it
+        }
+    }
+
+    override fun readCache(succeed: Boolean) {
+        if (succeed) {
+            page.showContent()
+        }
+        page.setTag(R.id.cache_succeed, succeed)
+    }
+
     override fun catch(e: Throwable) {
         super.catch(e)
-        if (page.stateEnabled) page.showError() else page.finish(false)
+
+        if (cacheSucceed || load) {
+            finish(false)
+        } else {
+            page.showError()
+        }
     }
 
     override fun finally(e: Throwable?) {
         super.finally(e)
-        if (e == null && !manual) {
-            if (page.stateEnabled) page.showContent() else page.finish()
+        if (e == null && auto) {
+            page.showContent()
+            load = true
         }
     }
 
     override fun handleError(e: Throwable) {
-        NetConfig.onStateError.invoke(e, page)
+        if (cacheSucceed || load) {
+            NetConfig.onError(e)
+        } else {
+            NetConfig.onStateError(e, page)
+        }
     }
 
     /**
      * 自动判断是添加数据还是覆盖数据, 以及数据为空或者NULL时[showEmpty]
      *
-     * @param hasMore 如果不传数据, 默认已加载完全部(建议此时可以关闭[PageRefreshLayout]的加载更多功能)
+     * @param hasMore 如果不传数据, 默认已加载完全部 (建议此时可以关闭[PageRefreshLayout]的加载更多功能)
      */
     fun addData(
         data: List<Any>,
@@ -60,7 +87,7 @@ class PageCoroutineScope(
         hasMore: BindingAdapter.() -> Boolean = { false }
     ) {
         page.addData(data, bindingAdapter, hasMore)
-        manual = true
+        autoOff()
     }
 
     /**
@@ -68,8 +95,7 @@ class PageCoroutineScope(
      */
     fun showEmpty() {
         page.showEmpty()
-        manual = true
-        cancel()
+        autoOff()
     }
 
     /**
@@ -79,8 +105,8 @@ class PageCoroutineScope(
      */
     fun showContent() {
         page.showContent()
-        manual = true
-        cancel()
+        load = true
+        autoOff()
     }
 
     /**
@@ -92,8 +118,7 @@ class PageCoroutineScope(
      */
     fun finish(success: Boolean = true) {
         page.finish(success)
-        manual = true
-        cancel()
+        autoOff()
     }
 
 }

+ 13 - 3
net/src/main/java/com/drake/net/scope/RefreshCoroutineScope.kt

@@ -9,8 +9,8 @@ package com.drake.net.scope
 
 import android.view.View
 import com.drake.net.NetConfig
+import com.drake.net.R
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
-import kotlinx.coroutines.cancel
 
 /**
  * 自动结束下拉刷新 协程作用域
@@ -18,7 +18,7 @@ import kotlinx.coroutines.cancel
 class RefreshCoroutineScope(
     val refresh: SmartRefreshLayout,
     loadMore: Boolean = false
-) : AndroidScope() {
+) : NetCoroutineScope() {
 
     init {
         refresh.setEnableLoadMore(loadMore)
@@ -33,6 +33,16 @@ class RefreshCoroutineScope(
         })
     }
 
+    override fun start() {
+        readCache = refresh.getTag(R.id.cache_succeed) as? Boolean ?: true
+    }
+
+    override fun readCache(succeed: Boolean) {
+        refresh.finishRefresh()
+        refresh.setTag(R.id.cache_succeed, false)
+        autoOff()
+    }
+
     override fun catch(e: Throwable) {
         super.catch(e)
         refresh.finishRefresh(false)
@@ -40,7 +50,7 @@ class RefreshCoroutineScope(
 
     override fun finally(e: Throwable?) {
         super.finally(e)
-        if (e == null) {
+        if (e == null && auto) {
             refresh.finishRefresh(true)
         }
     }

+ 30 - 11
net/src/main/java/com/drake/net/scope/StateCoroutineScope.kt

@@ -11,14 +11,14 @@ import android.app.Activity
 import android.view.View
 import androidx.fragment.app.Fragment
 import com.drake.net.NetConfig
+import com.drake.net.R
 import com.drake.statelayout.StateLayout
 import com.drake.statelayout.state
-import kotlinx.coroutines.cancel
 
 /**
- * 缺省页协程作用域
+ * 缺省页作用域
  */
-class StateCoroutineScope(val state: StateLayout) : AndroidScope() {
+class StateCoroutineScope(val state: StateLayout) : NetCoroutineScope() {
 
 
     constructor(view: View) : this(view.state())
@@ -36,29 +36,48 @@ class StateCoroutineScope(val state: StateLayout) : AndroidScope() {
                 cancel()
             }
         })
+    }
 
+    override fun start() {
+        (state.getTag(R.id.cache_succeed) as? Boolean)?.let {
+            readCache = false
+            cacheSucceed = it
+        }
+    }
 
+    override fun readCache(succeed: Boolean) {
+        if (succeed) {
+            state.showContent()
+        }
+        state.setTag(R.id.cache_succeed, succeed)
     }
 
     override fun catch(e: Throwable) {
         super.catch(e)
-        state.showError()
-        NetConfig.onStateError(e, state)
+        if (!cacheSucceed) {
+            state.showError()
+        }
+    }
+
+    override fun handleError(e: Throwable) {
+        if (cacheSucceed) {
+            super.handleError(e)
+        } else {
+            NetConfig.onStateError(e, state)
+        }
     }
 
     override fun finally(e: Throwable?) {
-        if (e == null) {
+        super.finally(e)
+        if (e == null && auto) {
             state.showContent()
         }
-        super.finally(e)
     }
 
-    /**
-     * 显示空缺省页
-     */
+
     fun showEmpty() {
         state.showEmpty()
-        cancel()
+        autoOff()
     }
 
 }

+ 2 - 2
net/src/main/java/com/drake/net/utils/Interval.kt

@@ -14,7 +14,6 @@ import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
 import com.drake.net.scope.AndroidScope
 import kotlinx.coroutines.ObsoleteCoroutinesApi
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.ReceiveChannel
 import kotlinx.coroutines.channels.TickerMode
 import kotlinx.coroutines.channels.ticker
@@ -152,7 +151,8 @@ class Interval(
         if (_state == TickerState.STATE_IDLE) return
         count = start
         scope?.cancel()
-        launch()
+        delay = unit.toMillis(initialDelay)
+        if (_state == TickerState.STATE_ACTIVE) launch()
     }
 
     /**

+ 15 - 50
net/src/main/java/com/drake/net/utils/ScopeUtils.kt

@@ -7,11 +7,9 @@
 
 package com.drake.net.utils
 
-import android.app.Activity
 import android.app.Dialog
 import android.os.Handler
 import android.os.Looper
-import android.view.View
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.Lifecycle
@@ -19,7 +17,6 @@ import androidx.lifecycle.LifecycleOwner
 import com.drake.brv.PageRefreshLayout
 import com.drake.net.scope.*
 import com.drake.statelayout.StateLayout
-import com.drake.statelayout.state
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
 import kotlinx.coroutines.CoroutineScope
 
@@ -33,7 +30,7 @@ import kotlinx.coroutines.CoroutineScope
 
 /**
  * 作用域开始时自动显示加载对话框, 结束时自动关闭加载对话框
- * 可以设置全局对话框 [NetConfig.onDialog]
+ * 可以设置全局对话框 [com.drake.net.NetConfig.onDialog]
  * @param dialog 仅该作用域使用的对话框
  *
  * 对话框被取消或者界面关闭作用域被取消
@@ -41,72 +38,41 @@ import kotlinx.coroutines.CoroutineScope
 fun FragmentActivity.scopeDialog(
     dialog: Dialog? = null,
     cancelable: Boolean = true, block: suspend CoroutineScope.() -> Unit
-): AndroidScope {
+): NetCoroutineScope {
     return DialogCoroutineScope(this, dialog, cancelable).launch(block = block)
 }
 
 fun Fragment.scopeDialog(
     dialog: Dialog? = null,
     cancelable: Boolean = true, block: suspend CoroutineScope.() -> Unit
-): AndroidScope {
+): NetCoroutineScope {
     return DialogCoroutineScope(activity!!, dialog, cancelable).launch(block = block)
 }
 
 // </editor-fold>
 
-// <editor-fold desc="缺省页">
-
 
 /**
  * 自动处理缺省页的异步作用域
  * 作用域开始执行时显示加载中缺省页
  * 作用域正常结束时显示成功缺省页
  * 作用域抛出异常时显示错误缺省页
- * 并且自动吐司错误信息, 可配置 [NetConfig.onStateError]
+ * 并且自动吐司错误信息, 可配置 [com.drake.net.NetConfig.onStateError]
  * 自动打印异常日志
  * @receiver 当前视图会被缺省页包裹
  *
  * 布局被销毁或者界面关闭作用域被取消
  */
-fun View.scopeState(block: suspend CoroutineScope.(StateCoroutineScope) -> Unit): AndroidScope {
-    val stateLayout = state()
-    val scope = StateCoroutineScope(stateLayout)
-    stateLayout.onRefresh {
-        scope.launch { block(scope) }
-    }.showLoading()
-    return scope
-}
-
-fun Fragment.scopeState(block: suspend CoroutineScope.(StateCoroutineScope) -> Unit): AndroidScope {
-    val stateLayout = state()
-    val scope = StateCoroutineScope(stateLayout)
-    stateLayout.onRefresh {
-        scope.launch { block(scope) }
-    }.showLoading()
-    return scope
-}
-
-fun Activity.scopeState(block: suspend CoroutineScope.(StateCoroutineScope) -> Unit): AndroidScope {
-    val stateLayout = state()
-    val scope = StateCoroutineScope(stateLayout)
-    stateLayout.onRefresh {
-        scope.launch { block(scope) }
-    }.showLoading()
-    return scope
-}
-
-fun StateLayout.scope(block: suspend CoroutineScope.(StateCoroutineScope) -> Unit): AndroidScope {
+fun StateLayout.scope(block: suspend CoroutineScope.(StateCoroutineScope) -> Unit): NetCoroutineScope {
     val scope = StateCoroutineScope(this)
-    onRefresh {
-        scope.launch { block(scope) }
-    }.showLoading()
+    scope.launch { block(scope) }
     return scope
 }
 
-// </editor-fold>
 
 /**
  * SmartRefreshLayout的异步作用域
+ *
  * 自动结束下拉刷新
  *
  * 布局被销毁或者界面关闭作用域被取消
@@ -114,29 +80,28 @@ fun StateLayout.scope(block: suspend CoroutineScope.(StateCoroutineScope) -> Uni
 fun SmartRefreshLayout.scopeRefresh(
     loadMore: Boolean = false,
     block: suspend CoroutineScope.() -> Unit
-): AndroidScope {
+): NetCoroutineScope {
     val scope = RefreshCoroutineScope(this, loadMore)
-    scope.launch(block = block)
+    scope.launch(block)
     return scope
 }
 
 /**
  * PageRefreshLayout的异步作用域
+ *
  * 1. 下拉刷新自动结束
  * 2. 上拉加载自动结束
  * 3. 捕获异常
  * 4. 打印异常日志
- * 5. 吐司部分异常[NetConfig.onStateError]
+ * 5. 吐司部分异常[com.drake.net.NetConfig.onStateError]
  * 6. 判断添加还是覆盖数据
  * 7. 自动显示缺省页
  *
  * 布局被销毁或者界面关闭作用域被取消
  */
-fun PageRefreshLayout.scope(block: suspend CoroutineScope.(PageCoroutineScope) -> Unit): AndroidScope {
+fun PageRefreshLayout.scope(block: suspend CoroutineScope.(PageCoroutineScope) -> Unit): PageCoroutineScope {
     val scope = PageCoroutineScope(this)
-    scope.launch {
-        block(scope)
-    }
+    scope.launch { block(scope) }
     return scope
 }
 
@@ -173,7 +138,7 @@ fun LifecycleOwner.scopeLife(
  *
  * 该作用域生命周期跟随整个应用, 注意内存泄漏
  */
-fun scopeNet(block: suspend CoroutineScope.() -> Unit): AndroidScope {
+fun scopeNet(block: suspend CoroutineScope.() -> Unit): NetCoroutineScope {
     return NetCoroutineScope().launch(block = block)
 }
 
@@ -181,7 +146,7 @@ fun scopeNet(block: suspend CoroutineScope.() -> Unit): AndroidScope {
 fun LifecycleOwner.scopeNetLife(
     lifeEvent: Lifecycle.Event = Lifecycle.Event.ON_DESTROY,
     block: suspend CoroutineScope.() -> Unit
-): AndroidScope {
+): NetCoroutineScope {
     return NetCoroutineScope(this, lifeEvent).launch(block = block)
 }
 

+ 23 - 1
net/src/main/java/com/drake/net/utils/SuspendUtils.kt

@@ -9,8 +9,12 @@ package com.drake.net.utils
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.supervisorScope
 import kotlinx.coroutines.withContext
 
+
+// <editor-fold desc="切换调度器">
+
 suspend fun <T> withMain(block: suspend CoroutineScope.() -> T) =
     withContext(Dispatchers.Main, block)
 
@@ -21,4 +25,22 @@ suspend fun <T> withDefault(block: suspend CoroutineScope.() -> T) =
     withContext(Dispatchers.Default, block)
 
 suspend fun <T> withUnconfined(block: suspend CoroutineScope.() -> T) =
-    withContext(Dispatchers.Unconfined, block)
+    withContext(Dispatchers.Unconfined, block)
+
+// </editor-fold>
+
+/**
+ * 允许抛出任何异常的作用域, 不会导致父协程取消和崩溃
+ */
+suspend fun tryScope(
+    error: suspend CoroutineScope.(Throwable) -> Unit = { it.printStackTrace() },
+    block: suspend CoroutineScope.() -> Unit
+) {
+    return supervisorScope {
+        try {
+            block()
+        } catch (e: Exception) {
+            error(e)
+        }
+    }
+}

+ 5 - 1
net/src/main/res/values/strings.xml

@@ -5,7 +5,7 @@
     <!--网络请求异常-->
     <string name="net_network_error">当前网络不可用</string>
     <string name="net_url_error">请求资源地址错误</string>
-    <string name="net_host_error">无法找到指定服务器主机</string>
+    <string name="net_host_error">网络连接不可用</string>
     <string name="net_connect_timeout_error">连接服务器超时,请重试</string>
     <string name="net_connect_exception">请检查网络连接</string>
     <string name="net_read_exception">读取数据错误</string>
@@ -23,4 +23,8 @@
     <!--对话框-->
     <string name="net_dialog_msg">加载中</string>
 
+
+    <item name="cache_succeed" type="id" />
+    <item name="load" type="id" />
+
 </resources>

+ 7 - 1
sample/build.gradle

@@ -39,7 +39,9 @@ dependencies {
 
     implementation 'com.squareup.moshi:moshi-kotlin:1.8.0'
     kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0'
-    implementation 'com.github.liangjingkanji:BRV:1.1.7'
+
+    implementation 'com.github.liangjingkanji:BRV:1.2.0'
+
     implementation 'com.scwang.smart:refresh-header-classics:2.0.0-alpha-1'
     implementation 'com.scwang.smart:refresh-footer-classics:2.0.0-alpha-1'
 
@@ -52,4 +54,8 @@ dependencies {
     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
 
+    implementation 'androidx.recyclerview:recyclerview:1.1.0'
+    implementation 'com.google.android.material:material:1.0.0'
+
+
 }

+ 15 - 17
sample/src/main/java/com/drake/net/sample/App.kt

@@ -1,10 +1,12 @@
 package com.drake.net.sample
 
 import android.app.Application
+import com.drake.net.cacheEnabled
 import com.drake.net.initNet
 import com.drake.statelayout.StateConfig
-import com.yanzhenjie.kalle.simple.SimpleUrlRequest
-import com.yanzhenjie.kalle.simple.cache.DiskCacheStore
+import com.scwang.smart.refresh.footer.ClassicsFooter
+import com.scwang.smart.refresh.header.ClassicsHeader
+import com.scwang.smart.refresh.layout.SmartRefreshLayout
 
 class App : Application() {
 
@@ -27,22 +29,18 @@ class App : Application() {
 
                         })*/
 
-            addInterceptor {
-
-
-                val request = it.request()
-
-                if (request is SimpleUrlRequest) {
-                }
-
-                it.proceed(request)
-            }
-
-            val cacheStore = DiskCacheStore.newBuilder(cacheDir.absolutePath)
-                .password("cache")
-                .build()
+            cacheEnabled()
+        }
 
-            cacheStore(cacheStore)
+        SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout ->
+            ClassicsHeader(
+                context
+            )
+        }
+        SmartRefreshLayout.setDefaultRefreshFooterCreator { context, layout ->
+            ClassicsFooter(
+                context
+            )
         }
 
     }

+ 25 - 10
sample/src/main/java/com/drake/net/sample/MainActivity.kt

@@ -1,9 +1,10 @@
 package com.drake.net.sample
 
 import android.os.Bundle
+import android.util.Log
 import androidx.appcompat.app.AppCompatActivity
 import com.drake.net.get
-import com.drake.net.utils.scopeDialog
+import com.drake.net.utils.scope
 import com.yanzhenjie.kalle.simple.cache.CacheMode
 import kotlinx.android.synthetic.main.activity_main.*
 
@@ -15,22 +16,36 @@ class MainActivity : AppCompatActivity() {
 
         setContentView(R.layout.activity_main)
 
+        content.onRefresh {
 
+            scope {
 
+                Log.d("日志", "(MainActivity.kt:50)    网络")
 
-        scopeDialog {
+                val data = get<String>(
+                    "https://raw.githubusercontent.com/liangjingkanji/BRV/master/README.md",
+                    CacheMode.NETWORK_YES_THEN_WRITE_CACHE,
+                    true
+                )
 
-            val data = get<String>(
-                "https://raw.githubusercontent.com/liangjingkanji/BRV/master/README.md",
-                absolutePath = true
-            ) {
-                cacheMode(CacheMode.HTTP_YES_THEN_WRITE_CACHE)
+                textView.text = data.await()
+
+            }.cache(false) {
+
+                Log.d("日志", "(MainActivity.kt:57)    缓存")
+
+                val data = get<String>(
+                    "https://raw.githubusercontent.com/liangjingkanji/BRV/master/README.md",
+                    CacheMode.READ_CACHE,
+                    true
+                )
+
+                textView.text = data.await()
             }
 
-            textView.text = data.await()
-        }
-    }
+        }.autoRefresh()
 
+    }
 }
 
 

+ 8 - 9
sample/src/main/res/layout/activity_main.xml

@@ -1,12 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
+
+<com.drake.brv.PageRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/content"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    tools:context=".MainActivity">
-
+    android:layout_height="match_parent">
 
     <ScrollView
         android:layout_width="match_parent"
@@ -16,10 +13,12 @@
         <TextView
             android:id="@+id/textView"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
+            android:layout_height="wrap_content"
             android:gravity="center"
-            android:text="主页" />
+            android:text="首页" />
+
 
     </ScrollView>
 
-</LinearLayout>
+</com.drake.brv.PageRefreshLayout>
+