Просмотр исходного кода

feat(network): 添加网络请求演示功能

- 在Service接口中新增GET、POST和流式请求方法
- 实现WelcomeVM中的HTTP请求逻辑,支持GET、POST和SSE流式响应
- 在WelcomeActivity中集成网络请求功能并绑定UI事件
- 更新布局文件添加GET、POST、SSE演示按钮和结果展示区域
- 新增Python Flask服务器用于网络请求测试
- 配置跨域资源共享(CORS)支持移动端访问
徐勤民 1 день назад
Родитель
Сommit
b640a7e7e5

+ 18 - 2
app/src/main/java/com/nova/brain/glass/repository/Service.kt

@@ -1,8 +1,24 @@
 package com.nova.brain.glass.repository
 
+import io.reactivex.Observable
+import okhttp3.RequestBody
+import okhttp3.ResponseBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Streaming
 
 interface Service {
-//    @GET("drug/stock/standard?storehouseCode=2&type=&form=&purpose=&restrict=&danger=&antibiotic=&keyword=&manufacturerCode=&supplierCode=&expirationDateMin=&expirationDateMax=&sort=id&asc=false&papeIndexOnView=2&pageSize=20&tenantId=101")
-//    fun standard(@Query("pageIndex") pageIndex: Int): Observable<WelcomeLIstModel>
+
+    @GET("get")
+    fun demoGet(): Observable<ResponseBody>
+
+    @POST("post")
+    fun demoPost(@Body body: RequestBody): Observable<ResponseBody>
+
+    @Streaming
+    @GET("stream/{n}")
+    fun demoStream(@Path("n") n: Int): Observable<ResponseBody>
 
 }

+ 16 - 5
app/src/main/java/com/nova/brain/glass/ui/WelcomeActivity.kt

@@ -2,27 +2,38 @@ package com.nova.brain.glass.ui
 
 import android.content.Intent
 import android.os.Bundle
+import androidx.lifecycle.ViewModelProvider
 import com.nova.brain.glass.R
 import com.nova.brain.glass.databinding.ActivityWelcomeBinding
 import com.nova.brain.glass.helper.OfflineCmdListener
 import com.nova.brain.glass.helper.OfflineCmdServiceHelper
-import com.xuqm.base.common.LogHelper
+import com.nova.brain.glass.viewmodel.WelcomeVM
 import com.xuqm.base.ui.BaseActivity
-import java.util.Timer
-import kotlin.concurrent.schedule
 
 class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
 
     override fun getLayoutId(): Int = R.layout.activity_welcome
     override fun fullscreen(): Boolean = true
 
+    private lateinit var vm: WelcomeVM
+
     override fun initView(savedInstanceState: Bundle?) {
         super.initView(savedInstanceState)
+        vm = ViewModelProvider(this).get(WelcomeVM::class.java)
+        window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
         binding.tv.setOnClickListener {
             startActivity(Intent(this@WelcomeActivity, TaskListActivity::class.java))
         }
-        window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        binding.btnGet.setOnClickListener { vm.demoGet() }
+        binding.btnPost.setOnClickListener { vm.demoPost() }
+        binding.btnSse.setOnClickListener { vm.demoPostSse() }
+    }
 
+    override fun initData() {
+        super.initData()
+        vm.result.observe(this) { text ->
+            binding.tvResult.text = text
+        }
     }
 
     private val offlineCmdListener = object : OfflineCmdListener {
@@ -51,4 +62,4 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
         super.onDestroy()
         window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
     }
-}
+}

+ 69 - 0
app/src/main/java/com/nova/brain/glass/viewmodel/WelcomeVM.kt

@@ -1,6 +1,75 @@
 package com.nova.brain.glass.viewmodel
 
+import androidx.lifecycle.MutableLiveData
+import com.nova.brain.glass.repository.Service
+import com.xuqm.base.di.manager.HttpManager
 import com.xuqm.sdhbwfu.core.viewModel.BaseViewModel
+import io.reactivex.schedulers.Schedulers
+import okhttp3.MediaType
+import okhttp3.RequestBody
 
 class WelcomeVM : BaseViewModel() {
+
+    val result = MutableLiveData<String>()
+
+    companion object {
+        // 修改为运行 server/app.py 的机器在局域网中的 IP
+        const val DEMO_SERVER_URL = "http://192.168.27.248:8080/"
+    }
+
+    private val demoComponent by lazy {
+        HttpManager.getAppComponent(DEMO_SERVER_URL)
+    }
+
+    private val service by lazy {
+        HttpManager.getApi(demoComponent, Service::class.java)
+    }
+
+    fun demoGet() {
+        result.value = "GET 请求中..."
+        service.demoGet()
+            .subscribeOn(Schedulers.io())
+            .subscribe({ body ->
+                result.postValue("GET 响应:\n${body.string()}")
+            }, { e ->
+                result.postValue("GET 失败: ${e.message}")
+            }).adds()
+    }
+
+    fun demoPost() {
+        result.value = "POST 请求中..."
+        val json = """{"demo":"post","from":"glass"}"""
+        val body = RequestBody.create(MediaType.parse("application/json"), json)
+        service.demoPost(body)
+            .subscribeOn(Schedulers.io())
+            .subscribe({ resp ->
+                result.postValue("POST 响应:\n${resp.string()}")
+            }, { e ->
+                result.postValue("POST 失败: ${e.message}")
+            }).adds()
+    }
+
+    fun demoPostSse() {
+        result.postValue("SSE 连接中...")
+        service.demoStream(5)
+            .subscribeOn(Schedulers.io())
+            .subscribe({ body ->
+                val sb = StringBuilder("SSE 流式响应:\n")
+                try {
+                    val reader = body.charStream().buffered()
+                    var line: String?
+                    while (reader.readLine().also { line = it } != null) {
+                        val l = line!!
+                        if (l.isNotEmpty()) {
+                            sb.appendLine(l)
+                            result.postValue(sb.toString())
+                        }
+                    }
+                } catch (e: Exception) {
+                    result.postValue("SSE 读取异常: ${e.message}")
+                }
+            }, { e ->
+                result.postValue("SSE 失败: ${e.message}")
+            }).adds()
+    }
 }

+ 70 - 1
app/src/main/res/layout/activity_welcome.xml

@@ -4,6 +4,7 @@
         android:layout_width="match_parent"
         android:background="@color/app_color_black"
         android:layout_height="match_parent">
+
         <TextView
             android:id="@+id/tv"
             android:layout_width="wrap_content"
@@ -13,9 +14,77 @@
             android:text="您可以说:Nova,我的任务有哪些?"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/btnRow" />
+
+        <LinearLayout
+            android:id="@+id/btnRow"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="center"
+            android:layout_marginHorizontal="16dp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/scrollResult"
+            app:layout_constraintTop_toBottomOf="@id/tv">
+
+            <TextView
+                android:id="@+id/btnGet"
+                android:layout_width="0dp"
+                android:layout_height="40dp"
+                android:layout_weight="1"
+                android:layout_marginEnd="8dp"
+                android:gravity="center"
+                android:background="#33FFFFFF"
+                android:textColor="#ff3FFF5F"
+                android:textSize="14sp"
+                android:text="GET演示" />
+
+            <TextView
+                android:id="@+id/btnPost"
+                android:layout_width="0dp"
+                android:layout_height="40dp"
+                android:layout_weight="1"
+                android:layout_marginEnd="8dp"
+                android:gravity="center"
+                android:background="#33FFFFFF"
+                android:textColor="#ff3FFF5F"
+                android:textSize="14sp"
+                android:text="POST演示" />
+
+            <TextView
+                android:id="@+id/btnSse"
+                android:layout_width="0dp"
+                android:layout_height="40dp"
+                android:layout_weight="1"
+                android:gravity="center"
+                android:background="#33FFFFFF"
+                android:textColor="#ff3FFF5F"
+                android:textSize="14sp"
+                android:text="SSE演示" />
+        </LinearLayout>
+
+        <ScrollView
+            android:id="@+id/scrollResult"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginTop="8dp"
+            app:layout_constraintTop_toBottomOf="@id/btnRow"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            />
+            app:layout_constraintEnd_toEndOf="parent">
+
+            <TextView
+                android:id="@+id/tvResult"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="12dp"
+                android:textColor="#ff3FFF5F"
+                android:textSize="11sp"
+                android:fontFamily="monospace"
+                android:text="" />
+        </ScrollView>
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 </layout>

+ 119 - 0
server/app.py

@@ -0,0 +1,119 @@
+"""
+演示 HTTP 服务
+- GET  /get           普通 GET 请求演示
+- POST /post          普通 POST 请求演示
+- GET  /stream/<n>    流式响应演示(逐行返回 n 条 JSON)
+
+运行方式:
+  pip install flask
+  python app.py
+
+默认监听 0.0.0.0:8080,局域网内 Android 设备通过 http://<本机IP>:8080 访问
+"""
+
+import json
+import time
+from flask import Flask, request, Response, jsonify
+
+app = Flask(__name__)
+
+PORT = 8080
+
+
+def cors(response):
+    response.headers["Access-Control-Allow-Origin"] = "*"
+    response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
+    response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
+    return response
+
+
+@app.after_request
+def after_request(response):
+    return cors(response)
+
+
+# ── GET /get ──────────────────────────────────────────────────────────────────
+
+@app.route("/get", methods=["GET", "OPTIONS"])
+def demo_get():
+    data = {
+        "code": 0,
+        "message": "GET 请求成功",
+        "data": {
+            "method": "GET",
+            "url": request.url,
+            "args": dict(request.args),
+            "headers": dict(request.headers),
+            "timestamp": int(time.time()),
+        },
+    }
+    return jsonify(data)
+
+
+# ── POST /post ────────────────────────────────────────────────────────────────
+
+@app.route("/post", methods=["POST", "OPTIONS"])
+def demo_post():
+    body = {}
+    raw = ""
+    try:
+        body = request.get_json(force=True) or {}
+        raw = json.dumps(body, ensure_ascii=False)
+    except Exception:
+        raw = request.data.decode("utf-8", errors="replace")
+
+    data = {
+        "code": 0,
+        "message": "POST 请求成功",
+        "data": {
+            "method": "POST",
+            "url": request.url,
+            "body": body,
+            "raw": raw,
+            "headers": dict(request.headers),
+            "timestamp": int(time.time()),
+        },
+    }
+    return jsonify(data)
+
+
+# ── GET /stream/<n> ───────────────────────────────────────────────────────────
+
+@app.route("/stream/<int:n>", methods=["GET"])
+def demo_stream(n):
+    """
+    逐行输出 n 条 JSON,每条之间延迟 500ms,模拟流式响应。
+    客户端用 @Streaming + ResponseBody 逐行读取即可。
+    """
+    n = min(max(n, 1), 20)  # 限制 1~20 条
+
+    def generate():
+        for i in range(n):
+            line = json.dumps(
+                {
+                    "index": i + 1,
+                    "total": n,
+                    "message": f"流式数据第 {i + 1} 条",
+                    "timestamp": int(time.time()),
+                },
+                ensure_ascii=False,
+            )
+            yield line + "\n"
+            time.sleep(0.5)
+
+    return Response(
+        generate(),
+        status=200,
+        mimetype="application/octet-stream",
+        headers={"X-Accel-Buffering": "no"},
+    )
+
+
+# ── 入口 ──────────────────────────────────────────────────────────────────────
+
+if __name__ == "__main__":
+    print(f"Demo server running on http://0.0.0.0:{PORT}")
+    print("  GET  http://<本机IP>:8080/get")
+    print("  POST http://<本机IP>:8080/post")
+    print("  GET  http://<本机IP>:8080/stream/5")
+    app.run(host="0.0.0.0", port=PORT, debug=False, threaded=True)

+ 1 - 0
server/requirements.txt

@@ -0,0 +1 @@
+flask>=2.3.0