Browse Source

输入框支持节流阀

drake 4 years ago
parent
commit
7c56e0dc32

+ 34 - 0
docs/debounce.md

@@ -0,0 +1,34 @@
+现在应用的搜索输入框一般情况下都是输入完搜索关键词后自动发起请求开始搜索
+
+这个过程涉及到以下需求:
+
+1. 不能每次变化都开始搜索请求, 这样会导致多余的网络资源浪费. 所以应该在用户停止输入后的指定时间后(默认800毫秒)开始搜索
+2. 当产生新的搜索请求后取消旧的请求以防止旧数据覆盖新数据
+3. 当输入内容没有变化(例如复制粘贴重复内容到搜索框)不会发起搜索请求
+
+<br>
+
+截图预览
+
+<img src="https://i.imgur.com/encjFdc.gif" width="250"/>
+
+<br>
+
+```kotlin
+var scope: CoroutineScope? = null
+
+et_input.debounce().listen(this) {
+    scope?.cancel() // 发起新的请求前取消旧的请求, 避免旧数据覆盖新数据
+    scope = scopeNetLife { // 保存旧的请求到一个变量中
+        tv_request_content.text = "请求中"
+        val data = Get<String>("http://api.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json",
+                               absolutePath = true).await()
+        tv_request_content.text = JSONObject(data).getJSONObject("result").getString("datetime_2")
+    }
+}
+```
+
+如果想要设置自己的节流阀超时时间请指定参数
+```kotlin
+fun EditText.debounce(timeoutMillis: Long = 800)
+```

+ 1 - 0
mkdocs.yml

@@ -58,6 +58,7 @@ nav:
   - 嵌套作用域: nested-scope.md
   - 最快请求结果: fastest.md
   - 重复请求: unique.md
+  - 节流阀: debounce.md
   - 取消请求: cancel.md
   - 日志记录器: log-recorder.md
   - 轮循器: interval.md

+ 69 - 14
net/src/main/java/com/drake/net/utils/FlowUtils.kt

@@ -16,28 +16,83 @@
 
 package com.drake.net.utils
 
+import android.text.Editable
+import android.text.TextWatcher
+import android.widget.EditText
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import com.drake.net.scope.AndroidScope
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.*
 
 /**
  * 收集Flow结果并过滤重复结果
  */
-fun <T> Flow<List<T>>.listen(
-    lifecycleOwner: LifecycleOwner? = null,
-    lifeEvent: Lifecycle.Event = Lifecycle.Event.ON_DESTROY,
+@OptIn(InternalCoroutinesApi::class)
+inline fun <T> Flow<T>.listen(
+    owner: LifecycleOwner? = null,
+    event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY,
     dispatcher: CoroutineDispatcher = Dispatchers.Main,
-    block: (List<T>) -> Unit
-): AndroidScope {
-    return AndroidScope(lifecycleOwner, lifeEvent, dispatcher).launch {
-        distinctUntilChanged().collect { data ->
-            block(data)
+    crossinline action: suspend CoroutineScope.(value: T) -> Unit
+): AndroidScope = AndroidScope(owner, event, dispatcher).launch {
+    this@listen.collect(object : FlowCollector<T> {
+        override suspend fun emit(value: T) = action(this@launch, value)
+    })
+}
+
+/**
+ * Flow直接创建作用域
+ * @param owner 跟随的生命周期组件
+ * @param event 销毁时机
+ * @param dispatcher 指定调度器
+ */
+@OptIn(InternalCoroutinesApi::class)
+inline fun <T> Flow<T>.scope(
+    owner: LifecycleOwner? = null,
+    event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY,
+    dispatcher: CoroutineDispatcher = Dispatchers.Main,
+    crossinline action: suspend CoroutineScope.(value: T) -> Unit
+): AndroidScope = AndroidScope(owner, event, dispatcher).launch {
+    this@scope.collect(object : FlowCollector<T> {
+        override suspend fun emit(value: T) = action(this@launch, value)
+    })
+}
+
+
+/**
+ * 为EditText的输入框文本变化启用节流阀, 即超过指定时间后(默认800毫秒)的输入框文本变化事件[TextWatcher.onTextChanged]会被下游收集到
+ *
+ * @param timeoutMillis 节流阀超时时间/单位毫秒, 默认值为800
+ */
+@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
+fun EditText.debounce(timeoutMillis: Long = 800) = callbackFlow<String> {
+
+    val textWatcher = object : TextWatcher {
+
+        override fun beforeTextChanged(
+            s: CharSequence,
+            start: Int,
+            count: Int,
+            after: Int
+        ) {
+        }
+
+        override fun onTextChanged(
+            s: CharSequence,
+            start: Int,
+            before: Int,
+            count: Int
+        ) {
+
+        }
+
+        override fun afterTextChanged(s: Editable) {
+            offer(s.toString())
         }
     }
-}
+
+    addTextChangedListener(textWatcher)
+    awaitClose { this@debounce.removeTextChangedListener(textWatcher) }
+}.debounce(timeoutMillis)
 

+ 1 - 25
net/src/main/java/com/drake/net/utils/Scope.kt

@@ -27,9 +27,6 @@ import com.drake.statelayout.StateLayout
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
 
 /**
  * 作用域内部全在主线程
@@ -160,25 +157,4 @@ fun Fragment.scopeNetLife(
     dispatcher: CoroutineDispatcher = Dispatchers.Main,
     block: suspend CoroutineScope.() -> Unit
 ) = NetCoroutineScope(this, lifeEvent, dispatcher).launch(block)
-//</editor-fold>
-
-
-/**
- * Flow直接创建作用域
- * @param owner 跟随的生命周期组件
- * @param event 销毁时机
- * @param dispatcher 指定调度器
- */
-@OptIn(InternalCoroutinesApi::class)
-inline fun <T> Flow<T>.scope(
-    owner: LifecycleOwner? = null,
-    event: Lifecycle.Event = Lifecycle.Event.ON_DESTROY,
-    dispatcher: CoroutineDispatcher = Dispatchers.Main,
-    crossinline action: suspend (value: T) -> Unit
-): AndroidScope = AndroidScope(owner, event, dispatcher).launch {
-    this@scope.collect(object : FlowCollector<T> {
-        override suspend fun emit(value: T) = action(value)
-    })
-}
-
-
+//</editor-fold>

+ 32 - 0
sample/src/main/java/com/drake/net/sample/ui/fragment/EditDebounceFragment.kt

@@ -0,0 +1,32 @@
+package com.drake.net.sample.ui.fragment
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import com.drake.net.Get
+import com.drake.net.sample.R
+import com.drake.net.utils.debounce
+import com.drake.net.utils.listen
+import com.drake.net.utils.scopeNetLife
+import kotlinx.android.synthetic.main.fragment_edit_debounce.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import org.json.JSONObject
+
+class EditDebounceFragment : Fragment(R.layout.fragment_edit_debounce) {
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+
+        var scope: CoroutineScope? = null
+
+        et_input.debounce().listen(this) {
+            scope?.cancel() // 发起新的请求前取消旧的请求, 避免旧数据覆盖新数据
+            scope = scopeNetLife { // 保存旧的请求到一个变量中
+                tv_request_content.text = "请求中"
+                val data = Get<String>("http://api.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json",
+                                       absolutePath = true).await()
+                tv_request_content.text = JSONObject(data).getJSONObject("result").getString("datetime_2")
+            }
+        }
+    }
+}

+ 11 - 0
sample/src/main/res/drawable/ic_debounce.xml

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M9.78,11.16l-1.42,1.42c-0.68,-0.69 -1.34,-1.58 -1.79,-2.94l1.94,-0.49C8.83,10.04 9.28,10.65 9.78,11.16zM11,6L7,2L3,6h3.02C6.04,6.81 6.1,7.54 6.21,8.17l1.94,-0.49C8.08,7.2 8.03,6.63 8.02,6H11zM21,6l-4,-4l-4,4h2.99c-0.1,3.68 -1.28,4.75 -2.54,5.88c-0.5,0.44 -1.01,0.92 -1.45,1.55c-0.34,-0.49 -0.73,-0.88 -1.13,-1.24L9.46,13.6C10.39,14.45 11,15.14 11,17c0,0 0,0 0,0h0v5h2v-5c0,0 0,0 0,0c0,-2.02 0.71,-2.66 1.79,-3.63c1.38,-1.24 3.08,-2.78 3.2,-7.37H21z" />
+</vector>

+ 25 - 0
sample/src/main/res/layout/fragment_edit_debounce.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".ui.fragment.EditDebounceFragment">
+
+    <EditText
+        android:id="@+id/et_input"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_marginHorizontal="32dp"
+        android:layout_marginTop="20dp"
+        android:hint="输入内容将自动查询当前时间" />
+
+
+    <TextView
+        android:id="@+id/tv_request_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="32dp"
+        android:textStyle="bold" />
+
+</LinearLayout>

+ 5 - 1
sample/src/main/res/menu/menu_main.xml

@@ -70,7 +70,11 @@
     <item
         android:id="@+id/unique"
         android:icon="@drawable/ic_unique"
-        android:title="唯一请求" />
+        android:title="重复请求" />
+    <item
+        android:id="@+id/edit_throttle"
+        android:icon="@drawable/ic_debounce"
+        android:title="节流阀" />
     <item
         android:id="@+id/state_layout"
         android:icon="@drawable/ic_state_layout"

+ 6 - 1
sample/src/main/res/navigation/nav_main.xml

@@ -119,7 +119,12 @@
     <fragment
         android:id="@+id/unique"
         android:name="com.drake.net.sample.ui.fragment.UniqueRequestFragment"
-        android:label="唯一请求"
+        android:label="重复请求"
         tools:layout="@layout/fragment_unique_request" />
+    <fragment
+        android:id="@+id/edit_throttle"
+        android:name="com.drake.net.sample.ui.fragment.EditDebounceFragment"
+        android:label="节流阀"
+        tools:layout="@layout/fragment_edit_debounce" />
 
 </navigation>