feat(network): 添加网络请求演示功能
- 在Service接口中新增GET、POST和流式请求方法 - 实现WelcomeVM中的HTTP请求逻辑,支持GET、POST和SSE流式响应 - 在WelcomeActivity中集成网络请求功能并绑定UI事件 - 更新布局文件添加GET、POST、SSE演示按钮和结果展示区域 - 新增Python Flask服务器用于网络请求测试 - 配置跨域资源共享(CORS)支持移动端访问
这个提交包含在:
父节点
d7b14b2bce
当前提交
b640a7e7e5
@ -1,8 +1,24 @@
|
|||||||
package com.nova.brain.glass.repository
|
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 {
|
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>
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -2,27 +2,38 @@ package com.nova.brain.glass.ui
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.nova.brain.glass.R
|
import com.nova.brain.glass.R
|
||||||
import com.nova.brain.glass.databinding.ActivityWelcomeBinding
|
import com.nova.brain.glass.databinding.ActivityWelcomeBinding
|
||||||
import com.nova.brain.glass.helper.OfflineCmdListener
|
import com.nova.brain.glass.helper.OfflineCmdListener
|
||||||
import com.nova.brain.glass.helper.OfflineCmdServiceHelper
|
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 com.xuqm.base.ui.BaseActivity
|
||||||
import java.util.Timer
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
||||||
|
|
||||||
override fun getLayoutId(): Int = R.layout.activity_welcome
|
override fun getLayoutId(): Int = R.layout.activity_welcome
|
||||||
override fun fullscreen(): Boolean = true
|
override fun fullscreen(): Boolean = true
|
||||||
|
|
||||||
|
private lateinit var vm: WelcomeVM
|
||||||
|
|
||||||
override fun initView(savedInstanceState: Bundle?) {
|
override fun initView(savedInstanceState: Bundle?) {
|
||||||
super.initView(savedInstanceState)
|
super.initView(savedInstanceState)
|
||||||
|
vm = ViewModelProvider(this).get(WelcomeVM::class.java)
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
binding.tv.setOnClickListener {
|
binding.tv.setOnClickListener {
|
||||||
startActivity(Intent(this@WelcomeActivity, TaskListActivity::class.java))
|
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 {
|
private val offlineCmdListener = object : OfflineCmdListener {
|
||||||
@ -51,4 +62,4 @@ class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,75 @@
|
|||||||
package com.nova.brain.glass.viewmodel
|
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 com.xuqm.sdhbwfu.core.viewModel.BaseViewModel
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
|
||||||
class WelcomeVM : BaseViewModel() {
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:background="@color/app_color_black"
|
android:background="@color/app_color_black"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv"
|
android:id="@+id/tv"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -13,9 +14,77 @@
|
|||||||
android:text="您可以说:Nova,我的任务有哪些?"
|
android:text="您可以说:Nova,我的任务有哪些?"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="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_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
</layout>
|
||||||
119
server/app.py
普通文件
119
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
server/requirements.txt
普通文件
1
server/requirements.txt
普通文件
@ -0,0 +1 @@
|
|||||||
|
flask>=2.3.0
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户