From 847d32650c55c24613e78c6544f76904ddc14630 Mon Sep 17 00:00:00 2001
From: drake
Date: Tue, 1 Aug 2023 15:20:40 +0800
Subject: [PATCH] =?UTF-8?q?doc:=20=E9=87=8D=E6=9E=84=E5=BC=80=E5=8F=91?=
=?UTF-8?q?=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 37 +--
docs/api/styles/jetbrains-mono.css | 18 +-
docs/auto-dialog.md | 73 ++---
docs/auto-page.md | 55 ----
docs/auto-pull.md | 38 +++
docs/auto-refresh.md | 40 ++-
docs/auto-state.md | 45 ++-
docs/cache.md | 64 ++--
docs/callback.md | 3 +-
docs/cancel.md | 44 +--
docs/config.md | 152 ++++-----
docs/convert-special.md | 129 --------
docs/converter-customize.md | 81 +++++
docs/converter-struct.md | 150 +++++++++
docs/converter.md | 281 ++--------------
docs/cookie.md | 14 +-
docs/coroutine-request.md | 17 +-
docs/css/extra.css | 303 ++----------------
docs/debounce.md | 36 +--
docs/default-response.md | 50 ---
docs/download-file.md | 48 ++-
docs/error-default.md | 29 --
docs/error-global.md | 41 ++-
docs/error-single.md | 23 +-
docs/{error-exception.md => error-throws.md} | 80 +++--
docs/error-tip.md | 80 +++--
docs/error.md | 25 ++
docs/exception-track.md | 28 --
docs/fastest.md | 48 ++-
docs/https.md | 14 +-
docs/img/book-open.svg | 1 +
docs/img/code-preview.png | Bin 0 -> 26782 bytes
docs/img/preview.png | Bin 0 -> 189960 bytes
docs/index.md | 95 +++---
docs/interceptor.md | 31 +-
docs/interval.md | 11 +-
docs/issues.md | 27 +-
docs/kotlin-serialization.md | 39 ++-
docs/log-notice.md | 41 ++-
docs/log-recorder.md | 54 ++--
docs/model-generate.md | 11 +-
docs/okhttp-client.md | 62 ++--
docs/practice.md | 11 -
docs/progress.md | 15 +-
docs/read-cache.md | 59 ----
docs/repeat-request.md | 21 +-
docs/request.md | 105 +++---
docs/scope.md | 48 +--
docs/switch-thread.md | 50 ---
docs/sync-request.md | 34 +-
docs/tag.md | 17 +-
docs/thread.md | 49 +++
docs/timing.md | 9 +-
docs/track.md | 24 ++
docs/updates.md | 2 +-
docs/upload-file.md | 6 +-
docs/view-model.md | 42 ++-
mkdocs.yml | 96 ++++--
sample/proguard-rules.pro | 2 +-
.../com/drake/net/sample/constants/Api.kt | 1 +
.../drake/net/sample/mock/MockDispatcher.kt | 17 +-
.../ui/fragment/EditDebounceFragment.kt | 7 +-
.../com/drake/net/sample/vm/UserViewModel.kt | 19 +-
sample/src/main/res/menu/menu_main.xml | 2 +-
sample/src/main/res/navigation/nav_main.xml | 2 +-
65 files changed, 1219 insertions(+), 1837 deletions(-)
delete mode 100644 docs/auto-page.md
create mode 100644 docs/auto-pull.md
delete mode 100644 docs/convert-special.md
create mode 100644 docs/converter-customize.md
create mode 100644 docs/converter-struct.md
delete mode 100644 docs/default-response.md
delete mode 100644 docs/error-default.md
rename docs/{error-exception.md => error-throws.md} (53%)
create mode 100644 docs/error.md
delete mode 100644 docs/exception-track.md
create mode 100644 docs/img/book-open.svg
create mode 100644 docs/img/code-preview.png
create mode 100644 docs/img/preview.png
delete mode 100644 docs/practice.md
delete mode 100644 docs/read-cache.md
delete mode 100644 docs/switch-thread.md
create mode 100644 docs/thread.md
create mode 100644 docs/track.md
diff --git a/README.md b/README.md
index a809a12a0..a091e8488 100644
--- a/README.md
+++ b/README.md
@@ -8,31 +8,31 @@
| 下载体验
-
+
-
+
-
+
-
+
-Android上可能是最强的网络框架, 基于[OkHttp](https://github.com/square/okhttp)/协程的非侵入式框架(不影响原有功能). 学习成本低/使用简单, 一行代码发起网络请求, 甚至无需初始化
+Net是基于[OkHttp](https://github.com/square/okhttp)/协程的非侵入式框架(可使用所有Api), 可升级OkHttp版本保持网络安全
-欢迎将本项目文档/注释进行国际化翻译, 感谢您的支持!
Welcome to international translation of this project's documents/notes, thank you for your support!
[Net 1.x](https://github.com/liangjingkanji/Net/tree/1.x) 版本使用RxJava实现
[Net 2.x](https://github.com/liangjingkanji/Net/tree/2.x) 版本使用协程实现
+[Net-okhttp3](https://github.com/liangjingkanji/Net-okhttp3) Net3.x的Android低版本兼容库
[Net 3.x](https://github.com/liangjingkanji/Net/) 版本使用协程实现, 可自定义OkHttp版本
@@ -44,9 +44,9 @@ Welcome to international translation of this project's documents/notes, thank yo
- [x] 专为Android而生
- [x] OkHttp最佳实践
- [x] 使用高性能Okio
-- [x] 支持OkHttp所有功能/组件
+- [x] 支持OkHttp所有Api
- [x] 随时升级OkHttp版本保证网络安全性
-- [x] 优秀的源码/注释/文档/示例
+- [x] 详细文档/低学习成本
- [x] 永远保持社区维护
## 主要功能
@@ -83,20 +83,7 @@ Welcome to international translation of this project's documents/notes, thank yo
## 安装
-添加远程仓库根据创建项目的 Android Studio 版本有所不同
-
-Android Studio Arctic Fox以下创建的项目 在项目根目录的 build.gradle 添加仓库
-
-```groovy
-allprojects {
- repositories {
- // ...
- maven { url 'https://jitpack.io' }
- }
-}
-```
-
-Android Studio Arctic Fox以上创建的项目 在项目根目录的 settings.gradle 添加仓库
+Project 的 settings.gradle 添加仓库
```kotlin
dependencyResolutionManagement {
@@ -107,15 +94,15 @@ dependencyResolutionManagement {
}
```
-然后在 module 的 build.gradle 添加依赖框架
+Module 的 build.gradle 添加依赖框架
```groovy
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" // 协程(版本自定)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
-implementation 'com.squareup.okhttp3:okhttp:4.10.0' // 要求OkHttp4以上
+implementation 'com.squareup.okhttp3:okhttp:4.11.0' // 要求OkHttp4以上
implementation 'com.github.liangjingkanji:Net:3.6.0'
```
-如果你是在 Android 5 (API level 21)以下开发, 要求使用OkHttp3.x请使用: [Net-okhttp3](https://github.com/liangjingkanji/Net-okhttp3)
+如果在 Android 5 (API level 21)以下开发, 请使用 [Net-okhttp3](https://github.com/liangjingkanji/Net-okhttp3)
## Contribute
diff --git a/docs/api/styles/jetbrains-mono.css b/docs/api/styles/jetbrains-mono.css
index 4592bb3a5..99c4b761a 100644
--- a/docs/api/styles/jetbrains-mono.css
+++ b/docs/api/styles/jetbrains-mono.css
@@ -1,33 +1,31 @@
@font-face{
font-family: 'JetBrains Mono';
- src: local('JetBrains Mono'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Regular.woff2') format('woff2'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Regular.ttf') format('truetype');
+ src: local('Iosevka Curly Medium'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/iosevka-curly/iosevka-curly-medium.woff2') format('woff2');
font-display: swap;
font-weight: normal;
font-style: normal;
}
@font-face{
font-family: 'JetBrains Mono';
- src: local('JetBrains Mono'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Bold.woff2') format('woff2'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/ttf/JetBrainsMono-Bold.ttf') format('truetype');
+ src: local('Iosevka Curly Bold'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/iosevka-curly/iosevka-curly-bold.woff2') format('woff2');
font-display: swap;
font-weight: bold;
font-style: normal;
}
@font-face{
font-family: 'HYZhengYuan';
- src: local('HYZhengYuan'),
- url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYZhengYuan.ttf') format('truetype');
+ src: local('HYYouYuan-55W'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYYouYuan/HYYouYuan-55W.ttf') format('truetype');
font-display: swap;
font-weight: normal;
font-style: normal;
}
@font-face{
font-family: 'HYZhengYuan';
- src: local('HYZhengYuan-75W'),
- url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYZhengYuan-75W.ttf') format('truetype');
+ src: local('HYYouYuan-75W'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYYouYuan/HYYouYuan-75W.ttf') format('truetype');
font-display: swap;
font-weight: bold;
font-style: normal;
diff --git a/docs/auto-dialog.md b/docs/auto-dialog.md
index b4ed29007..cbab287eb 100644
--- a/docs/auto-dialog.md
+++ b/docs/auto-dialog.md
@@ -1,9 +1,9 @@
-Net支持发起请求的时候自动弹出和关闭对话框(Loading Dialog)
+Net支持发起请求开始时显示加载框, 请求结束时隐藏加载框(无论成败)
-## 自动显示加载框
+## 自动显示
-只需要使用`scopeDialog`作用域即可.
-```kotlin
+
+```kotlin hl_lines="1"
scopeDialog {
tvFragment.text = Post("dialog") {
param("u_name", "drake") // 请求参数
@@ -14,14 +14,14 @@ scopeDialog {
-加载框默认使用的是Android原生加载框(MaterialDesign Dialog), 当然也提供参数传入指定每个请求的对话框
+默认是原生加载框(MaterialDesign Dialog), 可自定义
-## 指定单例加载框
+## 单例自定义
-就是仅针对当前网络请求指定加载框
+指定当前请求加载框
```kotlin
val dialog = BubbleDialog(requireActivity(), "加载中")
@@ -36,61 +36,46 @@ scopeDialog(dialog) {
-> 这里使用的iOS风格对话框: [BubbleDialog](https://liangjingkanji.github.io/Tooltip/bubble.html)
+!!! quote "菊花加载对话框"
+ 示例使用的iOS风格对话框: [BubbleDialog](https://liangjingkanji.github.io/Tooltip/bubble.html)
-## 指定全局加载框
+## 全局自定义
-在Application中配置Net时就可以设置默认的Dialog
+初始化时指定加载对话框构造器`NetDialogFactory`
-=== "初始配置全局加载框"
- ```kotlin
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
- setDialogFactory { // 全局加载对话框
- ProgressDialog(it).apply {
- setMessage("我是全局自定义的加载对话框...")
- }
+```kotlin
+NetConfig.initialize(Api.HOST, this) {
+ setDialogFactory {
+ ProgressDialog(it).apply {
+ setMessage("我是全局自定义的加载对话框...")
}
- }
- ```
-=== "修改全局加载框"
- ```kotlin
- NetConfig.dialogFactory = NetDialogFactory {
- ProgressDialog(it).apply {
- setMessage("我是全局自定义的加载对话框...")
}
- }
- ```
-
-
-
-如果不想修改全局加载框样式只是修改加载框文本, 可以覆盖文本(国际化同理)
+}
+```
-在当前项目下的values目录下的strings.xml添加以下一行可以修改文本
+如仅修改加载对话框文本, 在项目`values`目录的strings.xml添加以下
```xml
加载中
```
-## 生命周期
+!!! question "自定义的加载框不是Dialog"
+ 由于`scopeDialog`只能指定Dialog类型, 因此只能手动实现`Dialog`接口
-使用`scopeDialog`发起请求后, Dialog分为以下三个生命周期
+ 仅要求复写 [DialogCoroutineScope](https://github.com/liangjingkanji/Net/blob/2abf07e1d003ef44574278fd2010f3375225d964/net/src/main/java/com/drake/net/scope/DialogCoroutineScope.kt#L47) 内调用的`Dialog`方法
-|生命周期|描述|
-|-|-|
-|Dialog 显示|执行`scopeDialog`时显示加载框|
-|Dialog 自动结束|作用域内任务结束时关闭加载框|
-|Dialog 手动结束|加载框被手动取消时取消作用域内网络请求|
+## 生命周期
-## 自定义加载对话框
+使用`scopeDialog`发起请求后, 分为三个生命周期
-我想要自定义加载框视图
+| 加载框状态 | 作用域 |
+| ---------- | ----------------------------- |
+| 显示 | 执行`scopeDialog`时显示加载框 |
+| 隐藏 | 作用域内任务结束时隐藏加载框 |
+| 手动取消 | 取消作用域内所有网络请求 |
-- Dialog属于布局容器, 你可以继承Dialog然后创建属于自己的显示内容(类似Activity/Fragment), 比如该[iOS风格对话框](https://github.com/liangjingkanji/Tooltip/blob/HEAD/tooltip/src/main/java/com/drake/tooltip/dialog/BubbleDialog.kt)
-
-我的加载框不是Dialog
-- 虽然我们指定`scopeDialog`的加载框或者`setNetDialogFactory`时只允许传入一个Dialog对象, 但即使你使用的不是Dialog你也可以创建一个类继承Dialog, 然后在其生命周期函数中处理`自己特殊对话框`的展示和隐藏
diff --git a/docs/auto-page.md b/docs/auto-page.md
deleted file mode 100644
index 2b94e98dc..000000000
--- a/docs/auto-page.md
+++ /dev/null
@@ -1,55 +0,0 @@
-阅读自动分页加载之前请先阅读自动刷新
-
-Net属于低耦合框架, 分页加载同样需要依赖第三方组件: [BRV](https://github.com/liangjingkanji/BRV)(点击链接按文档依赖)
-
-
-创建布局
-```xml
-
-
-
-
-
-```
-
-创建列表
-```kotlin
-rv_pull.linear().setup {
- addType(R.layout.item_list)
-}
-```
-
-创建网络
-```kotlin
-page.onRefresh {
- scope {
- val data = Get("list") {
- param("page", index)
- }.await().data
- addData(data.list) {
- index < data.total
- }
- }
-}.autoRefresh()
-```
-
-- `index` 属于PageRefreshLayout的字段, 每次上拉加载自动+1递增, 下拉刷新自动重置
-- ` data.total`属于服务器返回的`列表全部数量`的字段, 最终使用什么字段或者判断条件请自己根据项目不同决定
-- `addData` 属于PageRefreshLayout的函数
- ```kotlin
- fun addData(
- data: List?,
- adapter: BindingAdapter? = null,
- isEmpty: () -> Boolean = { data.isNullOrEmpty() },
- hasMore: BindingAdapter.() -> Boolean = { true }
- )
- ```
- 具体请查看函数注释
\ No newline at end of file
diff --git a/docs/auto-pull.md b/docs/auto-pull.md
new file mode 100644
index 000000000..4e6746972
--- /dev/null
+++ b/docs/auto-pull.md
@@ -0,0 +1,38 @@
+首先请阅读上章节 [自动下拉刷新](auto-refresh.md), 已提及内容不再重复
+
+
+## 自动分页
+
+提供`addData()`来简化分页, 开发者可以借鉴实现
+
+```kotlin
+page.onRefresh {
+ scope {
+ val data = Get("list") {
+ param("page", index)
+ }.await().data
+ addData(data.list) {
+ index < data.total
+ }
+ }
+}.autoRefresh()
+```
+
+## 索引自增
+`index` 每次上拉加载自动++1, 刷新列表重置为`PageRefreshLayout.startIndex`
+
+## 有更多页
+
+根据`hasMore`返回结果是否关闭上拉加载, `isEmpty`决定是否显示`空数据`缺省页
+
+```kotlin
+fun addData(
+ data: List?,
+ adapter: BindingAdapter? = null,
+ isEmpty: () -> Boolean = { data.isNullOrEmpty() },
+ hasMore: BindingAdapter.() -> Boolean = { true }
+)
+```
+
+
+1. [PageRefreshLayout 下拉刷新/上拉加载](https://liangjingkanji.github.io/BRV/refresh.html)
\ No newline at end of file
diff --git a/docs/auto-refresh.md b/docs/auto-refresh.md
index 7dee3c407..5d6afc072 100644
--- a/docs/auto-refresh.md
+++ b/docs/auto-refresh.md
@@ -1,17 +1,17 @@
-Net属于低耦合框架, 自动下拉刷新需要依赖第三方组件: [BRV](https://github.com/liangjingkanji/BRV)(点击链接按文档依赖)
+!!! success "模块化依赖"
+ 如果自己处理下拉刷新可跳过本章, Net可以仅仅作为简单的网络框架存在
+
+Net可依赖三方库 [BRV](https://github.com/liangjingkanji/BRV) 实现自动处理下拉刷新
-使用固定版本号替换+符号
-
```groovy
-implementation 'com.github.liangjingkanji:BRV:+'
+implementation 'com.github.liangjingkanji:BRV:+' // 使用固定版本号替换+符号
```
-> 当然如果自己处理下拉刷新也是可以的, Net可以仅仅作为网络框架存在
+## PageRefreshLayout
-创建PageRefreshLayout
```xml
```
-创建列表
+## 创建列表
+
```kotlin
rv_push.linear().setup {
addType(R.layout.item_list)
}
```
-创建网络请求
+## 网络请求
+
+1. 请求开始, 显示下拉刷新动画
+2. 请求成功, 显示`内容`缺省页
+3. 请求失败, 显示`错误`缺省页
+
```kotlin hl_lines="2"
page.onRefresh {
scope {
@@ -46,19 +52,9 @@ page.onRefresh {
}.autoRefresh()
```
-
-
-> 注意高亮处使用的是`scope`而不是其他作用域, 只能使用scope, 否则无法跟随PageRefreshLayout生命周期等功能
-
-
-
-- 使用上和自动缺省页相似
-- BRV同样属于具备完善功能独立的RecyclerView框架
-- BRV的下拉刷新扩展自[SmartRefreshLayout_v2](https://github.com/scwang90/SmartRefreshLayout), 支持其全部功能且更多
-
## 生命周期
-|生命周期|描述|
-|-|-|
-|开始|PageRefreshLayout执行`showLoading/autoRefresh`后触发`onRefresh`, 然后开始网络请求|
-|结束|PageRefreshLayout被销毁(例如其所在的Activity或Fragment被销毁), 网络请求自动取消|
\ No newline at end of file
+| 生命周期 | 描述 |
+| -------- | -------------------------------------------------- |
+| 开始 | `showLoading/autoRefresh`触发`onRefresh`, 开始请求 |
+| 结束 | PageRefreshLayout被销毁, 请求自动取消 |
\ No newline at end of file
diff --git a/docs/auto-state.md b/docs/auto-state.md
index 24536c4cc..20c07f870 100644
--- a/docs/auto-state.md
+++ b/docs/auto-state.md
@@ -1,27 +1,22 @@
-考虑到低耦合, 所以自定义缺省页需要导入第三方组件依赖(点击链接按照文档依赖), 当然如果你使用其他方式处理缺省页可以跳过本章.
+!!! success "模块化依赖"
+ 如果自己处理缺省页可跳过本章, Net可以仅仅作为简单的网络框架存在
-依赖以下两种库其中之一即可支持自动显示缺省页
+
+Net可依赖三方库实现自动缺省页, 以下二选一依赖
-1. 依赖 [StateLayout](https://github.com/liangjingkanji/StateLayout)
-
- 使用固定版本号替换+符号
+1. 依赖 [StateLayout](https://github.com/liangjingkanji/StateLayout)
```groovy
- implementation 'com.github.liangjingkanji:StateLayout:+'
+ implementation 'com.github.liangjingkanji:StateLayout:+' // 使用固定版本号替换+符号
```
-1. 依赖 [BRV](https://github.com/liangjingkanji/BRV) (因为BRV包含StateLayout)
-
- 使用固定版本号替换+符号
+1. 依赖 [BRV](https://github.com/liangjingkanji/BRV) (因为BRV包含StateLayout)
```groovy
- implementation 'com.github.liangjingkanji:BRV:+'
+ implementation 'com.github.liangjingkanji:BRV:+' // 使用固定版本号替换+符号
```
-
-初始化缺省页
-
-需要在`Application`里配置全局自定义的 `加载中/加载失败/空数据布局`,可以复制使用Demo里自定义的布局资源,Demo中的`App.kt`配置如下
+## 初始化
+在Application中初始化缺省页
````kotlin
-//全局缺省页配置 [https://github.com/liangjingkanji/StateLayout]
StateConfig.apply {
emptyLayout = R.layout.layout_empty
loadingLayout = R.layout.layout_loading
@@ -29,9 +24,9 @@ StateConfig.apply {
}
````
+## 创建
-
-声明缺省页
+使用`StateLayout`包裹的内容即`内容`(content)
```xml
```
-自动显示缺省页
+## 网络请求
+
+1. 请求开始, 显示`加载中`缺省页
+2. 请求成功, 显示`内容`缺省页
+3. 请求失败, 显示`错误`缺省页
```kotlin
state.onRefresh {
@@ -63,12 +62,10 @@ state.onRefresh {
```
-> 注意高亮处使用的是`scope`而不是其他作用域, 只能使用scope, 否则无法跟随StateLayout生命周期(自动显示对应缺省页)等功能
-
## 生命周期
-|生命周期|描述|
-|-|-|
-|开始|StateLayout执行`showLoading`后触发`onRefresh`, 然后开始网络请求|
-|结束|缺省页被销毁(例如其所在的Activity或Fragment被销毁), 网络请求自动取消|
\ No newline at end of file
+| 生命周期 | 描述 |
+| -------- | ---------------------------------------------- |
+| 开始 | `showLoading`触发`onRefresh`, 开始请求 |
+| 结束 | 缺省页被销毁, 请求自动取消 |
\ No newline at end of file
diff --git a/docs/cache.md b/docs/cache.md
index 919e8fccb..1bfffe954 100644
--- a/docs/cache.md
+++ b/docs/cache.md
@@ -1,4 +1,4 @@
-**网络请求中缓存至关重要, 而Net是最好的**
+### 缓存优势
1. 页面秒开
2. 减少服务器压力
@@ -6,33 +6,35 @@
### Net缓存特点
-1. 缓存任何请求方式, POST/GET/PUT/HEAD...
-2. 缓存任何数据类型, File/图片/JSON/ProtoBuf/...
-3. 限制最大缓存体积, 缓存遵守磁盘LRU缓存算法, 当缓存达到限制时, 将会删除最近最少使用的缓存
-4. 高性能DiskLruCache来实现统一缓存
+1. 缓存任何请求方式
+2. 缓存任何数据, File/图片/JSON/ProtoBuf等
+3. 限制最大缓存空间
+4. 使用`DiskLruCache`实现, 删除最近最少使用
## 配置缓存
-不配置缓存设置是不会触发缓存的
+不配置`Cache`是不会启用缓存的
```kotlin
-NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
- // ...
- // 本框架支持Http缓存协议和强制缓存模式
- cache(Cache(cacheDir, 1024 * 1024 * 128)) // 缓存设置, 当超过maxSize最大值会根据最近最少使用算法清除缓存来限制缓存大小
+NetConfig.initialize(Api.HOST, this) {
+ // Net支持Http缓存协议和强制缓存模式
+ // 当超过maxSize最大值会根据最近最少使用算法清除缓存来限制缓存大小
+ cache(Cache(cacheDir, 1024 * 1024 * 128))
}
```
-这也是Net缓存强大之处, 和OkHttp共享缓存但是却可以缓存任何请求方式
+
+!!! note "判断响应来自缓存"
+ 如果`Response.cacheResponse`不为null的时, 代表Response来自本地缓存
## Http缓存协议
-这属于OkHttp默认的Http缓存协议控制, 要求满足一定条件
+OkHttp默认的Http缓存协议控制, 要求以下条件
-- 请求方式为Get
-- URL的MD5值作为Key, 所以一旦URL发生改变即不会算同一缓存
-- 存在响应头存在缓存控制: [Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
+- Get请求方式
+- 缓存使用Url为key, 因此Url改变会读不到缓存
+- 响应头控制缓存: [Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
-通过指定`CacheControl`也可以控制Http缓存协议(原理是添加请求头)
+通过`setCacheControl`可以控制Http缓存协议(原理是添加请求头)
```kotlin
scopeNetLife {
@@ -44,10 +46,7 @@ scopeNetLife {
}
```
-还可以指定缓存有效期, 更多使用请查看代码或者搜索[Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
-
-
-如果你后端同事的技术水平无法实现Http标准缓存协议, 或者你需要缓存Get以外的请求方法. 下面我们介绍使用`强制缓存模式`来完全由客户端控制缓存
+如果无法实现Http标准缓存协议, 或要缓存Get以外的请求方法, 可以使用`强制缓存模式`来由客户端控制缓存
## 强制缓存模式
@@ -62,22 +61,18 @@ scopeNetLife {
}
```
-读取缓存失败会引发`NoCacheException`异常(被全局错误处理器接收), 前提是你没有使用读取缓存失败后请求网络模式
-
| 强制缓存模式 | 描述 |
|-|-|
-| READ | 只读取缓存, 本操作并不会请求网络故不存在写入缓存 |
+| READ | 只读取缓存, 读不到`NoCacheException` |
| WRITE | 只请求网络, 强制写入缓存 |
| READ_THEN_REQUEST | 先从缓存读取,如果失败再从网络读取, 强制写入缓存 |
| REQUEST_THEN_READ | 先从网络读取,如果失败再从缓存读取, 强制写入缓存 |
-> 如果`response.cacheResponse`不为null的时候即代表response来自于本地缓存, 强制缓存或Http缓存协议都如此
-
## 自定缓存Key
-缓存Key默认是`请求方式+URL`后产生的sha1值(仅强制缓存模式有效), 并不会默认使用请求参数判断
+仅`强制缓存模式`有效, 缓存Key默认是`请求方式+URL`后产生的`sha1值`
-如果你要实现区别请求参数的缓存请自定义缓存key, 如下
+如果要缓存区别请求参数, 请自定义缓存key
```kotlin
scopeNetLife {
@@ -91,8 +86,8 @@ scopeNetLife {
## 缓存有效期
-1. 缓存有效期只针对`强制缓存模式`, 标准Http缓存协议遵守协议本身的有效期
-1. 缓存有效期过期只是让缓存无效, 并不会被删除(即无法读取). 缓存删除遵守LRU(最近最少使用)原则在所有缓存体积达到配置的值时自动删除(即使缓存有效期未到)
+1. 仅`强制缓存模式`有效, 标准Http缓存协议遵守协议本身的有效期
+1. 缓存有效期过期只是让缓存无效, 不会立即删除
根据(最近最少使用)原则在缓存空间达到配置值时删除(即使缓存有效期未到)
```kotlin
scopeNetLife {
@@ -106,7 +101,7 @@ scopeNetLife {
## 预览(缓存+网络)
-这里可以用到Net的预览模式(preview)来实现, 其实不仅仅是预览缓存也可以用于回退请求
+预览又可以理解为回退请求, 一般用于秒开首页或者回退加载数据
```kotlin
scopeNetLife {
@@ -124,9 +119,8 @@ scopeNetLife {
}
```
-> 一般用于秒开首页或者回退加载数据. 我们可以在preview{}只加载缓存. 然后再执行scopeNetLife来请求网络, 做到缓存+网络双重加载的效果
-
-有人可能觉得这和自己加载两次有什么区别, 区别是preview的方法参数可以控制加载
+!!! question "这和加载两次有什么区别?"
+ 区别是`preview`可以控制以下行为
-- `breakError` 读取缓存成功后不再处理错误信息, 默认false
-- `breakLoading` 读取缓存成功后结束加载动画, 默认true
+ 1. `breakError` 读取缓存成功后不再处理错误信息, 默认false
+ 2. `breakLoading` 读取缓存成功后结束加载动画, 默认true
diff --git a/docs/callback.md b/docs/callback.md
index d1ad27e00..88d1c8c5f 100644
--- a/docs/callback.md
+++ b/docs/callback.md
@@ -1,6 +1,7 @@
Net支持OkHttp的原有的队列请求`Callback`
-> Callback属于接口回调请求, 其代码冗余可读性不高, 并且无法支持并发请求协作
+!!! Failure "不推荐"
+ Callback属于接口回调, 其代码冗余, 且无法支持并发请求
```kotlin
diff --git a/docs/cancel.md b/docs/cancel.md
index 8a5b5a23f..4820ef94d 100644
--- a/docs/cancel.md
+++ b/docs/cancel.md
@@ -1,7 +1,4 @@
-Net虽然支持自动跟随生命周期取消网络请求, 绝大部分场景也足够. 但是有时还是需要手动取消, 例如取消下载文件.
-
-
-Net取消协程作用域自动取消内部网络请求, 也支持任意位置取消指定网络请求.
+部分场景需要手动取消请求, 例如取消下载
```kotlin
downloadScope = scopeNetLife {
@@ -11,13 +8,10 @@ downloadScope = scopeNetLife {
downloadScope.cancel() // 取消下载
```
-完整示例: [源码](https://github.com/liangjingkanji/Net/blob/master/sample/src/main/java/com/drake/net/sample/ui/fragment/DownloadFileFragment.kt)
-
## 任意位置取消
-发起请求的时候要求定义一个`Id`用于指定网络请求, 然后在需要的地方使用单例对象`Net.cancelId`取消请求.
+发起请求时指定`Id`
-创建请求
```kotlin
scopeNetLife {
tvFragment.text = Get("api"){
@@ -26,15 +20,25 @@ scopeNetLife {
}
```
-然后根据Id取消网络请求
-```kotlin
-Net.cancelId("请求用户信息")
-
-Net.cancelGroup("请求分组名称") // 设置分组
-```
-
-Group和Id在使用场景上有所区别, 预期上Group允许重复赋值给多个请求, Id仅允许赋值给一个请求, 但实际上都允许重复赋值
-在作用域中发起请求时会默认使用协程错误处理器作为Group: `setGroup(coroutineContext[CoroutineExceptionHandler])`
-如果你覆盖Group会导致协程结束不会自动取消请求
-
-
\ No newline at end of file
+=== "根据ID取消"
+ ``` kotlin
+ Net.cancelId("请求用户信息")
+ ```
+=== "根据Group取消"
+ ``` kotlin
+ Net.cancelGroup("请求分组名称")
+ ```
+
+## Group和Id区别
+
+| 函数 | 描述 |
+|-|-|
+| id | 请求唯一Id, 实际上重复也行, 但是取消请求时遍历到指定Id就会结束遍历 |
+| group | 允许多个请求使用相同group, 在取消请求时会遍历所有分组的请求
|
+
+!!! warning "作用域结束请求自动取消"
+ 在`scopeXX()`作用域中发起请求时会默认使用当前协程错误处理器作为Group
+ ```kotlin
+ setGroup(coroutineContext[CoroutineExceptionHandler])
+ ```
+ 在作用域结束时 会`cancelGroup`, 所以如果你手动指定分组会导致无法自动取消
\ No newline at end of file
diff --git a/docs/config.md b/docs/config.md
index 05a16a6fc..983aaa090 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,124 +1,106 @@
-全局配置建议在Application的onCreate函数中配置
+全局配置应在`Application.onCreate`中配置
## 初始配置
-=== "普通初始化"
+两种方式初始配置, 不初始化也能直接使用
+=== "Net初始化"
```kotlin
- class App : Application() {
- override fun onCreate() {
- super.onCreate()
-
- // https://github.com/liangjingkanji/Net/ 这是接口全局域名, 可以使用NetConfig.host进行单独的修改
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
- // 超时配置, 默认是10秒, 设置太长时间会导致用户等待过久
- connectTimeout(30, TimeUnit.SECONDS)
- readTimeout(30, TimeUnit.SECONDS)
- writeTimeout(30, TimeUnit.SECONDS)
- setDebug(BuildConfig.DEBUG)
- setConverter(SerializationConverter())
- }
- }
+ NetConfig.initialize(Api.HOST, this) {
+ // 超时配置, 默认是10秒, 设置太长时间会导致用户等待过久
+ connectTimeout(30, TimeUnit.SECONDS)
+ readTimeout(30, TimeUnit.SECONDS)
+ writeTimeout(30, TimeUnit.SECONDS)
+ setDebug(BuildConfig.DEBUG)
+ setConverter(SerializationConverter())
}
```
-=== "OkHttpClient.Builder"
-
+=== "OkHttp构造器初始化"
```kotlin
- class App : Application() {
- override fun onCreate() {
- super.onCreate()
- // https://github.com/liangjingkanji/Net/ 这是接口全局域名, 可以使用NetConfig.host进行单独的修改
- val okHttpClientBuilder = OkHttpClient.Builder()
- .setDebug(BuildConfig.DEBUG)
- .setConverter(SerializationConverter())
- .addInterceptor(LogRecordInterceptor(BuildConfig.DEBUG))
-
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this, okHttpClientBuilder)
- }
- }
+ val okHttpClientBuilder = OkHttpClient.Builder()
+ .setDebug(BuildConfig.DEBUG)
+ .setConverter(SerializationConverter())
+ .addInterceptor(LogRecordInterceptor(BuildConfig.DEBUG))
+ NetConfig.initialize(Api.HOST, this, okHttpClientBuilder)
```
-> 配置都是可选项, 不是不初始化就不能使用. 如果你是Xposed或者多进程项目中必须初始化传入上下文或者赋值 `NetConfig.app = this`
-
-在initNet函数作用域中的this属于`OkHttpClient.Builder()`, 可以配置任何OkHttpClient.Builder的属性以外还支持以下Net独有配置
+!!! failure "强制初始化"
+ 如果是多进程项目(例如Xposed)必须初始化, 因为多进程无法自动指定Context
-| 函数 | 描述 |
+| 可配置选项 | 描述 |
|-|-|
-| setDebug | 是否输出网络日志, 和`LogRecordInterceptor`互不影响 |
+| setDebug | 开启日志 |
| setSSLCertificate | 配置Https证书 |
| trustSSLCertificate | 信任所有Https证书 |
-| setConverter | [配置数据转换器](converter.md), 将网络返回的数据转换成你想要的数据结构 |
-| setRequestInterceptor | [配置请求拦截器](interceptor.md), 适用于添加全局请求头/参数 |
-| setErrorHandler | [配置全局错误处理](error-global.md) |
-| setDialogFactory | [配置全局对话框](auto-dialog.md) |
+| setConverter | [转换器](converter-customize.md), 将请求结果转为任何类型 |
+| setRequestInterceptor | [请求拦截器](interceptor.md), 全局请求头/请求参数 |
+| setErrorHandler | [全局错误处理](error-global.md) |
+| setDialogFactory | [全局对话框](auto-dialog.md) |
+
+!!! success "修改配置"
+ NetConfig存储所有全局配置变量, 可以后续修改, 且大部分支持单例指定配置
## 重试次数
-默认情况下你设置超时时间即可, OkHttp内部也有重试机制.
+可以添加`RetryInterceptor`拦截器即可实现失败以后会重试指定次数
-但是个别开发者需求指定重试次数则可以添加`RetryInterceptor`拦截器即可实现失败以后会重试指定次数
+默认情况下设置超时时间即可, OkHttp内部也有重试机制
```kotlin
-NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
+NetConfig.initialize(Api.HOST, this) {
// ... 其他配置
addInterceptor(RetryInterceptor(3)) // 如果全部失败会重试三次
}
```
+!!! warning "长时间阻碍用户交互"
+ OkHttp内部也有重试机制, 如果还添加重试拦截器可能导致请求时间过长, 长时间阻碍用户交互
-## 修改配置
-
-[NetConfig](api/-net/com.drake.net/-net-config/index.html)的字段即为全局配置, 可随时修改
-
-```kotlin
-NetConfig.converter = MyNewConverter() // 修改全局数据转换器
-NetConfig.okHttpClient // 修改全局默认客户端
-```
-
-更多请访问源码查看
-
-## BaseUrl
-
-这个概念来源于Retrofit, 因为Retrofit无法动态修改Host, 但是这个Net支持随时修改. 以下介绍三种修改方式
-
-1) 直接修改
+## 多域名
-```kotlin
-NetConfig.host = "新的BaseUrl"
-```
+概念源于`Retrofit`(称为BaseUrl), 因为Retrofit无法二次修改请求Host, 但Net支持随时修改
+以下介绍三种修改方式
-2) 传入路径
-如传入参数为路径(例如`/api/index`)会自动和`host`拼接组成完成URL, 但如果传入的以`http/https`开头的全路径则会直接作为请求URL
+=== "修改Host"
+ ```kotlin
+ NetConfig.host = Api.HOST_2
+ ```
-```kotlin
-scopeNetLife {
- val data = Get("https://github.com/liangjingkanji/Net/").await()
-}
-```
+=== "指定全路径"
+ 指定Path(例如`/api/index`)会自动和`NetConfig.host`拼接组成Url, 但指定以`http/https`开头的全路径则直接作为请求Url
+ ```kotlin
+ scopeNetLife {
+ val data = Get("https://github.com/path").await()
+ }
+ ```
-3) 使用拦截器
+=== "使用拦截器"
+ 请求时指定`tag`, 然后拦截器中根据tag判断修改host, 拦截器能修改所有请求/响应信息
-或者通过指定`tag`, 然后拦截器(interceptor)中根据tag动态修改host, 因为拦截器能修改一切请求参数
+ ```kotlin
+ scopeNetLife {
+ val data = Get("/api/index", "User").await() // User即为tag
+ }
+ // 拦截器修改请求URL不做介绍
+ ```
-```kotlin
-scopeNetLife {
- val data = Get("/api/index", "User").await() // User即为tag
-}
-// 拦截器修改请求URL不做介绍
-```
+## 网络安全配置
-## 多域名
+Net自动启用网络配置文件, 默认支持Http请求, 可自定义
-```kotlin
-scopeNetLife {
- Get("https://github.com/liangjingkanji/Net/").await() // 传入域名就会使用当前域名
- Get("/liangjingkanji/Net/").await() // 自动和NetConfig.host拼接
- Get(Api.Host2 + "/liangjingkanji/Net/").await() // 自己手动拼接
-}
+```xml title="network_security_config.xml"
+
+
+
```
-和BaseUrl一样你还可以在拦截器里面统一处理, 在拦截器里面判断tag或者path来拼接域名
-
+!!! failure "无法打包Apk"
+ 当开发者自定义使用非同名`network_security_config`时网络配置文件时会无法打包Apk
+ 请添加`tools:replace`
+ ```kotlin title="AndroidManifest.xml" hl_lines="3"
+
+ ```
diff --git a/docs/convert-special.md b/docs/convert-special.md
deleted file mode 100644
index 6fe9fdcc4..000000000
--- a/docs/convert-special.md
+++ /dev/null
@@ -1,129 +0,0 @@
-
-## 解析完整Json
-
-一般的解析过程是以下
-
-1. 后端返回的JSON数据
-
-```json
-{
- "code":0,
- "msg":"请求成功",
- "data": {
- "name": "彭于晏",
- "age": 27,
- "height": 180
- }
-}
-```
-
-2. 创建数据模型
-
-```kotlin
-data class UserModel (
- var code:Int,
- var msg:String,
- var data:Data,
-) {
- data class Data(var name: String, var age: Int, var height: Int)
-}
-```
-
-3. 发起网络请求
-
-```kotlin
-scopeNetLife {
- val data = Get("api").await().data
-}
-```
-
-## 解析Json中的字段
-
-这样每次都要`await().data`才是你要的`data`对象. 有些人就想省略直接不写code和msg, 希望直接返回data. 那么在转换器里面就只解析data字段即可
-
-简化数据对象
-
-```kotlin
-data class UserModel(var name: String, var age: Int, var height: Int)
-```
-
-转换器只解析data字段
-
-```kotlin
-class GsonConvert : JSONConvert(code = "code", message = "msg", success = "200") {
- private val gson = GsonBuilder().serializeNulls().create()
-
- override fun String.parseBody(succeed: Type): S? {
- val data = JSONObject(this).getString("data")
- return gson.fromJson(data, succeed)
- }
-}
-```
-
-使用简化的数据对象作为泛型
-
-```kotlin
-scopeNetLife {
- val data = Get("api").await()
-}
-```
-
-## 解析Json数组
-
-在Net中可以直接解析List等嵌套泛型数据, 解析List和普通对象没有区别
-
-```kotlin
-scopeNetLife {
- tvFragment.text = Get>("list") {
- converter = GsonConverter() // 单例转换器, 一般情况下是定义一个全局转换器
- }.await()[0].name
-}
-```
-
-## 解析泛型数据类
-
-这种方式在Retrofit中经常被使用到, 因为有些人认为code/msg也要使用. 实际上一般非成功错误码(例如200或者0)算业务定义错误应当在转换器抛出异常, 然后在错误处理回调中取获取错误码/信息
- ```kotlin
- // 数据对象的基类
- open class BaseResult {
- var code: Int = 0
- var msg: String = ""
- var data: T? = null
- }
-
- class Result(var name: String) : BaseResult()
- ```
-
-如果你成功错误码要求定义多个都算网络请求成功, 也是可以的并且不需要写泛型这么麻烦, Net转换器可以实现无论加不加`code/msg`都能正常解析返回
-
-```kotlin
-@kotlinx.serialization.Serializable
-class Result(var data: String = "数据", var msg: String = "", var code:Int = 0)
-```
-
-```kotlin
-@kotlinx.serialization.Serializable
-class Result(var data: String = "数据")
-```
-
-查看源码`SerializationConverter`可以看到转换器内进行了回退解析策略, 当截取`data`解析失败后会完整解析整个Json
-
-```kotlin hl_lines="15"
-code in 200..299 -> { // 请求成功
- val bodyString = response.body?.string() ?: return null
- val kType = response.request.kType
- ?: throw ConvertException(response, "Request does not contain KType")
- return try {
- val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息
- val srvCode = json.getString(this.code)
- if (srvCode == success) { // 对比后端自定义错误码
- json.getString("data").parseBody(kType)
- } else { // 错误码匹配失败, 开始写入错误异常
- val errorMessage = json.optString(message, NetConfig.app.getString(com.drake.net.R.string.no_error_message))
- throw ResponseException(response, errorMessage, tag = srvCode) // 将业务错误码作为tag传递
- }
- } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON
- bodyString.parseBody(kType)
- }
-}
-```
\ No newline at end of file
diff --git a/docs/converter-customize.md b/docs/converter-customize.md
new file mode 100644
index 000000000..ceec1c059
--- /dev/null
+++ b/docs/converter-customize.md
@@ -0,0 +1,81 @@
+Net自定义转换器可支持任何数据类型, 甚至`Bitmap`
+
+!!! failure "泛型和转换器关系"
+ 1. 如果`Post`, 那么`NetConverter.onConvert`返回值必须为Model
+ 2. 如果`Post`, 允许`NetConverter.onConvert`返回值为null
+ 3. 其他情况请抛出异常
+
+```kotlin
+scopeNetLife {
+ val userList = Get>("list") {
+ converter = GsonConverter()
+ }.await()
+}
+```
+
+Net由于低耦合原则不自带任何序列化框架
+
+## 设置转换器
+
+=== "全局"
+ ```kotlin hl_lines="2"
+ NetConfig.initialize(Api.HOST, this) {
+ setConverter(SerializationConverter())
+ }
+ ```
+=== "单例"
+ ```kotlin hl_lines="3"
+ scopeNetLife {
+ tvFragment.text = Get("api"){
+ converter = SerializationConverter()
+ }.await()
+ }
+ ```
+
+## 常见转换器
+
+实现[JSONConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/JSONConvert.kt)的`parseBody`方法使用自定义序列化框架解析
+
+| 序列化框架 | 示例代码 | 描述 |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------- |
+| [kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization) | [SerializationConverter](https://github.com/liangjingkanji/Net/blob/HEAD/sample/src/main/java/com/drake/net/sample/converter/SerializationConverter.kt) | Kotlin官方序列化框架 |
+| [kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization) | [ProtobufConverter](https://github.com/liangjingkanji/Net/blob/HEAD/sample/src/main/java/com/drake/net/sample/converter/ProtobufConverter.kt) | Kotlin官方序列化框架 |
+| [gson](https://github.com/google/gson) | [GsonConverter](https://github.com/liangjingkanji/Net/blob/HEAD/sample/src/main/java/com/drake/net/sample/converter/GsonConverter.kt) | 谷歌序列化框架 |
+| [fastJson](https://github.com/alibaba/fastjson) | [FastJsonConverter](https://github.com/liangjingkanji/Net/blob/HEAD/sample/src/main/java/com/drake/net/sample/converter/FastJsonConverter.kt) | 阿里巴巴序列化框架 |
+| [moshi](https://github.com/square/moshi) | [MoshiConverter](https://github.com/liangjingkanji/Net/blob/HEAD/sample/src/main/java/com/drake/net/sample/converter/MoshiConverter.kt) | Square序列化框架 |
+
+## 自定义转换器
+
+实现`NetConverter`返回自定义请求结果
+
+??? example "转换器实现非常简单"
+ ```kotlin title="NetConverter.kt" linenums="1"
+ interface NetConverter {
+
+ @Throws(Throwable::class)
+ fun onConvert(succeed: Type, response: Response): R?
+
+ companion object DEFAULT : NetConverter {
+ /**
+ * 返回结果应当等于泛型对象, 可空
+ * @param succeed 请求要求返回的泛型类型
+ * @param response 请求响应对象
+ */
+ override fun onConvert(succeed: Type, response: Response): R? {
+ return when {
+ succeed === String::class.java && response.isSuccessful -> response.body?.string() as R
+ succeed === ByteString::class.java && response.isSuccessful -> response.body?.byteString() as R
+ succeed is GenericArrayType && succeed.genericComponentType === Byte::class.java && response.isSuccessful -> response.body?.bytes() as R
+ succeed === File::class.java && response.isSuccessful -> response.file() as R
+ succeed === Response::class.java -> response as R
+ else -> throw ConvertException(response, "An exception occurred while converting the NetConverter.DEFAULT")
+ }
+ }
+ }
+ }
+ ```
+
+转换器中可以根据需加上解密数据, token失效跳转登录, 限制多端登录等逻辑
+
+1. 日志信息输出, 请阅读[日志记录器](log-recorder.md)
+2. 转换器中抛出异常被全局错误处理捕获, 请阅读[全局错误处理](error-handle.md)
\ No newline at end of file
diff --git a/docs/converter-struct.md b/docs/converter-struct.md
new file mode 100644
index 000000000..775a0a123
--- /dev/null
+++ b/docs/converter-struct.md
@@ -0,0 +1,150 @@
+
+上一章节介绍如何序列化框架解析JSON, 而本章是介绍如何定义映射数据类
+
+## JSON
+
+解析接口返回的完整JSON
+
+=== "JSON"
+ ```json
+ {
+ "code":0,
+ "msg":"请求成功",
+ "data": {
+ "name": "彭于晏",
+ "age": 27,
+ "height": 180
+ }
+ }
+ ```
+
+=== "数据类"
+ ```kotlin
+ data class UserModel (
+ var code:Int,
+ var msg:String,
+ var data:Data,
+ ) {
+ data class Data(var name: String, var age: Int, var height: Int)
+ }
+ ```
+
+=== "网络请求"
+ ```kotlin
+ scopeNetLife {
+ val data = Get("api").await().data
+ }
+ ```
+
+??? warning "以上设计不合理"
+ 正常情况下Http状态码200时只返回有效数据
+ ```json
+ {
+ "name": "彭于晏",
+ "age": 27,
+ "height": 180
+ }
+ ```
+ 任何非正常流程返回200状态码, 例如400(错误请求)/401(认证失败)
+ ```kotlin
+ {
+ "code":412302,
+ "msg":"密码错误",
+ }
+ ```
+ 只要认为需要解析结构体情况下, 都应属于正常流程
+
+## 剔除无效字段
+
+以下演示仅解析`data`字段返回有效数据
+
+此数据类只需要包含data值
+
+```kotlin
+data class UserModel(var name: String, var age: Int, var height: Int)
+```
+
+转换器只解析data字段
+
+```kotlin
+class GsonConvert : JSONConvert(code = "code", message = "msg", success = "200") {
+ private val gson = GsonBuilder().serializeNulls().create()
+
+ override fun String.parseBody(succeed: Type): S? {
+ val data = JSONObject(this).getString("data")
+ return gson.fromJson(data, succeed)
+ }
+}
+```
+
+请求直接返回
+
+```kotlin
+scopeNetLife {
+ val data = Get("api").await()
+}
+```
+
+## 不规范数据
+
+推荐在转换器中解析之前处理好数据
+
+1. 字段值为`"null"`而不是`null`, 或者json在字符串中
+ ```json
+ {
+ "data": "{ "title": "name" }"
+ "msg": "null"
+ }
+ ```
+ ```kotlin title="替换为规范内容"
+ json = bodyString.replace("\"{", "{")
+ json = bodyString.replace("}\"", "}")
+ json = bodyString.replace("\"null\"", "null")
+ ```
+
+2. 服务器成功时不返回数据或者返回`null`
+ ```kotlin
+ if (response.body == null || bodyString == "null") {
+ "{}".bodyString.parseBody(succeed)
+ }
+ ```
+
+3. 字段值为null, 使用 [kotlin-serialization](kotlin-serialization.md) 自动使用字段默认值
+ ```kotlin
+ {
+ "msg": null
+ }
+ ```
+4. 字段无引号或字段名为数字, 使用 [kotlin-serialization](kotlin-serialization.md) 可以禁用JSON规范限制
+ ```json title="数字使用map解析"
+ {
+ "data": {
+ 23: 32
+ }
+ }
+ ```
+ ```kotlin hl_lines="3" title="禁用JSON规范限制"
+ val jsonDecoder = Json {
+ // ...
+ isLenient = true
+ }
+ ```
+
+
+## 泛型数据类
+
+某些地区很多开发者习惯这么使用, 因为他们接口返回无关字段, 但是技术不够无法自定义转换器来简化取值
+
+所以他们选择更复杂的方式: 使用泛型来简化
+
+```kotlin
+open class BaseResult {
+ var code: Int = 0
+ var msg: String = ""
+ var data: T? = null
+}
+
+class Result(var name: String) : BaseResult()
+```
+
+!!! quote "用加法解决问题的人,总有人愿意用乘法给你答案"
\ No newline at end of file
diff --git a/docs/converter.md b/docs/converter.md
index d8d7f1fc3..aeda64d48 100644
--- a/docs/converter.md
+++ b/docs/converter.md
@@ -1,36 +1,16 @@
-Net可以通过自定义转换器支持任何数据类型转换, 甚至`List>`等嵌套泛型对象
+Net支持请求返回的数据类型取决于你的转换器实现
-例如请求动作Post`指定泛型为Model`, 则转换器NetConverter中的函数`onConvert返回值必须为Model`, 如果转换失败或者发生异常都算请求错误
+# Get<任何对象>("path").await()
-> 注意如果你在转换器里面返回null. 那么指定的泛型也应当是可空类型, 例如`Post("api")`
-
-```kotlin
-scopeNetLife {
- val userList = Get>("list") {
- converter = GsonConverter()
- }.await()
-}
-```
-
-
-
-如果你要返回映射好的数据模型对象, 那么肯定要求创建转换器的. 本框架由于低耦合原则不自带解析框架
-
-[常用的Json转换器-代码示例](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/converter)
-
-
-
-## 默认返回数据类型
-
-Net支持请求返回的数据类型取决于你的转换器(也就是支持返回任何对象), 默认情况不创建转换器也支持返回以下数据类型
+默认转换器支持返回以下数据类型
| 函数 | 描述 |
|-|-|
| String | 字符串 |
| ByteArray | 字节数组 |
-| ByteString | 内部定义的一种字符串对象 |
-| Response | 最基础的响应 |
+| ByteString | 更多功能的字符串对象 |
| File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) |
+| Response | 所有响应信息(响应体/响应头/请求信息等) |
使用示例
@@ -40,238 +20,31 @@ scopeNetLife {
}
```
-??? summary "默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)"
- ```kotlin
- val DEFAULT = object : NetConverter {
-
- override fun onConvert(
- succeed: Type,
- response: Response
- ): R? {
- return when (succeed) {
- String::class.java -> response.body?.string() as R
- ByteString::class.java -> response.body?.byteString() as R
- ByteArray::class.java -> response.body?.bytes() as R
- Response::class.java -> response as R
- File::class.java -> response.file() as R
- else -> throw ConvertException(
- response,
- "The default converter does not support this type"
- )
- }
- }
- }
- ```
-
-假设这里没有你需要的数据类型请[自定义转换器](#_3)(例如返回Json或Protocol)
-
-## 设置转换器
-转换器分为全局和单例, 单例可以覆盖全局的转换器. 如果不设置转换器就会采用默认的转换器
-
-=== "全局"
- ```kotlin hl_lines="2"
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
- setConverter(SerializationConverter())
- }
- ```
-=== "单例"
- ```kotlin hl_lines="3"
- scopeNetLife {
- tvFragment.text = Get("api"){
- converter = SerializationConverter()
- }.await()
- }
- ```
-
-## Json解析库转换器
-
-一般业务我们可以直接继承[JSONConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/JSONConvert.kt)
-使用自己的JSON解析器解析数据, 完全自定义需求可以直接实现[NetConverter](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)(比如直接转换IO流)
-
-=== "Gson"
-
- ```kotlin
- class GsonConvert : JSONConvert(code = "code", message = "msg", success = "200") {
- val gson = GsonBuilder().serializeNulls().create()
-
- override fun String.parseBody(succeed: Type): S? {
- return gson.fromJson(this, succeed)
- }
- }
- ```
-
-=== "kotlin-serialization"
-
- ```kotlin
- class SerializationConverter(
- val success: String = "0",
- val code: String = "code",
- val message: String = "msg"
- ) : NetConverter {
-
- private val jsonDecoder = Json {
- ignoreUnknownKeys = true // JSON和数据模型字段可以不匹配
- coerceInputValues = true // 如果JSON字段是Null则使用默认值
- }
-
- override fun onConvert(succeed: Type, response: Response): R? {
- try {
- // 此处是为了继承默认转换器支持的返回类型
- return NetConverter.onConvert(succeed, response)
- } catch (e: ConvertException) {
- val code = response.code
- when {
- code in 200..299 -> { // 请求成功
- val bodyString = response.body?.string() ?: return null
- val kType = response.request.kType() ?: return null
- return try {
- val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息
- if (json.getString(this.code) == success) { // 对比后端自定义错误码
- bodyString.parseBody(kType)
- } else { // 错误码匹配失败, 开始写入错误异常
- val errorMessage = json.optString(
- message,
- NetConfig.app.getString(com.drake.net.R.string.no_error_message)
- )
- throw ResponseException(response, errorMessage)
- }
- } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON
- bodyString.parseBody(kType)
- }
- }
- code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误
- code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误
- else -> throw ConvertException(response)
- }
- }
- }
-
- fun String.parseBody(succeed: KType): R? {
- return jsonDecoder.decodeFromString(Json.serializersModule.serializer(succeed), this) as R
- }
- }
- ```
-
- SerializationConverter就是仿照JSONConverter代码实现
-
-=== "FastJson"
-
- ```kotlin
- class FastJsonConvert : JSONConvert(code = "code", message = "msg", success = "200") {
-
- override fun String.parseBody(succeed: Type): S? {
- return JSON.parseObject(this, succeed)
- }
- }
- ```
-
-=== "Moshi"
-
- ```kotlin
- class MoshiConvert : JSONConvert(code = "code", message = "msg", success = "200") {
- val moshi = Moshi.Builder().build()
-
- override fun String.parseBody(succeed: Type): S? {
- return moshi.adapter(succeed).fromJson(this)
- }
- }
- ```
-
-1. 使用转换器时请添加其依赖: [GSON](https://github.com/google/gson) | [kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization) | [FastJson](https://github.com/alibaba/fastjson) | [Moshi](https://github.com/square/moshi)
-2. 推荐使用 `kotlinx.Serialization`, 其可解析[任何泛型](kotlin-serialization.md)
-3. Sample有完整代码示例
-
-以上转换器示例是建立在数据结构为以下表格的固定格式下, 如果有特殊的业务可能需要自行修改
-
-| 转换器参数 | 描述 |
-|-|-|
-| code | 即后端定义的`成功码`字段名 |
-| message | 即后端定义的`错误消息`字段名 |
-| success | 即`成功码`的值等于指定时才算网络请求成功 |
-
-
-
-比如截图中的意为, 当返回的Json中包含state字段且值为ok时请求才算是真正成功才会返回数据, 否则都会抛出异常. 其中message为错误信息字段名
-
-假设简单的指定名称不能满足你复杂的业务逻辑, 请复制`JSONConvert`源码到你项目中修改或者直接自己实现`NetConverter`
-
-> 注意解析器(Gson或者Moshi)的解析对象记得定义为类成员, 这样可以不会导致每次解析都要创建一个新的解析对象, 减少内存消耗
-
-
-## 自定义转换器
-
-通过实现`NetConverter`接口可以编写自己的逻辑网络请求返回的数据, `NetConvert.DEFAULT`为默认的转换器支持返回File/String/Response等
-
-
-
-框架中自带一个`JSONConverter`可以作为参考或者直接使用. 其可以转换JSON数据.
-
-??? summary "JSONConverter 源码"
- ```kotlin
- /**
- * 常见的JSON转换器实现, 如果不满意继承实现自定义的业务逻辑
- *
- * @param success 后端定义为成功状态的错误码值
- * @param code 错误码在JSON中的字段名
- * @param message 错误信息在JSON中的字段名
- */
- abstract class JSONConvert(
- val success: String = "0",
- val code: String = "code",
- val message: String = "msg"
- ) : NetConverter {
-
- override fun onConvert(succeed: Type, response: Response): R? {
- try {
- // 此处是为了继承默认转换器支持的返回类型
- return NetConverter.onConvert(succeed, response)
- } catch (e: ConvertException) {
- val code = response.code
- when {
- code in 200..299 -> { // 请求成功
- val bodyString = response.body?.string() ?: return null
- return try {
- val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息
- if (json.getString(this.code) == success) { // 对比后端自定义错误码
- bodyString.parseBody(succeed)
- } else { // 错误码匹配失败, 开始写入错误异常
- val errorMessage = json.optString(
- message,
- NetConfig.app.getString(com.drake.net.R.string.no_error_message)
- )
- throw ResponseException(response, errorMessage)
- }
- } catch (e: JSONException) { // 固定格式JSON分析失败直接解析JSON
- bodyString.parseBody(succeed)
- }
- }
- code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误
- code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误
- else -> throw ConvertException(response)
+??? example "转换器实现非常简单"
+ ```kotlin title="NetConverter.kt" linenums="1"
+ interface NetConverter {
+
+ @Throws(Throwable::class)
+ fun onConvert(succeed: Type, response: Response): R?
+
+ companion object DEFAULT : NetConverter {
+ /**
+ * 返回结果应当等于泛型对象, 可空
+ * @param succeed 请求要求返回的泛型类型
+ * @param response 请求响应对象
+ */
+ override fun onConvert(succeed: Type, response: Response): R? {
+ return when {
+ succeed === String::class.java && response.isSuccessful -> response.body?.string() as R
+ succeed === ByteString::class.java && response.isSuccessful -> response.body?.byteString() as R
+ succeed is GenericArrayType && succeed.genericComponentType === Byte::class.java && response.isSuccessful -> response.body?.bytes() as R
+ succeed === File::class.java && response.isSuccessful -> response.file() as R
+ succeed === Response::class.java -> response as R
+ else -> throw ConvertException(response, "An exception occurred while converting the NetConverter.DEFAULT")
}
}
}
-
- /**
- * 反序列化JSON
- *
- * @param succeed JSON对象的类型
- * @receiver 原始字符串
- */
- abstract fun String.parseBody(succeed: Type): R?
}
-
```
-JSONConvert的核心逻辑
-
-1. 判断服务器的错误码
-1. 判断后端自定义的错误码
-1. 如果判断发生错误则抛出一个包含错误信息的异常
-1. 如果都判断成功则开始解析数据并return数据对象
-
-在转换器中根据需要你可以在这里加上常见的解密数据, token失效跳转登录, 限制多端登录等逻辑. 日志信息输出请阅读: [日志记录器](log-recorder.md)
-
-如果是错误信息建议抛出异常, 就可以在全局异常处理器中统一处理, 请阅读:[全局错误处理](error-handle.md)
-
-
\ No newline at end of file
+假设不支持你需要的数据类型, 例如JSON/ProtoBuf/Bitmap等请[自定义转换器](converter-customize.md#_3)
\ No newline at end of file
diff --git a/docs/cookie.md b/docs/cookie.md
index 40483a027..676d4602d 100644
--- a/docs/cookie.md
+++ b/docs/cookie.md
@@ -1,15 +1,15 @@
-Net使用的是OkHttp的Cookie管理方案(CookieJar), 并且提供持久化存储的Cookie管理实现(PersistentCookieJar)
+使用OkHttp的`CookieJar`, Net提供持久化Cookie`PersistentCookieJar`
```kotlin
-NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
+NetConfig.initialize(Api.HOST, this) {
// 添加持久化Cookie
cookieJar(PersistentCookieJar(this@App))
}
```
-PersistentCookieJar可以手动增删Cookie
+可以手动增删Cookie
-| 函数 | 描述 |
+| PersistentCookieJar | 描述 |
|-|-|
| addAll | 添加Cookie |
| getAll | 获取某个域名的所有Cookie |
@@ -17,10 +17,10 @@ PersistentCookieJar可以手动增删Cookie
| clear | 删除客户端全部Cookie |
-你可以通过客户端可以获取到已设置的cookieJar
+可通过客户端获取到已配置cookieJar
```kotlin
(NetConfig.okHttpClient.cookieJar as? PersistentCookieJar)?.clear()
```
-
-PersistentCookieJar使用数据库实现Cookies存储, 你可以指定`dbName`来创建不同的数据库让不同的客户端隔绝Cookie共享
+!!! note "隔绝Cookies共享"
+ 为`PersistentCookieJar`指定不同`dbName`阻止不同的客户端共享Cookies
\ No newline at end of file
diff --git a/docs/coroutine-request.md b/docs/coroutine-request.md
index befff75b1..43118885b 100644
--- a/docs/coroutine-request.md
+++ b/docs/coroutine-request.md
@@ -1,12 +1,6 @@
-Net在2.0开始引入协程来支持并发和异步, 虽然很多网络框架支持协程, 但是Net对于协程生命周期的控制算得上是独有.
-并且Net不仅仅网络请求, 其也支持创建任何异步任务.
+Net的协程作用域会自动处理协程生命周期
-> 这里的`同时/并发/并行`统称为并发(具体是不是并行不需要开发者来考虑)
-
-
-在上章节已经使用过了网络的并发请求
-
-这里再演示同时(并发)请求百度网站`一万次`并且一次取消
+在上章节已经介绍如何发起并发网络请求, 这里演示同时(并发)请求`一万次`, 然后取消全部
```kotlin
val job = scopeNetLife {
@@ -28,9 +22,10 @@ thread {
}
```
+
-Net主要推荐使用的是协程请求, 但是同时支持其他方式发起请求
+Net主要使用协程请求, 但也支持其他方式发起请求
-- 协程请求
- [同步请求](sync-request.md)
-- [回调请求](callback.md)
\ No newline at end of file
+- [回调请求](callback.md)
+- 协程请求
diff --git a/docs/css/extra.css b/docs/css/extra.css
index ab80fff67..a3ccf56a6 100644
--- a/docs/css/extra.css
+++ b/docs/css/extra.css
@@ -1,69 +1,31 @@
-:root > * {
- --md-code-fg-color: #A9B7C6;
- --md-code-bg-color: #2b2b2b;
- --md-code-hl-color: #214283;
- --md-code-hl-number-color: #82AAFF;
- --md-code-hl-special-color: #A9B7C6;
- --md-code-hl-function-color: #FFE64C;
- --md-code-hl-constant-color: hsla(250, 70%, 64%, 1);
- --md-code-hl-keyword-color: #CC7832;
- --md-code-hl-string-color: #6A8759;
- --md-code-hl-name-color: var(--md-code-fg-color);
- --md-code-hl-operator-color: #A9B7C6;
- --md-code-hl-punctuation-color: #A9B7C6;
- --md-code-hl-comment-color: #787878;
- --md-code-hl-generic-color: #A9B7C6;
- --md-code-hl-variable-color: #A9B7C6;
-
- --md-typeset-color: #333333;
- --drake-highlight: #d63200;
- --drake-accent: #e95f59;
- --drake-highlight-opacity: #d6320022;
- --md-admonition-fg-color: #333333;
- --drake-font-size: 13px;
-}
-
-[data-md-color-scheme="drake"] {
- --md-primary-fg-color: hsla(0, 0%, 100%, 1);
- --md-primary-fg-color--light: hsla(0, 0%, 100%, 0.7);
- --md-primary-fg-color--dark: hsla(0, 0%, 0%, 0.07);
- --md-primary-bg-color: hsla(0, 0%, 0%, 0.87);
- --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54);
- --md-accent-fg-color: #d63200;
- --md-accent-fg-color--light: #d63200;
- --md-accent-fg-color--dark: #d63200;
- --md-typeset-a-color: #d63200 !important;
-}
-
-/*字体渲染*/
@font-face{
- font-family: 'JetBrains Mono';
- src: local('JetBrainsMono-Regular'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Regular.woff2') format('woff2');
+ font-family: 'Iosevka Curly';
+ src: local('Iosevka Curly Medium'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/iosevka-curly/iosevka-curly-medium.woff2') format('woff2');
font-display: swap;
font-weight: normal;
font-style: normal;
}
@font-face{
- font-family: 'JetBrains Mono';
- src: local('JetBrainsMono-Bold'),
- url('https://raw.githubusercontent.com/JetBrains/JetBrainsMono/master/fonts/webfonts/JetBrainsMono-Bold.woff2') format('woff2');
+ font-family: 'Iosevka Curly';
+ src: local('Iosevka Curly Bold'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/iosevka-curly/iosevka-curly-bold.woff2') format('woff2');
font-display: swap;
font-weight: bold;
font-style: normal;
}
@font-face{
- font-family: 'HYZhengYuan';
- src: local('HYZhengYuan-55W'),
- url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYZhengYuan.ttf') format('truetype');
+ font-family: 'HYYouYuan';
+ src: local('HYYouYuan-55W'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYYouYuan/HYYouYuan-55W.ttf') format('truetype');
font-display: swap;
font-weight: normal;
font-style: normal;
}
@font-face{
- font-family: 'HYZhengYuan';
- src: local('HYZhengYuan-75W'),
- url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYZhengYuan-75W.ttf') format('truetype');
+ font-family: 'HYYouYuan';
+ src: local('HYYouYuan-75W'),
+ url('https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/font/HYYouYuan/HYYouYuan-75W.ttf') format('truetype');
font-display: swap;
font-weight: bold;
font-style: normal;
@@ -74,241 +36,18 @@
-webkit-font-smoothing: subpixel-antialiased;
-moz-osx-font-smoothing: auto;
text-rendering: optimizeLegibility;
- font-family: "JetBrains Mono", HYZhengYuan, monospace !important;
-}
-
-/*布局*/
-.md-content {
- max-width: 49.5rem;
-}
-.md-grid {
- max-width: 80rem;
-}
-
-/*表格*/
-.md-typeset__table {
- display: block;
- padding: 0 .8rem;
- margin: 1em 0;
-}
-table tr:nth-child(2n), thead {
- background-color: #fafafa;
-}
-.md-typeset table:not([class]) {
- border-collapse: collapse;
- border-spacing: 0px;
- width: 100%;
- break-inside: auto;
- text-align: left;
- display: table;
- box-shadow:none;
- font-size: var(--drake-font-size);
-}
-
-.md-typeset table:not([class]) th {
- border: 1px solid #dfe2e5;
- background-color: #f2f2f2;
- padding: 6px 13px;
- font-weight: bold;
- color: var(--md-typeset-color);
-}
-.md-typeset table:not([class]) td {
- border: 1px solid #dfe2e5;
-}
-
-/*隐藏搜索框, 因为不支持中文搜索*/
-.md-search__form {
- visibility: hidden;
+ font-family: "Iosevka Curly", HYYouYuan !important;
}
-/*引用*/
-.md-typeset :is(.admonition,details) {
- font-size: 12px;
-}
-[dir=ltr] .md-typeset blockquote {
- border-left:none;
-}
-.md-typeset blockquote {
- color: inherit;
- padding: 10px 16px;
- background-color: #fdefee;
- position: relative;
- border-left: none;
- margin: 2em 0;
-}
-.md-typeset blockquote p {
- margin: 0 0 !important;
-}
-.md-typeset blockquote:before {
- display: block;
- position: absolute;
- content: '';
- width: 4px;
- left: 0;
- top: 0;
- height: 100%;
- background-color:var(--drake-accent);
- border-radius: 4px;
-}
-
-/*字间距*/
+code,
+.md-nav,
+.md-typeset code,
.md-typeset {
- line-height: 1.8;
- font-size: var(--drake-font-size);
-}
-.md-typeset pre {
- line-height: 1.6;
-}
-
-/*标签*/
-.md-typeset .tabbed-set>label {
- border-bottom: 2px solid transparent;
- color: var(--md-typeset-color);
- line-height: 1.3;
- font-size: var(--drake-font-size);
- margin-bottom: .8em;
- font-weight:normal;
-}
-.md-typeset .tabbed-set>input:checked+label {
- font-weight:500;
- line-height: 1.3;
- margin-bottom: .8em;
-}
-
-/*侧边导航*/
-.md-nav__item .md-nav__link--active {
- color: var(--drake-highlight);
- font-weight:500;
-}
-.md-nav__title[for="__drawer"] {
- display: none;
-}
-div .md-source__fact {
- display: none;
-}
-.md-source__icon+.md-source__repository {
- margin-left: -1em;
- padding-left: 0;
- font-weight: 500;
-}
-.md-nav__link {
- font-size: var(--drake-font-size);
- line-height: 1.6;
+ font-size: 14px !important;
}
-/*代码块*/
-.md-typeset code {
- font-size: inherit;
- border-radius: 2px !important;
- border: none !important;
-}
-.md-typeset pre>code {
- padding: 0.8em 0.8em;
-}
-code span::selection {
- background: #214283;
-}
-.highlight code::selection {
- background: #214283;
-}
-
-/*代码片段*/
-p code, article > code, li > code, td > code, th > code, a > code {
- background-color: transparent !important;
- color: var(--drake-highlight) !important;
- padding: 0 2px !important;
-}
-
-img {
- border-radius: 2px;
- margin: 4px 0;
-}
-
-/*链接*/
-.md-content a {
- color: var(--drake-highlight) !important;
- text-decoration: underline;
- margin: 0 2px;
-}
-
-/*编辑按钮*/
-.md-typeset .md-content__button {
- color: var(--md-default-fg-color--lighter) !important;
-}
-.md-icon svg {
- width: 14px;
-}
-
-/*标题*/
-h1, h2, h3, h4, h5, h6, .md-header-nav__title {
- font-weight: bold !important;
- color: #273849;
-}
-.md-typeset h1 {
- text-align: center;
- font-size: 1.45em;
- color:#273849;
-}
-.md-typeset h2 {
- display: inline-block;
- font-size: 1.45em;
-}
-h2:after {
- display: block;
- content: '';
- height: 2px;
- margin-top: 4px;
- background-color:#273849;
- border-radius: 2px;
- margin-right: 1.1em;
-}
-
-/*清单*/
-.md-typeset [type=checkbox]:checked+.task-list-indicator:before {
- background-color: #43A047;
-}
-.md-typeset .task-list-indicator:before {
- background-color: #c7c7c7;
-}
-.md-typeset .task-list-control {
- margin-right: 8px;
-}
-
-/*复制图标*/
-.md-clipboard:after {
- background-color: #4d4d4d;
-}
-
-/*头部*/
-.md-ellipsis {
- font-weight: bold;
-}
-
-/*折叠块*/
-/*标题展开状态*/
-.md-typeset .admonition-title, .md-typeset summary {
- border-left: none;
- margin: 0;
-}
-/*标题背景*/
-.md-typeset .abstract>.admonition-title, .md-typeset .abstract>summary, .md-typeset .summary>.admonition-title, .md-typeset .summary>summary, .md-typeset .tldr>.admonition-title, .md-typeset .tldr>summary {
- background-color: #f2f2f2;
- border: 1px solid #dfe2e5;
- font-weight: bold;
-}
-/*内容展开状态*/
-.md-typeset .admonition, .md-typeset details {
- border-left: none;
- box-shadow: none;
- padding: 0;
- font-size: var(--drake-font-size);
-}
-/*标题栏左侧图标*/
-.md-typeset .abstract>.admonition-title:before, .md-typeset .abstract>summary:before, .md-typeset .summary>.admonition-title:before, .md-typeset .summary>summary:before, .md-typeset .tldr>.admonition-title:before, .md-typeset .tldr>summary:before {
- background-color: var(--md-admonition-fg-color);
- top: .5rem;
-}
-/*箭头图标*/
-.md-typeset summary:after {
- top: .5rem;
+.highlight span.filename,
+.md-typeset .admonition-title,
+.md-typeset summary {
+ font-weight: normal;
}
\ No newline at end of file
diff --git a/docs/debounce.md b/docs/debounce.md
index 1dc087fcb..753c74fb1 100644
--- a/docs/debounce.md
+++ b/docs/debounce.md
@@ -1,14 +1,15 @@
-现在应用的搜索输入框一般情况下都是输入完搜索关键词后自动发起请求开始搜索
+!!! question "节流"
+ 在一定时间间隔内,只执行最后一次请求, 忽略其他多余的请求
-这个过程涉及到以下需求:
+搜索输入框一般都是输入完关键词后自动开始搜索
-1. 不能每次变化都开始搜索请求, 这样会导致多余的网络资源浪费. 所以应该在用户停止输入后的指定时间后(默认800毫秒)开始搜索
-2. 当产生新的搜索请求后取消旧的请求以防止旧数据覆盖新数据
-3. 当输入内容没有变化(例如复制粘贴重复内容到搜索框)不会发起搜索请求
+这个过程涉及到
-
+1. 每次变化都搜索会导致服务器压力, 应在停止输入满足一定时间后自动搜索
+2. 当产生新的搜索请求后应取消旧请求, 以防止旧数据覆盖新数据
+3. 当输入内容没有变化(例复制粘贴重复内容到搜索框)不会发起搜索请求
-截图预览
+
@@ -17,24 +18,17 @@
```kotlin
var scope: CoroutineScope? = null
-et_input.debounce().distinctUntilChanged().launchIn(this) {
+// distinctUntilChanged 表示过滤掉重复结果
+binding.etInput.debounce().distinctUntilChanged().launchIn(this) {
scope?.cancel() // 发起新的请求前取消旧的请求, 避免旧数据覆盖新数据
- scope = scopeNetLife { // 保存旧的请求到一个变量中, scopeNetLife其函数决定网络请求生命周期
- tvFragment.text = "请求中"
- val data = Get("http://api.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json").await()
- tvFragment.text = JSONObject(data).getJSONObject("result").getString("datetime_2")
+ scope = scopeNetLife { // 保存旧的请求到一个变量中
+ binding.tvFragment.text = "请求中"
+ binding.tvFragment.text = Get(Api.TIME).await()
}
}
```
-如果想要设置自己的节流阀超时时间请指定参数
+指定参数设置节流阀超时时间
```kotlin
fun EditText.debounce(timeoutMillis: Long = 800)
-```
-
-过滤掉重复结果使用函数`distinctUntilChanged`
-
-## 生命周期
-其生命周期依然遵守[网络请求作用域函数scope*](scope.md#_2)
-
-例如示例中使用的`scopeNetLife`就会在Activity或Fragment关闭时自动取消网络请求
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/docs/default-response.md b/docs/default-response.md
deleted file mode 100644
index f0986701c..000000000
--- a/docs/default-response.md
+++ /dev/null
@@ -1,50 +0,0 @@
-Net支持请求返回的数据类型取决于你自己的转换器实现(即理论上支持返回任何对象):
-
-# Get<任何对象>("path").await()
-
-如果不自定义转换器默认支持返回以下数据类型
-
-| 函数 | 描述 |
-|-|-|
-| String | 字符串 |
-| ByteArray | 字节数组 |
-| ByteString | 内部定义的一种字符串对象 |
-| File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) |
-| Response | 最基础的, 包含全部响应信息的对象(响应体/响应头/请求信息等) |
-
-使用示例
-
-```kotlin
-scopeNetLife {
- Get("api").await().headers("响应头名称") // 返回响应头
-}
-```
-
-??? summary "默认使用的是: [NetConverter.DEFAULT](https://github.com/liangjingkanji/Net/blob/master/net/src/main/java/com/drake/net/convert/NetConverter.kt)"
- ```kotlin
- interface NetConverter {
-
- @Throws(Throwable::class)
- fun onConvert(succeed: Type, response: Response): R?
-
- companion object DEFAULT : NetConverter {
- /**
- * 返回结果应当等于泛型对象, 可空
- * @param succeed 请求要求返回的泛型类型
- * @param response 请求响应对象
- */
- override fun onConvert(succeed: Type, response: Response): R? {
- return when {
- succeed === String::class.java && response.isSuccessful -> response.body?.string() as R
- succeed === ByteString::class.java && response.isSuccessful -> response.body?.byteString() as R
- succeed is GenericArrayType && succeed.genericComponentType === Byte::class.java && response.isSuccessful -> response.body?.bytes() as R
- succeed === File::class.java && response.isSuccessful -> response.file() as R
- succeed === Response::class.java -> response as R
- else -> throw ConvertException(response, "An exception occurred while converting the NetConverter.DEFAULT")
- }
- }
- }
- }
- ```
-
-假设这里没有你需要的数据类型请[自定义转换器](/converter/#_3)(例如返回Json或Protocol)
\ No newline at end of file
diff --git a/docs/download-file.md b/docs/download-file.md
index 21ef87811..6d88c710b 100644
--- a/docs/download-file.md
+++ b/docs/download-file.md
@@ -1,6 +1,4 @@
-## 简单下载
-
-下载文件和普通的接口请求唯一区别就是泛型不同
+泛型改为`File`即可
```kotlin
scopeNetLife {
@@ -8,11 +6,9 @@ scopeNetLife {
}
```
-Download函数一调用就会开始执行下载文件请求, 然后`await`则会等待下载文件完成然后返回一个File对象
-
## 下载选项
-支持丰富的下载定制方案, 并且会不断地更新完善
+丰富的下载定制方案, 并且在不断更新
```kotlin
scopeNetLife {
@@ -25,24 +21,22 @@ scopeNetLife {
}
```
-配置选项
-
-| 函数 | 描述 |
-|-|-|
-| setDownloadFileName | 下载的文件名称 |
-| setDownloadDir | 下载保存的目录, 也支持包含文件名称的完整路径, 如果使用完整路径则无视`setDownloadFileName`设置 |
-| setDownloadMd5Verify | 下载文件MD5校验, 如果服务器响应头`Content-MD5`值和指定路径已经存在的文件MD5相同, 则跳过下载直接返回该File |
-| setDownloadFileNameConflict | 下载文件路径存在同名文件时是创建新文件(添加序号)还是覆盖, 例如`file_name(1).apk` |
-| setDownloadFileNameDecode | 文件名称是否使用URL解码, 例如下载的文件名如果是中文, 服务器传输给你的会是被URL编码的字符串. 你使用URL解码后才是可读的中文名称 |
-| setDownloadTempFile | 下载是否使用临时文件, 避免下载失败后覆盖同名文件或者无法判别是否已下载完整, 仅在下载完整以后才会显示为原有文件名 |
-| addDownloadListener | 下载进度监听器, 具体介绍在[进度监听](progress.md)中 |
-
-> 不使用`await`函数则下载报错也不会被Net捕捉到, 将会被忽略, 使用await则会触发Net的错误处理, 终止当前作用域(scope)内其他网络请求, 被Net全局错误处理捕获
-
-## 缓存文件
-
-文件缓存推荐以下三种方式
-
-- 文件判断: 这种方式比较自由, 你自己去判断本地磁盘是否有该文件, 没有才发起请求, 比如你根据文件名判断. 无需网络
-- 缓存模式: 占用设备两份空间(因为缓存和下载后的文件都要占空间), 并且读取缓存的时候会本地磁盘复制依旧有耗时. 如果下载地址动态可以自定义缓存Key. 无需网络
-- MD5校验: 这种比较安全, 就是由服务器返回文件的MD5给你, 请查看`BaseRequest.setDownloadMd5Verify`方法. 要求服务器返回指定响应头, 要求联网
\ No newline at end of file
+| 配置选项 | 描述 |
+| --------------------------- | ------------------------------------------------------------ |
+| addDownloadListener | [下载进度监听器](progress.md) |
+| setDownloadFileName | 下载文件名 |
+| setDownloadDir | 下载目录 |
+| setDownloadMd5Verify | 下载文件MD5校验 |
+| setDownloadFileNameConflict | 下载文件同名冲突解决 |
+| setDownloadFileNameDecode | 文件名Url解码中文 |
+| setDownloadTempFile | 临时文件名 |
+
+## 重复下载
+
+防止重复下载有以下方式
+
+| 函数 | 描述 |
+| -------- | --------------------------------------------------------- |
+| 文件判断 | 判断本地是否存在同名文件 |
+| 缓存模式 | 开启缓存, 占用设备两份空间(缓存/下载成功文件都占空间) |
+| MD5校验 | 服务器返回`Content-MD5`, 客户端开启`setDownloadMd5Verify` |
\ No newline at end of file
diff --git a/docs/error-default.md b/docs/error-default.md
deleted file mode 100644
index 0af61ee20..000000000
--- a/docs/error-default.md
+++ /dev/null
@@ -1,29 +0,0 @@
-Net具备完善的错误处理机制, 能捕获大部分网络请求或者异步任务导致的崩溃, 减少App崩溃和收集详细的错误信息
-
-
-以下场景的抛出的异常会被Net捕获到(不会导致崩溃)
-
-1. 作用域内部 (scope**等函数大括号`{}`内部)
-1. 拦截器中 (Interceptor/RequestInterceptor)
-1. 转换器中 (NetConverter)
-
-
-
-
-如果捕获到错误默认会执行以下操作
-
-- `Logcat`中会输出详细的异常堆栈信息, 如果想要输出更详细内容请阅读[自定义异常](error-exception.md)
-- `Toast`吐司错误异常信息, 如果想要自定义或者国际化错误文本请阅读[自定义错误提示](error-tip.md)
-
-
-要改变以上的默认错误处理请阅读阅读[全局错误处理](error-global.md), 默认全局错误处理器实现源码: [NetErrorHandler](https://github.com/liangjingkanji/Net/blob/97c31dddde7ced5aa75411d2581c858ca494669e/net/src/main/java/com/drake/net/interfaces/NetErrorHandler.kt#L18)
-
-
-> 建议在全局错误处理器中将捕获到的Exception(除无网络异常意外)上报到崩溃统计平台
-
-
-
-
-
-
-
diff --git a/docs/error-global.md b/docs/error-global.md
index 5def0f6d8..4040d54d9 100644
--- a/docs/error-global.md
+++ b/docs/error-global.md
@@ -1,23 +1,30 @@
-Net可以通过实现`NetErrorHandler`接口来监听全局错误处理, 当你通过`setErrorHandler`后Net就不会再执行默认的错误处理了
+可实现`NetErrorHandler`接口来监听全局错误处理
-```kotlin
-NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
-
- setErrorHandler(object : NetErrorHandler() {
+=== "创建"
+ ```kotlin
+ class NetworkingErrorHandler : NetErrorHandler {
override fun onError(e: Throwable) {
- super.onError(e)
- }
-
- override fun onStateError(e: Throwable, view: View) {
- super.onStateError(e, view)
+ // .... 其他错误
+ if (e is ResponseException && e.tag == 401) { // 判断异常为token失效
+ // 打开登录界面或者弹登录失效对话框
+ }
}
- })
-}
-```
+ }
+ ```
+=== "配置"
+ ```kotlin
+ NetConfig.initialize(Api.HOST, this) {
+ setErrorHandler(NetworkingErrorHandler))
+ }
+ ```
-|场景|处理函数|处理方式|
+|NetErrorHandler|使用场景|触发位置|
|-|-|-|
-|普通网络请求/自动加载框|`onError`| 默认吐司错误信息 |
-|使用自动处理缺省页的作用域|`onStateError`| 仅部分错误信息会吐司, 因为缺省页不需要所有的错误信息都吐司(toast)提示, 因为错误页可能已经展示错误信息, 所以这里两者处理的函数区分. |
+|`onError`| 吐司错误信息 | `scopeNetLife/scopeDialog` |
+|`onStateError` | 要求错误显示在缺省页 |`PageRefreshLayout.scope/StateLayout.scope`|
+
+
+!!! warning "以下情况全局错误处理无效"
-> `scope/scopeLife`不会触发任何全局错误NetErrorHandler, 请使用单例错误处理方式`catch`, 因为其用于处理异步任务,不应当用于网络请求
\ No newline at end of file
+ 1. 异步任务作用域(`scope/scopeLife`)发生的错误
+ 2. 使用[单例错误处理](error-single.md)处理的错误
\ No newline at end of file
diff --git a/docs/error-single.md b/docs/error-single.md
index 7a6c7a2d2..9c0dd088a 100644
--- a/docs/error-single.md
+++ b/docs/error-single.md
@@ -1,13 +1,8 @@
-单例捕获即只捕获某个作用域或者接口, 不会影响到全局
-
-- 捕获请求
-- 捕获作用域
-
-
+捕获当前请求/作用域错误, 将不会再被全局错误处理
## 捕获请求
-一个作用域内常常有多个请求发生. 默认情况下一个请求发生错误就会取消当前作用域内部所有协程, 这个时候我们可以捕获错误请求来进行其他处理
+一个请求发生错误会取消当前作用域内所有请求, 但单独捕获后不会再影响其他请求
例如
```kotlin
@@ -27,7 +22,7 @@ scopeNetLife {
Get("api2").await() // 上面失败, 此处继续执行
}
```
-当然如果你创建不同的作用域分别请求那是互不影响的
+当然如果创建不同的作用域分别请求那是互不影响的
```kotlin
scopeNetLife {
Get("api").await() // 失败
@@ -45,15 +40,13 @@ scopeNetLife {
scope {
val data = Get("http://www.thisiserror.com/").await()
}.catch {
- // 协程内部发生错误回调, it为异常
+ // 协程内发生错误回调, it为异常对象
}.finally {
- // 协程内协程全部执行完成, it为异常(如果是正常结束则it为null)
+ // 协程执行完毕回调(不论成败), it为异常对象
}
```
-以下函数幕后字段`it`为异常对象, 如果正常完成it则为null. 如果属于请求被手动取消则it为`CancellationException`
-
-| 函数 | 描述 |
+| 函数 | 区别 |
|-|-|
-| catch | 作用域被`catch`则不会被传递到全局异常处理回调中: [全局处理异常](exception-handle.md), 除非使用`handleError`再次传递给全局 |
-| finally | 同样可以获取到异常对象, 且不影响全局异常回调处理 |
\ No newline at end of file
+| catch | 发生错误后回调, 取消不回调
不会再执行全局错误处理, 可使用`handleError`再次传递给全局 |
+| finally | 执行完毕回调(不论成败), 取消作用域时`it`为`CancellationException`
执行全局错误处理 |
\ No newline at end of file
diff --git a/docs/error-exception.md b/docs/error-throws.md
similarity index 53%
rename from docs/error-exception.md
rename to docs/error-throws.md
index 8274db285..ceb75213c 100644
--- a/docs/error-exception.md
+++ b/docs/error-throws.md
@@ -29,55 +29,51 @@
> 转换器中发生的所有异常除非是NetException的子类否则都将被ConvertException包裹(即捕获的是ConvertException, cause才为实际抛出异常).
-## 使用异常属性
+## 异常传递字段
-Net自带的一些异常都会有一个类型为Any的属性`tag`. 可以用来传递任何对象来用于判断错误类型. 比如`ResponseException`我常用于作为请求服务器成功但是服务器业务错误. 然后tag为业务错误码
+Net自带异常会有类型为Any的字段`tag`, 可用传递对象用于判断错误处理
+
+例如`ResponseException`常用于作为请求服务器成功但后端业务错误, 然后tag为传递的错误码
示例代码
-在转换器中获取401
-
-```kotlin
-class SerializationConverter(
- val success: String = "0",
- val code: String = "code",
- val message: String = "msg"
-) : NetConverter {
-
- override fun onConvert(succeed: Type, response: Response): R? {
- try {
- return NetConverter.onConvert(succeed, response)
- } catch (e: ConvertException) {
- val code = response.code
- when {
- code in 200..299 -> { // 请求成功
- // ... 假设Token失效. 后端返回业务错误码 srvCode = 401
- throw ResponseException(response, errorMessage, tag = srvCode) // 将业务错误码作为tag传递
+=== "转换器抛出异常"
+
+ ```kotlin
+ class SerializationConverter(
+ val success: String = "0",
+ val code: String = "code",
+ val message: String = "msg"
+ ) : NetConverter {
+
+ override fun onConvert(succeed: Type, response: Response): R? {
+ try {
+ return NetConverter.onConvert(succeed, response)
+ } catch (e: ConvertException) {
+ val code = response.code
+ when {
+ code in 200..299 -> { // 请求成功
+ // ... 假设Token失效. 后端返回业务错误码 srvCode = 401
+ throw ResponseException(response, errorMessage, tag = srvCode) // 将业务错误码作为tag传递
+ }
+ code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误
+ code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误
+ else -> throw ConvertException(response)
}
- code in 400..499 -> throw RequestParamsException(response, code.toString()) // 请求参数错误
- code >= 500 -> throw ServerResponseException(response, code.toString()) // 服务器异常错误
- else -> throw ConvertException(response)
}
}
}
-}
-```
-
-全局错误处理器
-
-```kotlin
-// 创建错误处理器
-MyErrorHandler : NetErrorHandler {
- override fun onError(e: Throwable) {
- // .... 其他错误
- if (e is ResponseException && e.tag == 401) { // 判断异常为token失效
- // 打开登录界面或者弹登录失效对话框
+ ```
+
+=== "全局错误处理异常"
+
+ ```kotlin
+ class NetworkingErrorHandler : NetErrorHandler {
+ override fun onError(e: Throwable) {
+ // .... 其他错误
+ if (e is ResponseException && e.tag == 401) { // 判断异常为token失效
+ // 打开登录界面或者弹登录失效对话框
+ }
}
}
-}
-
-// 初始化Net的时候设置错误处理器
-NetConfig.initialize("host", this) {
- setErrorHandler(MyErrorHandler()
-}
-```
\ No newline at end of file
+ ```
\ No newline at end of file
diff --git a/docs/error-tip.md b/docs/error-tip.md
index 8e4221c29..bd5ddaa83 100644
--- a/docs/error-tip.md
+++ b/docs/error-tip.md
@@ -1,31 +1,63 @@
-网络请求发生错误一定要提示给用户, 提示语一般情况下是可读的语义句
+!!! question "网络错误提示"
+ 网络请求发生错误一定要提示给用户, 并且是可读语义句
-如果你是想修改默认吐司的错误文本信息或者做国际化语言可以参考以下方法
+如需修改默认吐司错误文本或者国际化语言可以参考以下方法
## 创建多语言
-默认错误处理的文本被定义在`strings.xml`中, 我们可以在项目中创建多语言values来创建同名string实现国际化. 比如英语是`values-en`下创建文件`strings.xml`
-
-```xml
-
-连接网络失败
-当前网络不可用
-请求资源地址错误
-无法找到指定服务器主机
-连接服务器超时,%s
-下载过程发生错误
-读取缓存失败
-解析数据时发生异常
-请求失败
-请求参数错误
-服务响应错误
-发生空异常
-未知网络错误
-未知错误
-无错误信息
-```
+错误提示文本被定义在框架中`strings.xml`, 在自己应用项目中创建同名`name`可复写Net定义的文本
+
+或者中创建多语言values实现国际化, 例如英语是`values-en`下创建文件`strings.xml`
+
+??? example "错误文本"
+ ```xml
+
+ 连接网络失败
+ 请求资源地址错误
+ 无法找到指定服务器主机
+ 连接服务器超时,%s
+ 下载过程发生错误
+ 读取缓存失败
+ 解析数据时发生异常
+ 请求失败
+ 请求参数错误
+ 服务响应错误
+ 发生空异常
+ 未知网络错误
+ 未知错误
+ 无错误信息
+
+
+ 加载中
+ ```
## 创建NetErrorHandler
-这实际上就是[自定义全局错误处理](error-global.md), 不过你可以复制默认的实现仅修改下文本信息即可
+使用[自定义全局错误处理](error-global.md)完全复写, 可以不提示错误或者上传错误日志
+
+??? example "全局错误处理"
+ ```kotlin
+ fun onError(e: Throwable) {
+ val message = when (e) {
+ is UnknownHostException -> NetConfig.app.getString(R.string.net_host_error)
+ is URLParseException -> NetConfig.app.getString(R.string.net_url_error)
+ is NetConnectException -> NetConfig.app.getString(R.string.net_connect_error)
+ is NetSocketTimeoutException -> NetConfig.app.getString(
+ R.string.net_connect_timeout_error,
+ e.message
+ )
+ is DownloadFileException -> NetConfig.app.getString(R.string.net_download_error)
+ is ConvertException -> NetConfig.app.getString(R.string.net_parse_error)
+ is RequestParamsException -> NetConfig.app.getString(R.string.net_request_error)
+ is ServerResponseException -> NetConfig.app.getString(R.string.net_server_error)
+ is NullPointerException -> NetConfig.app.getString(R.string.net_null_error)
+ is NoCacheException -> NetConfig.app.getString(R.string.net_no_cache_error)
+ is ResponseException -> e.message
+ is HttpFailureException -> NetConfig.app.getString(R.string.request_failure)
+ is NetException -> NetConfig.app.getString(R.string.net_error)
+ else -> NetConfig.app.getString(R.string.net_other_error)
+ }
-
+ Net.debug(e)
+ TipUtils.toast(message)
+ }
+ ```
diff --git a/docs/error.md b/docs/error.md
new file mode 100644
index 000000000..47b3e80f9
--- /dev/null
+++ b/docs/error.md
@@ -0,0 +1,25 @@
+Net有完善的错误处理机制, 具备捕获异常/取消请求/错误提示/追踪链路
+
+!!! success "收集网络日志"
+ 在Net作用域内发生的异常都会被全局错误处理捕获, 可以将其筛选上传日志
+
+
+以下位置抛出异常会被捕获
+
+| 函数 | 描述 |
+|-|-|
+| 作用域 | `scopeXX`代码块中 |
+| 拦截器 | `Interceptor/RequestInterceptor` |
+| 转换器 | `NetConverter` |
+
+如果捕获到错误默认会执行以下操作
+
+1. `Logcat`输出异常堆栈信息, [自定义异常抛出](error-throws.md)
+2. `Toast`显示错误文本, [自定义错误提示](error-tip.md)
+
+
+!!! failure "捕获不到异常"
+ 如果请求未执行`await()`, 那么即使发生错误也不会被捕获到
+
+
+自定义请阅读[全局错误处理](error-global.md)
\ No newline at end of file
diff --git a/docs/exception-track.md b/docs/exception-track.md
deleted file mode 100644
index a9f2fdac5..000000000
--- a/docs/exception-track.md
+++ /dev/null
@@ -1,28 +0,0 @@
-Net中网络请求导致的异常都会在LogCat中打印, 同时被全局的NetErrorHandler的onError拦截到(除非catch住)
-
-
-演示访问一个不存在的请求路径
-```kotlin
-scopeNetLife {
- tvFragment.text = Get("https://githuberror.com/liangjingkanji/Net/").await()
-}
-```
-
-查看LogCat可以看到异常堆栈信息, 这属于URL未知异常
-
-
-
-截图可以看到有具体的URL和请求代码位置
-
-
-### 关闭日志
-
-在初始化时候可以关闭日志打印
-
-```kotlin
-NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
- setDebug(false) // 关闭日志, 我们一般使用 BuildConfig.DEBUG
-}
-```
-
-或者设置字段`NetConfig.debug`的值
\ No newline at end of file
diff --git a/docs/fastest.md b/docs/fastest.md
index 6d38e8418..4705ddc6e 100644
--- a/docs/fastest.md
+++ b/docs/fastest.md
@@ -1,7 +1,10 @@
-Net支持多个接口请求并发, 仅返回最快的请求结果, 剩余请求将被自动取消, 同样可以用于筛选掉无法响应的域名
-
+多个请求并发执行
-> 接口请求错误被忽略(LogCat依然可以看到异常信息), 但如果所有请求全部异常则抛出最后一个请求的异常作为错误处理
+1. 当某个请求成功时, 剩余请求将被自动取消
+2. 全部错误时则会抛出最后一个请求错误作为结果
+
+!!! Note "异常信息"
+ 先失败的请求错误会被忽略, 但LogCat依然会输出异常
示例
```kotlin
@@ -21,31 +24,24 @@ scopeNetLife {
## 取消剩余
-上面的示例代码实际上不会在获取到最快的结果后自动取消请求, 我们需要`setGroup()`才可以
+上面的示例不会在获取到结果后取消剩余请求, 需设置同一请求分组才可以
```kotlin
scopeNetLife {
// 同时发起四个网络请求
- val deferred2 = Get("api") { setGroup("最快") }
- val deferred3 = Post("api") { setGroup("最快") }
- val deferred = Get("api0") { setGroup("最快") } // 错误接口
- val deferred1 = Get("api1") { setGroup("最快") } // 错误接口
+ val deferred2 = Get("api") { setGroup("初始化") }
+ val deferred3 = Post("api") { setGroup("初始化") }
+ val deferred = Get("api0") { setGroup("初始化") } // 错误接口
+ val deferred1 = Get("api1") { setGroup("初始化") } // 错误接口
// 只返回最快的请求结果
- tvFragment.text = fastest(listOf(deferred, deferred1, deferred2, deferred3), "最快")
+ tvFragment.text = fastest(listOf(deferred, deferred1, deferred2, deferred3), "初始化")
}
```
-网络请求的取消本质上依靠`group`来辨别,如果使用`setGroup`函数设置分组名称就可以在返回最快结果后取消掉其他网络请求, 反之不会取消其他网络请求
-
-
-> group可以是任何类型任何值, 只有请求的`setGroup`参数和`fastest`函数的group参数等于即可
-
-
-
## 类型不一致
-假设并发的接口返回的数据类型不同 或者想要监听最快请求返回的结果回调请使用`transform`函数
+当需要返回结果, 但多接口返回数据类型不同, 使用`transform`转换为同一类型结果
```kotlin
scopeNetLife {
@@ -64,13 +60,9 @@ scopeNetLife {
}
```
-有的场景下并发的接口返回的数据类型不同, 但是fastest只能返回一个类型, 我们可以使`transform`的回调函数返回结果都拥有一个共同的接口, 然后去类型判断
-
-
-
> 只有最快返回结果的网络请求(或异步任务)的`transform`回调才会被执行到
-## 捕获Fastest
+## 捕获错误
```kotlin
scopeNetLife {
val task = Get("api2")
@@ -78,16 +70,12 @@ scopeNetLife {
val task2 = Get("api2")
val data = try {
- fastest(task, task1, task2) // 当 task/task1/task2 全部异常之后再并发执行 backupTask/backupTask1
+ fastest(listOf(task, task1, task2))
+ // 当task/task1/task2全部错误后才并发执行backupTask/backupTask1
} catch (e: Exception) {
val backupTask = Get("api2")
val backupTask1 = Get("api")
- fastest(backupTask, backupTask1)
+ fastest(listOf(backupTask, backupTask1))
}
}
-```
-
-
-
-
-> 不要尝试使用这种方式来取代CDN加速
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/docs/https.md b/docs/https.md
index ff3a0e9f1..f3df3fb80 100644
--- a/docs/https.md
+++ b/docs/https.md
@@ -1,10 +1,8 @@
-Https访问主要就是证书配置问题, Net可以使用OkHttp一切函数组件, 且简化证书配置流程
-
-> OkHttp如何配置证书, Net就可以如何配置证书
+Net可快速配置Https证书, 或者使用OkHttp的方式
## 使用CA证书
-Https如果是使用的CA颁发的证书, 不需要任何配置Net可以直接访问
+Https如果使用的CA证书, 不需要任何配置可以直接访问
```kotlin
scopeNetLife {
@@ -14,12 +12,12 @@ scopeNetLife {
## 信任所有证书
-信任所有证书可以解决无法访问私有证书的Https地址问题, 但是这么做就失去了使用证书的含义, 不建议此做法
+信任所有证书可以解决无法访问私有证书的Https地址问题
=== "全局配置"
```kotlin
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this){
+ NetConfig.initialize(Api.HOST, this){
trustSSLCertificate() // 信任所有证书
}
```
@@ -37,12 +35,12 @@ scopeNetLife {
## 导入证书
-私有证书可以放到任何位置, 只要能读取到`InputStream`流对象即可
+私有证书可以放到任何位置, 只要读取到`InputStream`流对象即可
=== "全局配置"
```kotlin
- NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) {
+ NetConfig.initialize(Api.HOST, this) {
val privateCertificate = resources.assets.open("https.certificate")
setSSLCertificate(privateCertificate)
}
diff --git a/docs/img/book-open.svg b/docs/img/book-open.svg
new file mode 100644
index 000000000..b0cbc997c
--- /dev/null
+++ b/docs/img/book-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/img/code-preview.png b/docs/img/code-preview.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6fd23b9930be0d509b7d603214f33ebc409a628
GIT binary patch
literal 26782
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
literal 0
HcmV?d00001
diff --git a/docs/img/preview.png b/docs/img/preview.png
new file mode 100644
index 0000000000000000000000000000000000000000..da466e86e862dc7bc46ea5ee1fb5f4239529075c
GIT binary patch
literal 189960
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