diff --git a/README.md b/README.md index a809a12a0..9e831ee70 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,23 @@ | 下载体验

-

+

- + - +

-

+


-Android上可能是最强的网络框架, 基于[OkHttp](https://github.com/square/okhttp)/协程的非侵入式框架(不影响原有功能). 学习成本低/使用简单, 一行代码发起网络请求, 甚至无需初始化 +Net是基于[OkHttp](https://github.com/square/okhttp)/协程的非侵入式框架(可使用所有Api), 可升级OkHttp版本保持网络安全
@@ -33,6 +33,7 @@ Welcome to international translation of this project's documents/notes, thank yo [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 +45,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] 永远保持社区维护 ## 主要功能 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..daaad0986 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -13,26 +13,28 @@ ## 配置缓存 -不配置缓存设置是不会触发缓存的 +不配置`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. 缓存有效期过期只是让缓存无效, 并不会被删除(即无法读取), 缓存删除遵守Lru(最近最少使用)原则在所有缓存体积达到配置值时删除(即使缓存有效期未到) ```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..e43b34096 100644 --- a/docs/cancel.md +++ b/docs/cancel.md @@ -1,7 +1,4 @@ -Net虽然支持自动跟随生命周期取消网络请求, 绝大部分场景也足够. 但是有时还是需要手动取消, 例如取消下载文件. -
- -Net取消协程作用域自动取消内部网络请求, 也支持任意位置取消指定网络请求. +Net跟随作用域生命周期自动取消网络请求, 绝大部分场景无需手动处理, 但是例如取消下载文件等可能需要手动取消 ```kotlin downloadScope = scopeNetLife { @@ -15,9 +12,8 @@ downloadScope.cancel() // 取消下载 ## 任意位置取消 -发起请求的时候要求定义一个`Id`用于指定网络请求, 然后在需要的地方使用单例对象`Net.cancelId`取消请求. +发起请求时指定`Id` -创建请求 ```kotlin scopeNetLife { tvFragment.text = Get("api"){ @@ -26,15 +22,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..1e5d72b21 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2,123 +2,86 @@ ## 初始配置 -=== "普通初始化" +两种方式初始配置, 不初始化也能直接使用 +=== "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" -``` - - -2) 传入路径 -如传入参数为路径(例如`/api/index`)会自动和`host`拼接组成完成URL, 但如果传入的以`http/https`开头的全路径则会直接作为请求URL - -```kotlin -scopeNetLife { - val data = Get("https://github.com/liangjingkanji/Net/").await() -} -``` - -3) 使用拦截器 - -或者通过指定`tag`, 然后拦截器(interceptor)中根据tag动态修改host, 因为拦截器能修改一切请求参数 +## 多域名 -```kotlin -scopeNetLife { - val data = Get("/api/index", "User").await() // User即为tag -} -// 拦截器修改请求URL不做介绍 -``` +概念源于`Retrofit`(称为BaseUrl), 因为Retrofit无法二次修改请求Host, 但Net支持随时修改 -## 多域名 +以下介绍三种修改方式 -```kotlin -scopeNetLife { - Get("https://github.com/liangjingkanji/Net/").await() // 传入域名就会使用当前域名 - Get("/liangjingkanji/Net/").await() // 自动和NetConfig.host拼接 - Get(Api.Host2 + "/liangjingkanji/Net/").await() // 自己手动拼接 -} -``` +=== "修改Host" + ```kotlin + NetConfig.host = Api.HOST_2 + ``` -和BaseUrl一样你还可以在拦截器里面统一处理, 在拦截器里面判断tag或者path来拼接域名 +=== "指定全路径" + 指定Path(例如`/api/index`)会自动和`NetConfig.host`拼接组成Url, 但指定以`http/https`开头的全路径则直接作为请求Url + ```kotlin + scopeNetLife { + val data = Get("https://github.com/path").await() + } + ``` +=== "使用拦截器" + 请求时指定`tag`, 然后拦截器中根据tag判断修改host, 拦截器能修改所有请求/响应信息 + ```kotlin + scopeNetLife { + val data = Get("/api/index", "User").await() // User即为tag + } + // 拦截器修改请求URL不做介绍 + ``` \ No newline at end of file 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..71438061e --- /dev/null +++ b/docs/converter-customize.md @@ -0,0 +1,232 @@ +Net自定义转换器可支持任何数据类型, 甚至`Bitmap` + +!!! failure "泛型和转换器" + 例如请求动作Post`指定泛型为Model`, 则转换器NetConverter中的函数`onConvert返回值必须为Model`, + 如果转换失败或者发生异常都算请求错误 + +> 注意如果你在转换器里面返回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) + + + +## 设置转换器 + +转换器分为全局和单例, 单例可以覆盖全局的转换器. 如果不设置转换器就会采用默认的转换器 + +=== "全局" + ```kotlin hl_lines="2" + NetConfig.initialize(Api.HOST, 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数据. + +??? example "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中后端定义的错误码和错误信息 + val srvCode = json.getString(this.code) + if (srvCode == 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, tag = srvCode) // 将业务错误码作为tag传递 + } + } 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) + } + } + } + + /** + * 反序列化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 diff --git a/docs/converter-struct.md b/docs/converter-struct.md new file mode 100644 index 000000000..19c376345 --- /dev/null +++ b/docs/converter-struct.md @@ -0,0 +1,116 @@ + +上一章节介绍如何序列化框架解析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":"密码错误", + } + ``` + 只要认为需要解析结构体情况下, 都应属于正常流程 + +## JSON中字段 + +如果接口返回很多无用字段, 而我们只需要其中`data`字段值场景下我们可以在解析前先处理下JSON + +数据类只需要包含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/Map等和普通对象没有区别, 取决于如何实现转换器 + +```kotlin +scopeNetLife { + tvFragment.text = Get>("list") { + converter = GsonConverter() // 单例转换器, 一般情况下是定义一个全局转换器 + }.await()[0].name +} +``` + +## 泛型数据类 + +某些地区很多开发者习惯这么使用, 因为他们接口返回无关字段, 但是技术不够无法自定义转换器来简化取值 + +所以他们选择更复杂的方式: 使用泛型来简化 + +```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..19676f36d 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/#_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..b5d5596a1 100644 --- a/docs/coroutine-request.md +++ b/docs/coroutine-request.md @@ -1,12 +1,9 @@ -Net在2.0开始引入协程来支持并发和异步, 虽然很多网络框架支持协程, 但是Net对于协程生命周期的控制算得上是独有. -并且Net不仅仅网络请求, 其也支持创建任何异步任务. +Net在2.0开始引入协程并发请求, 虽然很多网络框架支持协程, 但是唯有Net考虑到协程生命周期 -> 这里的`同时/并发/并行`统称为并发(具体是不是并行不需要开发者来考虑) +并且不仅仅网络请求, 其也支持创建任何异步任务
-在上章节已经使用过了网络的并发请求 - -这里再演示同时(并发)请求百度网站`一万次`并且一次取消 +在上章节已经介绍如何发起并发网络请求, 这里再演示同时(并发)网络请求`一万次`并且一次性全部取消 ```kotlin val job = scopeNetLife { @@ -28,9 +25,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..3f5119d0a 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,16 @@ -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; + font-weight: normal; } \ No newline at end of file diff --git a/docs/debounce.md b/docs/debounce.md index 1dc087fcb..cfead77e7 100644 --- a/docs/debounce.md +++ b/docs/debounce.md @@ -1,10 +1,13 @@ -现在应用的搜索输入框一般情况下都是输入完搜索关键词后自动发起请求开始搜索 +!!! question "节流" + 在一定时间间隔内,只执行最后一次请求, 忽略其他多余的请求 -这个过程涉及到以下需求: +现代应用的搜索输入框一般情况下都是输入完关键词后自动开始搜索 -1. 不能每次变化都开始搜索请求, 这样会导致多余的网络资源浪费. 所以应该在用户停止输入后的指定时间后(默认800毫秒)开始搜索 -2. 当产生新的搜索请求后取消旧的请求以防止旧数据覆盖新数据 -3. 当输入内容没有变化(例如复制粘贴重复内容到搜索框)不会发起搜索请求 +这个过程涉及到以下 + +1. 每次变化都搜索会导致服务器压力, 应在停止输入满足一定时间后自动搜索 +2. 当产生新的搜索请求后应取消旧请求, 以防止旧数据覆盖新数据 +3. 当输入内容没有变化(例复制粘贴重复内容到搜索框)不会发起搜索请求
@@ -27,14 +30,9 @@ et_input.debounce().distinctUntilChanged().launchIn(this) { } ``` -如果想要设置自己的节流阀超时时间请指定参数 +指定参数设置节流阀超时时间 ```kotlin fun EditText.debounce(timeoutMillis: Long = 800) ``` -过滤掉重复结果使用函数`distinctUntilChanged` - -## 生命周期 -其生命周期依然遵守[网络请求作用域函数scope*](scope.md#_2) - -例如示例中使用的`scopeNetLife`就会在Activity或Fragment关闭时自动取消网络请求 \ No newline at end of file +过滤掉重复结果使用函数`distinctUntilChanged` \ 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..00afe8538 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`设置 | +| 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全局错误处理捕获 +| setDownloadFileNameDecode | 使用Url解码中文文件名, 如果文件名是中文, 服务器返回的Url编码字符串, 需解码才可读 | +| setDownloadTempFile | 使用临时文件名, 下载未完成的文件名使用`.downloading`后缀 | +| addDownloadListener | [下载进度监听器](progress.md) | -## 缓存文件 +## 重复下载 -文件缓存推荐以下三种方式 +防止重复下载有以下方式 -- 文件判断: 这种方式比较自由, 你自己去判断本地磁盘是否有该文件, 没有才发起请求, 比如你根据文件名判断. 无需网络 -- 缓存模式: 占用设备两份空间(因为缓存和下载后的文件都要占空间), 并且读取缓存的时候会本地磁盘复制依旧有耗时. 如果下载地址动态可以自定义缓存Key. 无需网络 -- MD5校验: 这种比较安全, 就是由服务器返回文件的MD5给你, 请查看`BaseRequest.setDownloadMd5Verify`方法. 要求服务器返回指定响应头, 要求联网 \ No newline at end of file +| 函数 | 描述 | +| -------- | --------------------------------------------------------- | +| 文件判断 | 判断本地是否存在同名文件 | +| 缓存模式 | 开启缓存, 占用设备两份空间(缓存/下载成功文件都占空间) | +| 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..c0b9e65f3 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..910a70459 100644 --- a/docs/error-single.md +++ b/docs/error-single.md @@ -27,7 +27,7 @@ scopeNetLife { Get("api2").await() // 上面失败, 此处继续执行 } ``` -当然如果你创建不同的作用域分别请求那是互不影响的 +当然如果创建不同的作用域分别请求那是互不影响的 ```kotlin scopeNetLife { Get("api").await() // 失败 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..f1cfcffd8 100644 --- a/docs/https.md +++ b/docs/https.md @@ -1,10 +1,8 @@ -Https访问主要就是证书配置问题, Net可以使用OkHttp一切函数组件, 且简化证书配置流程 - -> OkHttp如何配置证书, Net就可以如何配置证书 +Net可快速配置Https证书, 且允许使用OkHttp任何Api ## 使用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 000000000..b6fd23b99 Binary files /dev/null and b/docs/img/code-preview.png differ diff --git a/docs/img/preview.png b/docs/img/preview.png new file mode 100644 index 000000000..da466e86e Binary files /dev/null and b/docs/img/preview.png differ diff --git a/docs/index.md b/docs/index.md index 01ee9d217..9a2400302 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,71 +1,69 @@ -本框架使用Android主流的Http框架OkHttp作为请求内核, 遵守不影响OkHttp原有的函数组件使用原则开发 +Net是基于[OkHttp](https://github.com/square/okhttp)/协程的非侵入式框架(可使用所有Api), 可升级OkHttp版本保持网络安全
-

STAR/分享可以让更多人参与到本开源项目, 点击文档右上角小铅笔可直接修订文档 ↗

+

STAR/分享可以让更多人参与到本开源项目


-## 前言 - -1. 任何本文档没有提到的功能都可以通过搜索`"OkHttp如何**"`来解决, 因为本框架支持OkHttp所有功能/组件 -1. 建议创建一个Api.kt的`object`单例类存储所有请求路径常量 -1. `Post/Get等`函数属于请求动作. `scope**`等函数属于作用域, 假设你有某个请求需要重复使用建议封装`请求动作`而不是作用域 -1. 如果你觉得文档看不懂或者有歧义那肯定是作者问题, 请反馈给作者或者自我修订 +!!! note "前言" + - 未实现功能可以搜索`"OkHttp如何XX"`来扩展 + - 阅读示例/源码来学习如何封装 + - 如果觉得文档看不懂那肯定是作者问题, 请反馈给作者或者自我修订 ## 使用
-这里演示发起网络`请求百度网站`内容的三个步骤 +这里演示发起网络`请求网站内容`的三个步骤 1. 创建作用域 -1. 发起请求 -1. 接收数据 +1. 发起请求动作 +1. 等待数据返回 -=== "单个请求" +=== "简单请求" ```kotlin - scopeNetLife { // 创建作用域 + scopeNetLife { 创建作用域 // 这个大括号内就属于作用域内部 val data = Get("https://github.com/liangjingkanji/Net/").await() // 发起GET请求并返回`String`类型数据 } ``` -=== "串行请求" +=== "同步请求" ```kotlin scopeNetLife { - val data = Get("http://www.baidu.com/").await() // 请求A 发起GET请求并返回数据 - val data = Get("https://github.com/liangjingkanji/Net/").await() // 请求B 将等待A请求完毕后发起GET请求并返回数据 + val userInfo = Get("https://github.com/liangjingkanji/BRV/").await() // 立即请求 + val config = Get("https://github.com/liangjingkanji/Net/"){ + param("userId", userInfo.id) // 使用上个请求的数据作为参数 + }.await() // 请求B 将等待A请求完毕后发起GET请求并返回数据 } ``` === "并发请求" ```kotlin scopeNetLife { // 以下两个网络请求属于同时进行中 - val aDeferred = Get("https://github.com/liangjingkanji/Net/") // 发起GET请求并返回一个对象(Deferred)表示"任务A" - val bDeferred = Get("https://github.com/liangjingkanji/Net/") // 发起请求并返回"任务B" + val getUserInfoAsync = Get("https://github.com/liangjingkanji/Net/") // 立即请求 + val getConfigAsync = Get("https://github.com/liangjingkanji/BRV/") // 立即请求 - // 随任务同时进行, 但是数据依然可以按序返回 - val aData = aDeferred.await() // 等待任务A返回数据 - val bData = bDeferred.await() // 等待任务B返回数据 + val userInfo = getUserInfoAsync.await() // 等待数据返回 + val config = getConfigAsync.await() } ``` -多个网络请求放在同一个作用域内就可以统一控制, 如果你的多个网络请求毫无关联, 你可以创建多个作用域. +多个网络请求在同一个作用域内可以统一控制, 如果多个网络请求之间毫无关联, 可以创建多个作用域来请求 -> 多进程或Xposed项目要求[初始化](config.md/#_1) +> 多进程或Xposed项目要求先[初始化](config.md/#_1)
-> 当`Get`或`Post`等函数调用就会开始发起网络请求, `await`只是等待其请求成功返回结果, 所以如果你在`await`后执行的网络请求,这不属于并发(属于串行) +并发请求错误示例 -并发的错误示例 ```kotlin hl_lines="3" scopeNetLife { // 请求A - val aDeferred = Get("https://github.com/liangjingkanji/Net/").await() - // 请求B, 由于上面使用`await()`函数, 所以必须等待A请求返回结果后才会执行B - val bDeferred = Get("https://github.com/liangjingkanji/Net/") + val userInfo = Get("https://github.com/liangjingkanji/Net/").await() + // 由于上面使用`await()`函数, 所以必须等待A请求返回结果后才会执行B + val getConfigAsync = Post("https://github.com/liangjingkanji/Net/") - val bData = bDeferred.await() // 等待任务B返回数据 + val config = getConfigAsync.await() // 等待任务B返回数据 } ``` @@ -81,38 +79,16 @@ scopeNetLife { | Response | 最基础的响应 | | File | 文件对象, 这种情况其实应当称为[下载文件](download-file.md) | -非以上类型要求[自定义转换器](converter.md) - -> 转换器的返回值决定你的网络请求的返回结果类型, 你甚至可以返回null, 前提是泛型为可空类型 - - -## RestFul -Net支持RestFul设计风格 - ```kotlin scopeNetLife { - tvFragment.text = Get("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Post("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Head("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Put("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Patch("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Delete("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Trace("https://github.com/liangjingkanji/Net/").await() - tvFragment.text = Options("https://github.com/liangjingkanji/Net/").await() + val file = Get(Api.FILE).await() } ``` -## 函数 - -默认在IO线程执行网络请求(通过作用域参数可以控制Dispatch调度器), 要求在协程作用域内执行. +详细查看[转换器](converter.md), 非以上类型要求[自定义转换器](converter-customize.md) -|请求函数|描述| -|-|-| -| [Get](api/-net/com.drake.net/-get.html)|标准Http请求方法| -| [Post](api/-net/com.drake.net/-post.html)|标准Http请求方法| -| [Head](api/-net/com.drake.net/-head.html)|标准Http请求方法| -| [Options](api/-net/com.drake.net/-options.html)|标准Http请求方法| -| [Trace](api/-net/com.drake.net/-trace.html)|标准Http请求方法| -| [Delete](api/-net/com.drake.net/-delete.html)|标准Http请求方法| -| [Put](api/-net/com.drake.net/-put.html)|标准Http请求方法| -| [Patch](api/-net/com.drake.net/-patch.html)|标准Http请求方法| \ No newline at end of file +--- +[下载Apk](https://github.com/liangjingkanji/Net/releases/latest/download/net-sample.apk){ .md-button } +[下载源码](https://github.com/liangjingkanji/Net.git){ .md-button } +[示例代码](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/ui/fragment){ .md-button } +[界面演示](https://github.com/liangjingkanji/BRV){ .md-button } diff --git a/docs/interceptor.md b/docs/interceptor.md index f489654ff..abd10aed7 100644 --- a/docs/interceptor.md +++ b/docs/interceptor.md @@ -1,11 +1,12 @@ -总共支持两种拦截器 +根据使用场景实现以下接口来拦截网络请求 -1. `Interceptor`, 支持市面上的所有OkHttp拦截器组件库, 更方便修改请求/响应信息, 可以转发请求 -2. `RequestInterceptor`, 部分场景更简单易用的轻量拦截器, 更方便添加全局请求头/参数, 无法转发请求 +1. `Interceptor`: 支持三方OkHttp拦截器组件, 允许修改请求/响应信息, 可以转发请求 +2. `RequestInterceptor`: Net独有的轻量级拦截器, 允许修改全局请求头/请求参数, 无法转发请求
-> 实际项目中可能存在需求加密请求/解密响应, 非常不建议封装Post/Get等请求动作(低扩展性/增加学习成本), 任何项目需求都可以通过自定义拦截器和转换器实现
-> [常见拦截器示例](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/interceptor) +!!! Failure "禁止随意封装" + 不应为全局参数/加密等封装请求方法, 应自定义拦截器/转换器来实现, [常见拦截器示例](https://github.com/liangjingkanji/Net/tree/master/sample/src/main/java/com/drake/net/sample/interceptor) + ## 拦截器 @@ -17,14 +18,14 @@ class App : Application() { override fun onCreate() { super.onCreate() - NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { + NetConfig.initialize(Api.HOST, this) { addInterceptor(RefreshTokenInterceptor()) } } } ``` -以下为简单演示客户端自动刷新token拦截器 +演示客户端自动刷新token的拦截器 ```kotlin /** @@ -50,12 +51,10 @@ class RefreshTokenInterceptor : Interceptor { ## 请求拦截器 -RequestInterceptor属于轻量级的请求拦截器, 在每次请求的时候该拦截器都会被触发(无法修改响应信息), 方便添加全局请求头/参数 - -示例 +轻量级拦截器(`RequestInterceptor`), 其Api更适合添加全局请求头/参数 ```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { +NetConfig.initialize(Api.HOST, this) { setRequestInterceptor(object : RequestInterceptor { override fun interceptor(request: BaseRequest) { request.param("client", "Net") @@ -65,4 +64,4 @@ NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { } ``` -可以看到`setRequestInterceptor`是set开头. 仅支持一个请求拦截器, 不像`addInterceptor`支持多个请求拦截器 \ No newline at end of file +可以看出`setRequestInterceptor`仅支持一个, `addInterceptor`支持多个拦截器 \ No newline at end of file diff --git a/docs/interval.md b/docs/interval.md index 094c54a4a..0f9a5cc4f 100644 --- a/docs/interval.md +++ b/docs/interval.md @@ -1,4 +1,4 @@ -轮询器(计时器)属于项目中常见需求, 而本框架拥有非常强大的轮询器工具. 支持以下特性 +Net自带轮询器(计时器), 包含以下特性 - 正计时 - 倒计时 @@ -8,11 +8,11 @@ - 页面销毁自动取消
-=== "指定轮循次数/间隔" +=== "限制次数/间隔" ```kotlin interval = Interval(100, 1, TimeUnit.SECONDS).life(this) // 自定义计数器个数的轮询器 ``` -=== "仅轮循间隔" +=== "无限执行" ```kotlin interval = Interval(1, TimeUnit.SECONDS) // 每秒回调一次, 不会自动结束 ``` @@ -31,7 +31,7 @@ interval.subscribe { }.start() ``` -| Interval函数 | 描述 | +| Interval | 描述 | |-|-| | start | 开始轮询器 | | stop | 停止 | @@ -43,6 +43,4 @@ interval.subscribe { | subscribe | 每次计时都会执行该回调 | | finish | 当计时器完成时执行该回调, 执行stop后也会回调 | | life | 指定生命周期自动取消轮询器 | -| onlyResumed | 当界面不可见时暂停, 当界面可见时继续 | - -[完整源码](https://github.com/liangjingkanji/Net/blob/master/sample/src/main/java/com/drake/net/sample/ui/fragment/SuperIntervalFragment.kt) \ No newline at end of file +| onlyResumed | 当界面不可见时暂停, 当界面可见时继续 | \ No newline at end of file diff --git a/docs/issues.md b/docs/issues.md index 5c6bd24ce..d927868b8 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -1,8 +1,19 @@ -## 常见问题 - -- [networkSecurityConfig导致无法打包](https://github.com/liangjingkanji/Net/issues/57) -- [没有我需要的请求参数类型](https://github.com/liangjingkanji/Net/issues/56) -- [没有我需要的功能](https://github.com/liangjingkanji/Net/issues/58) -- [使用inline reified封装请求函数导致崩溃](https://github.com/liangjingkanji/Net/issues/54) -- [错误提示 toast 内存泄漏](https://github.com/liangjingkanji/Net/issues/65) -- [如何使用Cookie](https://github.com/liangjingkanji/Net/issues/51) +Net最大的特点在于完美支持OkHttp的所有功能组件, +而Android上大部分都是基于OkHttp的网络请求解决方案
+所以如果存在Net没有实现的功能可以百度/谷歌搜索`"OkHttp如何实现XX"`, 然后可以很容易在Net中使用 + +## 低版本兼容 + +如果你是在 Android 5 (API level 21) +以上开发建议使用最新版本: [Net](https://github.com/liangjingkanji/Net)
+如果要求低至 Android 4.4 (API level 19) +请使用兼容版本: [Net-okhttp3](https://github.com/liangjingkanji/Net-okhttp3) + +如果需更低版本支持建议拉取仓库修改`minSdkVersion`后编译成aar使用 + +## 开发者交流 + +- [反馈问题](https://github.com/liangjingkanji/Net/issues) +- [其他开发者提及问题](https://github.com/liangjingkanji/Net/issues) +- [交流社区](https://github.com/liangjingkanji/Net/discussions) +- diff --git a/docs/kotlin-serialization.md b/docs/kotlin-serialization.md index 8bfbba6c1..0a2bc6678 100644 --- a/docs/kotlin-serialization.md +++ b/docs/kotlin-serialization.md @@ -1,25 +1,30 @@ -1. 从Net3开始支持使用[kotlin-serialization](https://github.com/Kotlin/kotlinx.serialization)(以下简称ks) +1. 从Net3开始支持使用 [Kotlin-Serialization](https://github.com/Kotlin/kotlinx.serialization) (以下简称ks) +2. 更多了解请阅读: [Kotlin最强解析库 - kotlin-serialization](https://juejin.cn/post/6963676982651387935) -1. 更多使用教程请阅读: [Kotlin最强解析库 - kotlin-serialization](https://juejin.cn/post/6963676982651387935) +## 特点 -
-## kotlin-serialization 特点 - -- kotlin官方库 +- 官方库 - 动态数据类型解析 - 自定义序列化器 - 支持ProtoBuf/JSON等数据结构序列化 -- 非空覆盖(即返回的Json字段为null则使用数据类默认值) -- 启用宽松模式, Json和数据类字段无需一致 +- 非空覆盖(即JSON字段为null则使用变量默认值) +- 启用宽松模式, JSON和数据类结构无需一致 - 解析任何类型(Map/List/Pair...) -> ks的数据模型类都要求使用注解`@Serializable`(除非自定义解析过程), 父类和子类都需要使用
-> 一般开发中都是使用[插件生成数据模型](model-generate.md), 所以这并不会增加工作量. 即使手写也只是一个注解, 但是可以带来默认值支持和更安全的数据解析 +!!! Failure "强制注解" + ks的数据类都要求使用注解`@Serializable`(除非自定义解析), 父类和子类都需要 + + + +!!! Success "生成默认值" + 使用[插件生成数据Model](model-generate.md), 支持自动生成默认值和注解 + + 生成默认值可避免后端返回异常数据导致解析崩溃, 以及反复编写判空代码 ## 依赖 -项目 build.gradle +Project build.gradle ```kotlin @@ -27,7 +32,7 @@ classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" // 和Kotlin插件同一个版本号即可 ``` -module build.gradle +Model build.gradle ```kotlin apply plugin: "kotlin-kapt" @@ -37,12 +42,11 @@ implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" ## 配置转换器 -这里使用Demo中的[SerializationConvert](https://github.com/liangjingkanji/Net/blob/master/sample/src/main/java/com/drake/net/sample/converter/SerializationConverter.kt)作演示. -如果你业务有特殊需要可以复制Demo中的转换器代码稍加修改 +这里使用示例代码中 [SerializationConvert](https://github.com/liangjingkanji/Net/blob/master/sample/src/main/java/com/drake/net/sample/converter/SerializationConverter.kt) 作为演示 === "全局配置" ```kotlin - NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { + NetConfig.initialize(Api.HOST, this) { setConverter(SerializationConvert()) // ... 其他配置 } @@ -58,7 +62,6 @@ implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" ```kotlin scopeNetLife { - // 这里后端直接返回的Json数组 val userList = Get>("list") { converter = SerializationConvert() }.await() @@ -72,9 +75,6 @@ scopeNetLife { data class UserModel(var name: String, var age: Int, var height: Int) ``` -> 具体解析返回的JSON中的某个字段请在转换器里面自定, 其注意如果存在父类, 父类和子类都需要使用`@Serializable`注解修饰
-如果想详细了解KS, 请阅读文章: [Kotlin最强解析库 - kotlin-serialization](https://juejin.cn/post/6963676982651387935) - ## 常用配置 以下为反序列化Json常用配置 @@ -91,7 +91,6 @@ val jsonDecoder = Json { @Serializable data class Data(var name:String = "", var age:Int = 0) ``` -> 建议为数据类所有字段设置默认值, 避免后端数据缺失导致解析异常, 也减少频繁判空操作 ### 启用默认值 diff --git a/docs/log-notice.md b/docs/log-notice.md index 1d66f83dc..1efc53960 100644 --- a/docs/log-notice.md +++ b/docs/log-notice.md @@ -1,10 +1,11 @@ -前面介绍过如何使用AndroidStudio来抓取网络日志. 但是我们可能需要在App使用过程或者让测试人员查看日志 +使用三方库 [Chucker](https://github.com/ChuckerTeam/chucker) 在通知栏记录网络请求日志 -这里介绍一个第三方日志拦截器[chucker](https://github.com/ChuckerTeam/chucker), 他会在Net发起请求后自动在设备通知栏显示网络请求记录, 点击通知可以跳转详情 +
+ ![](https://github.com/ChuckerTeam/chucker/raw/main/assets/chucker-http.gif){ width="300" } +
Chucker
+
- - -添加依赖 +依赖 ```groovy implementation "com.github.chuckerteam.chucker:library:3.5.2" @@ -34,4 +35,4 @@ override fun onCreate() { } ``` -更多自定义功能请查看[chucker](https://github.com/ChuckerTeam/chucker)主页 +自定义功能浏览 [Chucker](https://github.com/ChuckerTeam/chucker) 官网 diff --git a/docs/log-recorder.md b/docs/log-recorder.md index 78639be5d..62f692d16 100644 --- a/docs/log-recorder.md +++ b/docs/log-recorder.md @@ -1,23 +1,24 @@ -由于LogCat日志可读性差, 所以Net支持以下两种方案 +Net支持两种方式 +1. AndroidStudio的[Profiler](https://developer.android.com/studio/profile/network-profiler?hl=zh-cn) + - 动态曲线图 + - 可查看所有OkHttp的请求 + - 无法捕获启动一瞬间的请求 +
-1. 使用AndroidStudio的[Profiler](https://developer.android.com/studio/profile/network-profiler?hl=zh-cn)监听网络 - - 可以查看项目所有OkHttp框架发起的网络请求 - - 网络请求是动态曲线图, 查不太方便 - - 启动应用时立刻触发的请求无法捕捉 +2. [Okhttp Profiler](https://github.com/itkacher/OkHttpProfiler) + - 列表显示 + - 要求添加`LogRecordInterceptor` + - 原理是插件捕获LogCat日志, 线上环境请关闭 -
-2. 安装[Okhttp Profiler](https://github.com/itkacher/OkHttpProfiler)插件 (推荐) - - 列表显示请求 - - 要求添加Net的`LogRecordInterceptor`拦截器 - - 实际上是插件捕获logCat生成的日志, 线上环境需要关闭 +以下介绍`LogRecordInterceptor` ## 添加日志拦截器 ```kotlin hl_lines="2" -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { +NetConfig.initialize(Api.HOST, this) { addInterceptor(LogRecordInterceptor(BuildConfig.DEBUG)) } ``` @@ -28,7 +29,7 @@ NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { | requestByteCount | 请求日志信息最大字节数, 默认1MB | | responseByteCount | 响应日志信息最大字节数, 默认4MB | -这样会可以在LogCat看到日志输出, 但是我们要使用插件预览就需要第 2 步 +此时仅LogCat输出日志, 要预览请安装插件 ## 安装插件 @@ -43,7 +44,10 @@ NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { -> 请在每次使用前都先打开插件窗口, 如果有延迟或者不显示就反复打开下窗口 +!!! warning "不显示日志" + 请在请求前确保有打开过插件窗口, 如果依然不显示可以反复打开/关闭窗口 + + 每次AS更新都需要该插件作者适配, 可能存在beta版本作者没有适配情况 @@ -60,35 +64,36 @@ NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { | 清空 | 清空记录 | -## 自定义日志(解密) +## 自定义日志 -通过继承`LogRecordInterceptor`可以覆写函数自定义自己的日志输出逻辑 +继承`LogRecordInterceptor`复写函数实现自定义 -1. 如果你的请求体是被加密的内容, 你可以通过覆写`requestString`函数返回解密后的请求信息 -2. 如果你的响应体是被加密的内容, 你可以通过覆写`responseString`函数返回解密后的响应信息 +1. 复写`getRequestLog`返回解密请求参数 +2. 复写`getResponseLog`返回解密响应参数 -然后初始化时添加自己实现拦截器即可 +初始化时添加自己的拦截器 ```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { +NetConfig.initialize(Api.HOST, this) { addInterceptor(MyLogRecordInterceptor(BuildConfig.DEBUG)) } ``` -## LogCat过滤 -实际上Net的网络日志还是会被打印到LogCat, 然后通过插件捕捉显示. +## 日志过滤 -如果不想LogCat的冗余日志影响查看其它日志, 可以通过AndroidStudio的功能折叠隐藏, 添加一个`OKPREL_`过滤字段即可 +不想网络日志影响其他日志查看, 可以添加`OKPREL_`为日志折叠规则 ## 其他网络框架 -可能你项目中还残留其他网络框架, 也可以使用Net的日志记录器`LogRecorder`来为其他框架打印日志信息. 如果是基于OkHttp的框架那可以直接使用LogRecordInterceptor +`LogRecordInterceptor`属于OkHttp拦截器, 其他网络请求框架也可以使用 + +甚至可以直接使用`LogRecorder`输出日志 -| 函数 | 描述 | +| LogRecorder | 描述 | |-|-| | generateId | 产生一个唯一标识符, 用于判断为同一网络请求 | | recordRequest | 记录请求信息 | diff --git a/docs/model-generate.md b/docs/model-generate.md index 2be5e70a3..5fbdf56fd 100644 --- a/docs/model-generate.md +++ b/docs/model-generate.md @@ -1,6 +1,4 @@ -在请求网络数据过程中会需要编写大量的数据模型对象, 映射后端返回的数据创建数据对象. 因为我们业务逻辑中一般是直接数据对象更为方便 - -创建数据模型我不推荐手写, 错误率高且不方便. 我推荐使用`JSON To Kotlin Class` 插件自动生成 +应该没人会完全手写数据模型吧? 建议使用`JSON To Kotlin Class` 插件完成 ## 安装插件 @@ -18,9 +16,10 @@ -然后就会在你选中的分包下生成一个数据模型类了 +然后就会在选中的分包下生成一个数据模型类了 -> 不要输入json数组去生成, 输入数组里面的对象即可(否则生成的类会自动继承ArrayList). 然后请求的时候使用List泛型即可, 例如`Get>` +!!! Warning "不要生成数组" + 不要输入JSON数组生成(否则生成的类会继承ArrayList), 输入JSON对象, 请求使用`Get>` ## 高级设置 @@ -38,7 +37,7 @@ ### 使用的框架 -生成数据模型时会兼容你使用的框架, 例如Moshi和Ks可能需要注解, 然后会自动生成SerialName这些名称注解 +生成数据模型时会兼容你使用的框架, 例如Moshi和ks可能需要注解, 然后会自动生成SerialName这些名称注解 diff --git a/docs/okhttp-client.md b/docs/okhttp-client.md index 8b7937ff7..3cf60cabe 100644 --- a/docs/okhttp-client.md +++ b/docs/okhttp-client.md @@ -1,6 +1,4 @@ -每个请求都会存在一个客户端对象, 既OkHttpClient - -Net在全局维护了一个OkHttpClient对象, 在NetConfig.okHttpClient的字段 +Net全局持有一个`OkHttpClient`对象发起请求 ```kotlin object NetConfig { @@ -8,40 +6,38 @@ object NetConfig { } ``` -> 当然也支持创建一个新的客户端来发起请求(配置区别于全局客户端) - -## 全局OkHttpClient +## 全局 ```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { +NetConfig.initialize(Api.HOST, this) { // 此处this即为OkHttpClient.Builder } ``` -## 单例OkHttpClient +## 单例 每个请求可能存在独立的OkHttpClient配置, 我们可以单例配置客户端选项 -1. 在全局的OkHttpClient配置基础下修改 - -```kotlin -scopeNetLife { - tv_response.text = Get("https://github.com/liangjingkanji/Net/") { - setClient { - // 此处this即为OkHttpClient.Builder - trustCertificate() - } - }.await() -} -``` - -2. 完全重新创建一个OkHttpClient, 一般情况不推荐重新创建一个OkHttpClient, 因为一个新的OkHttpClient会重新创建线程池/连接池等造成内存消耗等 - -```kotlin -scopeNetLife { - tv_response.text = Get("https://github.com/liangjingkanji/Net/") { - okHttpClient = OkHttpClient.Builder().build() - }.await() -} -``` \ No newline at end of file +=== "修改全局客户端" + ```kotlin + scopeNetLife { + tv_response.text = Get(Api.PATH) { + setClient { + // 此处this即为OkHttpClient.Builder + trustCertificate() + } + }.await() + } + ``` + 在全局的OkHttpClient配置基础下修改 + +=== "创建新客户端" + ```kotlin + scopeNetLife { + tv_response.text = Get(Api.PATH) { + okHttpClient = OkHttpClient.Builder().build() + }.await() + } + ``` + 创建新的OkHttpClient, 一般不推荐, 因为新OkHttpClient会重新创建线程池/连接池等造成内存消耗 \ No newline at end of file diff --git a/docs/practice.md b/docs/practice.md deleted file mode 100644 index 5cadd8326..000000000 --- a/docs/practice.md +++ /dev/null @@ -1,11 +0,0 @@ -本章节毫无意义, 仅仅是有些人完全没有下载过本仓库, 且不知道项目中存在sample模块 - -## 使用场景 -实际上本框架使用也就两三行代码而已, 并且常见使用场景在本项目的sample模块中也进行了全面的讲解 - -

- -## 结合案例 -但有些人可能完全是新手, 那么推荐结合BRV的sample, 里面有常见的界面数据加载示例 - -

\ No newline at end of file diff --git a/docs/read-cache.md b/docs/read-cache.md deleted file mode 100644 index a937006ef..000000000 --- a/docs/read-cache.md +++ /dev/null @@ -1,59 +0,0 @@ -Net_v2基于[Kalle](https://github.com/yanzhenjie/Kalle)开发, 支持Kalle的9种缓存模式 - -缓存模式要求在初始化的时候开启 - -```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { - cacheEnabled() // 开启缓存 -} -``` - -=== "请求缓存或网络" - ```kotlin - scopeNetLife { - // 先读取缓存, 如果缓存不存在再请求网络 - tvFragment.text = Get("api", cache = CacheMode.READ_CACHE_NO_THEN_NETWORK).await() - Log.d("日志", "读取缓存") - } - ``` -=== "读取缓存然后请求网络" - - ```kotlin - scopeNetLife { - // 然后执行这里(网络请求) - tvFragment.text = Post("api", cache = CacheMode.NETWORK_YES_THEN_WRITE_CACHE).await() - Log.d("日志", "网络请求") - }.preview { - // 先执行这里(仅读缓存), 任何异常都视为读取缓存失败 - tvFragment.text = Get("api", cache = CacheMode.READ_CACHE).await() - Log.d("日志", "读取缓存") - } - ``` - -预读模式本质上就是创建一个`preview`附加作用域, 里面的所有异常崩溃都会被静默捕捉(算作缓存失败), 会优先于`scope*`执行, 然后再执行scope本身, -而且一旦缓存读取成功(`preview`内部无异常)即使网络请求失败也可以不提醒用户任何错误信息(可配置) - -
- -> `preview`并没有说只能用在网络缓存上, 也可以用于其他的处理场景 - -
- -## 缓存模式 - -缓存模式属于`CacheMod`枚举, 建议开发者浏览缓存模式的源码和注释,有助于理解和更好的使用缓存模式。 - -| 常量 | 描述 | -| ------------------------------ | ------------------------------------------------------------ | -| `HTTP` | 发起请求前如果本地已经有缓存,如果缓存未过期则返回缓存数据,
如果过期则带上缓存头去服务器做校验。如果服务器响应304则返回缓存数据,否则读取服务器数据,
根据服务器响应头来决定是否写入缓存数据 | -| `HTTP_YES_THEN_WRITE_CACHE` | 发起请求前如果本地已经有缓存则带缓存头,服务器响应304才返回缓存数据,否则读取服务器数据,
并写入缓存数据 | -| `NETWORK` | 发起请求前不管本地是否有缓存,都不会带上缓存头,
不论服务器响应头如何,绝不写入缓存数据 | -| `NETWORK_YES_THEN_HTTP` | 发起请求前不管本地是否有缓存,都不会带上缓存头,
根据服务器响应头来决定是否写入缓存数据 | -| `NETWORK_YES_THEN_WRITE_CACHE` | 发起请求前不管本地是否有缓存,都不会带上缓存头,
请求成功后写入缓存数据 | -| `NETWORK_NO_THEN_READ_CACHE` | 发起请求前不管本地是否有缓存,都不会带上缓存头,请求失败后尝试读取缓存,
根据服务器响应头来决定是否写入缓存数据 | -| `READ_CACHE ` | 仅读取缓存 | -| `READ_CACHE_NO_THEN_NETWORK` | 读取缓存,如果缓存不存在就请求网络,
不写入缓存数据 | -| `READ_CACHE_NO_THEN_HTTP` | 先读取缓存,如果缓存不存在就请求网络,
根据服务器响应头来决定是否写入缓存数据 | -| `READ_CACHE_NO_THEN_NETWORK_THEN_WRITE_CACHE` | 先本地有缓存则读取缓存,如果没有缓存则读取网络并且写入缓存,
该模式请求成功后会永久使用缓存, 但你可以指定动态的cacheKey来让缓存失效
例如一天后失效, 可以做到客户端完全控制缓存 | - -
\ No newline at end of file diff --git a/docs/repeat-request.md b/docs/repeat-request.md index 99f260f94..a14219524 100644 --- a/docs/repeat-request.md +++ b/docs/repeat-request.md @@ -1,25 +1,20 @@ -某个重复发起的请求, 在发起的时候自动取消旧的网络请求.
+常用于筛选列表请求, 选择新的筛选条件时应将上次未完成的取消后再发起请求 -这种应用场景常见于筛选菜单, 每次点击菜单都会发起网络请求返回筛选后的列表, 但是请求未完成时, 用户又点击了新的筛选条件, 这个时候应该取消上次请求, 重新发起新的请求
+在Net禁止重复请求仅2行代码 -这个需求在Net中非常好实现, 保存一个变量即可 - -```kotlin +```kotlin hl_lines="4" var scope: AndroidScope? = null -btn_request.setOnClickListener { - tv_result.text = "请求中" - scope?.cancel() // 如果存在则取消 +btnFilter.setOnClickListener { + scope?.cancel() scope = scopeNetLife { val result = Post("api").await() - Log.d("日志", "请求到结果") // 你一直重复点击"发起请求"按钮会发现永远无法拿到请求结果, 因为每次发起新的请求会取消未完成的 - tv_result.text = result } } ``` -当`scope`不为空时即表示存在上个请求, 我们无论上个请求是否完成都调用`cancel`函数保证取消即可 +当`scope`不为空时表示存在旧请求, 无论旧请求是否完成都可以调用`cancel()`保证取消即可 -> 详细的关于取消网络请求的操作查看: [取消请求](cancel.md)
-> 可以限制部分重复请求一定时间内读取缓存: [缓存有效期](/cache/#_3) \ No newline at end of file +1. [取消请求](cancel.md)
+2. 限制时间强制使用缓存, [配置缓存有效期](cache.md#_3) \ No newline at end of file diff --git a/docs/request.md b/docs/request.md index 1577fafbb..478767757 100644 --- a/docs/request.md +++ b/docs/request.md @@ -1,73 +1,44 @@ -Net中关于请求的类只有两个类和他们共同的抽象父类 -```kotlin -BaseRequest - |- UrlRequest - |- BodyRequest -``` - - -根据请求方法不同使用的Request也不同 +!!! question "请求参数" + 根据请求方式不同请求参数分为两类 -```kotlin -GET, HEAD, OPTIONS, TRACE, // Url request -POST, DELETE, PUT, PATCH // Body request -``` + UrlRequest: GET, HEAD, OPTIONS, TRACE // Query(请求参数位于Url中)
+ BodyRequest: POST, DELETE, PUT, PATCH // Body(请求体为流) -代码示例 +使用示例 ```kotlin scopeNetLife { - Get("api") { - // this 即为 UrlRequest - }.await() - - Post("api") { - // this 即为 BodyRequest + val userInfo = Post(Api.LOGIN) { + param("username", "drake") + param("password", "6f2961eb44b12123393fff7e449e50b9de2499c6") }.await() } ``` -## 请求参数 - -```kotlin -scopeNetLife { // 创建作用域 - // 这个大括号内就属于作用域内部 - val data = Get("https://github.com/liangjingkanji/Net/"){ - param("u_name", "drake") - param("pwd", "123456") - }.await() // 发起GET请求并返回`String`类型数据 -} -``` - -|请求函数|描述| +|函数|描述| |-|-| -|`param`|支持基础类型/文件/RequestBody/Part| -|`json`|请求参数为JSONObject/JsonArray/String| -|`setQuery/addQuery`|设置/添加Url参数, 如果当前请求为Url请求则该函数等效于`param`函数| +|`param`| Url请求时为Query, Body请求时为表单/文件| +|`json`|JSON字符串| +|`setQuery/addQuery`|Url中的Query参数, 如果当为Url请求则该函数等效`param`| |`setHeader/addHeader`|设置/添加请求头| -如果没有添加文件/流那么就是通过BodyRequest内部的`FormBody`发起请求. 反之就是通过`MultipartBody`发起请求. - -> 当然你可以完全自定义Body来请求, 譬如以下的Json请求 - +## JSON请求 -## Json请求 +这里仅演示三种参数类型上传JSON, 详细请查看方法重载 -这里提供三种创建Json请求的示例代码. 酌情选用 - -=== "JSON键值对(推荐)" +=== "Pair(推荐)" ```kotlin val name = "金城武" val age = 29 val measurements = listOf(100, 100, 100) - - scopeNetLife { - tvFragment.text = Post("api") { - // 只支持基础类型的值, 如果值为对象或者包含对象的集合/数组会导致其值为null - json("name" to name, "age" to age, "measurements" to measurements) - }.await() - } + + scopeNetLife { + tvFragment.text = Post("api") { + // 只支持基础类型的值, 如果值为对象或者包含对象的集合/数组会导致其值为null + json("name" to name, "age" to age, "measurements" to measurements) + }.await() + } ``` === "JSONObject" @@ -87,34 +58,34 @@ scopeNetLife { // 创建作用域 } ``` -=== "自定义的body" +=== "自定义RequestBody" ```kotlin val name = "金城武" val age = 29 val measurements = listOf(100, 100, 100) - + scopeNetLife { tvFragment.text = Post("api") { - body = MyJsonBody(name, age, measurements) + body = CustomizerJSONBody(name, age, measurements) }.await() } ``` -对于某些可能JSON请求参数存在固定值: +个别项目JSON中存在固定值, 开发者不想每次都写一遍: -1. 可以考虑继承RequestBody来扩展出自己的新的Body对象, 然后赋值给`body`字段 -2. 添加请求拦截器[RequestInterceptor](/interceptor/#_1) +1. 实现RequestBody默认添加参数 +2. 使用请求拦截器来添加全局参数 [RequestInterceptor](/interceptor/#_1) -## 自定义请求函数 +## 自定义扩展函数 -前面提到`json(Pair)`函数不支持对象值, 因为框架内部使用的`org.json.JSONObject`其不支持映射对象字段 +前面提到`json()`不能传对象, 因为内部使用`org.json.JSONObject`不支持映射对象字段 -这里可以创建扩展函数来支持你想要的Json解析框架, 比如以下常用的Json解析框架示例 +那可以自己创建扩展函数来使用支持解析对象的序列化框架, 如下 === "Gson" ```kotlin fun BodyRequest.gson(vararg body: Pair) { - this.body = Gson().toJson(body.toMap()).toRequestBody(MediaConst.JSON) + this.body = Gson().toJson(body.toMap()).toRequestBody(MediaConst.JSON) } ``` === "FastJson" @@ -135,23 +106,25 @@ scopeNetLife { } ``` -- 举一反三可以创建其他功能自定义的请求函数 -- 扩展函数要求为顶层函数, 即直接在文件中 (kotlin基础语法) - ## 全局请求参数 -对于动态生成的全局请求头或参数都可以通过实现`RequestInterceptor`来设置全局的请求拦截器来添加, 如果RequestInterceptor不满足你的需求可以使用拦截器(Interceptor)来实现 +使用`RequestInterceptor`请求拦截器添加全局参数/请求头, 如果不满足需求可以使用更复杂的`Interceptor` ```kotlin -NetConfig.initialize("https://github.com/liangjingkanji/Net/", this) { - // 添加请求拦截器, 每次请求都会触发的, 可配置全局/动态参数 - setRequestInterceptor(MyRequestInterceptor()) +class GlobalHeaderInterceptor : RequestInterceptor { + + /** 本方法每次请求发起都会调用, 这里添加的参数可以是动态参数 */ + override fun interceptor(request: BaseRequest) { + request.setHeader("client", "Android") + request.setHeader("token", UserConfig.token) + } } ``` -## 请求函数 - -关于全部的请求配置选项推荐阅读函数文档或者阅读源码. Net提供清晰的函数结构浏览方便直接阅读源码 - - +```kotlin +NetConfig.initialize(Api.HOST, this) { + setRequestInterceptor(GlobalHeaderInterceptor()) +} +``` +更多请求参数相关请阅读Api文档/函数列表 \ No newline at end of file diff --git a/docs/scope.md b/docs/scope.md index 85d631e39..6dc1b3a39 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -1,28 +1,26 @@ -协程请求要求在协程的作用域中调用, 这里介绍如何创建不同的作用域获取不同的功能 +创建不同协程作用域可以实现不同的功能 -本质上Net的请求动作函数返回的是一个Deferred对象. 可以在任何协程作用域内执行. 但是考虑到完整的生命周期和错误处理等推荐使用Net内部定义的作用域. - -> 发生在Net作用域内的任何异常都会被捕获, 有效减少应用崩溃率. 如果配合[kotlin-serialization](kotlin-serialization.md)还可以解决因服务器返回null字段导致的崩溃 +!!! Success "减少崩溃" + Net所有作用域内抛出异常都会被捕获到, 可以防止应用崩溃
## 异步任务的作用域 -创建可以捕捉异常的协程作用域, 但是不会触发`NetErrorHandler`(全局错误处理者). 该作用域于一般用于普通的异步任务 +可以捕捉异常的协程作用域, 用于构建普通异步任务 |函数|描述| |-|-| -|`scope`|创建最基础的作用域, 所有作用域都包含异常捕捉| -|`scopeLife`|创建跟随生命周期取消的作用域| +|`scope`|创建最基础的作用域| +|`scopeLife`|创建跟随生命周期自动取消的作用域| |`ViewModel.scopeLife`|创建跟随ViewModel生命周期的作用域, [如何在ViewModel创建作用域](view-model.md)| ## 网络请求的作用域 -网络请求的作用域可以根据生命周期自动取消网络请求, 发生错误也会自动弹出吐司(可以自定义或者取消), 并且具备一些场景的特殊功能(例如加载对话框, 缺省页, 下拉刷新等) - -网络请求的作用域比上面提到的异步任务的作用域多的区别就是 +除异步任务外还适用于网络请求场景的作用域, 对比上面`异步任务的作用域`区别: -1. 发生错误会触发全局错误处理`NetErrorHandler` -2. 具备一些特殊场景功能, 比如自动下拉刷新, 自动显示加载库等 +1. 发生错误自动吐司(可以自定义或者取消) +2. 发生错误会触发全局错误处理`NetErrorHandler` +3. 具备一些特殊场景功能, 比如根据网络请求结果自动处理下拉刷新/上拉加载/缺省页/加载框的状态 | 函数 | 描述 | |-|-| @@ -33,23 +31,25 @@ |`PageRefreshLayout.scope`|创建跟随[PageRefreshLayout](https://github.com/liangjingkanji/BRV)生命周期的作用域| |`StateLayout.scope`|创建跟随[StateLayout](https://github.com/liangjingkanji/BRV)生命周期的作用域| -
-> PageRefreshLayout/StateLayout 属于[BRV](https://github.com/liangjingkanji/BRV)框架中的布局, 用于支持[自动化缺省页/下拉刷新](auto-state.md) -
+!!! Failure "区分函数接受者" + 注意`StateLayout.scope`等存在`函数接受者`的方法和`scope`属于两个方法, 严禁混用 +!!! quote "第三方库支持" + PageRefreshLayout/StateLayout 属于第三方开源项目 [BRV](https://github.com/liangjingkanji/BRV) + 框架中的布局, 可用于支持[自动化缺省页/下拉刷新](auto-state.md)
-> 如果想了解详细的协程使用方式, 可以查看一篇文章: [最全面的Kotlin协程: Coroutine/Channel/Flow 以及实际应用](https://juejin.im/post/6844904037586829320) +如果想更了解协程使用方式, +可以阅读一篇文章: [最全面的Kotlin协程: Coroutine/Channel/Flow 以及实际应用](https://juejin.im/post/6844904037586829320) -有时候可能面临嵌套的`scope*`函数或者作用域内有子作用域情况, 这个时候的生命周期是如何 +## 嵌套作用域 - -## 嵌套Scope +有时候可能面临内嵌`scopeXX`函数(嵌套作用域), 这时候生命周期如下 ```kotlin hl_lines="5" -scopeNet { +scopeNetLife { val task = Post("api0").await() - scopeNet { + scopeNetLife { val task = Post("api0").await() // 此时发生请求错误 }.catch { // A @@ -59,9 +59,9 @@ scopeNet { } ``` -- 以下嵌套作用域错误将会仅发生在`A`处, 并被捕获, 同时不影响外部`scopeNet`的请求和异常捕获 -- 两个`scopeNet`的异常抛出和捕获互不影响 -- `scopeNet/scopeDialog/scope`等函数同理 +- 错误将在`A`处可以获取到, 且不影响外部`scopeNetLife`的请求 +- 两个`scopeNetLife`的异常抛出和捕获互不影响 +- `scopeXX()`等函数同理 ## 子作用域 diff --git a/docs/switch-thread.md b/docs/switch-thread.md deleted file mode 100644 index 8a223c5e5..000000000 --- a/docs/switch-thread.md +++ /dev/null @@ -1,50 +0,0 @@ -所有`scope*`前缀函数的创建的作用域都默认情况下为主线程, 即可以在作用域内直接操作UI - -既然是默认当然也可以直接修改scope内的作用域线程也可以在里面进行多次切换 - -> 在协程中切换线程只需要切换调度器即可 - -## 切换作用域调度器 - -```kotlin -scopeNetLife(dispatcher = Dispatchers.IO) { - binding.tvFragment.text = Get("api").await() -} -``` - -## 在作用域内部切换 - -可能某些情况只是作用域内部分阻塞任务需要在其他线程执行. 我们可以直接在作用域内部进行多次的线程切换 - -=== "主线程作用域内切换子线程" - ```kotlin hl_lines="2" - scopeNetLife { - binding.tvFragment.text = withIO { - // 假设此处是一个IO读写阻塞任务 - return "读出结果" - } - } - ``` - -=== "子线程作用域内切换主线程" - ```kotlin hl_lines="2" - scopeNetLife(dispatcher = Dispatchers.IO) { - binding.tvFragment.text = withMain { - // 假设此处是一个IO读写阻塞任务 - return "读出结果" - } - } - ``` - -|函数|描述| -|-|-| -|[withMain](api/-net/com.drake.net.utils/with-main.html)|切换到主线程| -|[withIO](api/-net/com.drake.net.utils/with-i-o.html)|切换到IO线程| -|[withDefault](api/-net/com.drake.net.utils/with-default.html)|切换到默认线程(属于子线程)| -|[withUnconfined](api/-net/com.drake.net.utils/with-unconfined.html)|切换到无限制调度器, 其取决于上一个执行的线程切换| -|launch|无返回值的协程挂起函数, 可指定线程| -|async|有返回值的协程挂起函数, 但得通过`await()`返回值. 可指定线程| -|[runMain](api/-net/com.drake.net.utils/with-unconfined.html)|切换到主线程, 该函数可以在任何地方调用不限于协程作用域| - -- `with*`函数属于调用就立即执行, 在作用域内会阻塞(不会阻塞主线程) -- `launch/async` 属于执行并发任务, 两者区分就是有无返回值 diff --git a/docs/sync-request.md b/docs/sync-request.md index 569781cd1..6dcfd61e6 100644 --- a/docs/sync-request.md +++ b/docs/sync-request.md @@ -1,8 +1,13 @@ -Net支持在当前线程执行, 会阻塞当前线程的同步请求 -- `execute` +Net支持在当前线程执行阻塞线程的同步请求 -这里介绍的是不使用协程的同步请求. 由于Android主线程不允许发起网络请求, 这里我们得随便创建一个子线程才可以开发起同步请求 +!!! question "什么是同步请求" + 即上个请求结束才会发起下个请求, 实际上协程也可以实现但是他不会阻塞线程 -=== "同步请求" + 同步请求应用场景一般是在拦截器(执行在子线程)中使用 + +因为Android主线程不允许发起网络请求, 这里我们创建一个子线程来演示 + +=== "返回数据" ```kotlin thread { @@ -12,31 +17,22 @@ Net支持在当前线程执行, 会阻塞当前线程的同步请求 -- `execute } } ``` -=== "toResult" + +=== "返回Result" ```kotlin thread { - val result = Net.post("api").toResult().getOrDefault("请求发生错误, 我这是默认值") + val result = Net.post("api").toResult().getOrDefault("请求发生错误, 我是默认值") tvFragment?.post { tvFragment?.text = result // view要求在主线程更新 } } ``` -1. `execute`在请求发生错误时会抛出异常 -2. `toResult`不会抛出异常, 通过`exception*`函数来获取异常信息, 且支持默认值等特性 +1. `execute`在请求错误时会直接抛出异常 +2. `toResult`不会抛出异常, 可`getOrThrow/exceptionOrNull`等返回异常对象 + + -> 同步请求应用场景一般是在拦截器中使用, 拦截器默认是子线程 -作用域具体介绍可以看[创建作用域](scope.md) -|请求函数|描述| -|-|-| -|Net.get|标准Http请求方法| -|Net.post|标准Http请求方法| -|Net.head|标准Http请求方法| -|Net.options|标准Http请求方法| -|Net.trace|标准Http请求方法| -|Net.delete|标准Http请求方法| -|Net.put|标准Http请求方法| -|Net.patch|标准Http请求方法| diff --git a/docs/tag.md b/docs/tag.md index 4a0866287..fa4bfe84a 100644 --- a/docs/tag.md +++ b/docs/tag.md @@ -1,9 +1,9 @@ -Net支持两种类型数据贯穿整个请求流程(请求 -> 拦截器 -> 转换器) +Net支持两种方式携带数据, 贯穿整个请求流程(请求 -> 拦截器 -> 转换器) - tag: `HashMap, Any?>` 标签 - extra: `HashMap` 额外数据 -> 他们的区别是key是Class还是String类型, 具体使用哪一种请根据自己方便来 +区别为key是`Class`还是`String`, 自由选择 ## 标签使用 @@ -18,8 +18,10 @@ scopeNetLife { } ``` -> `tagOf(Person())` 等效 `tag(Person::class.java, Person())`, 只是使用泛型推断区别
-> 但`tag(Person())` 等效 `tag(Any::class.java, Person())`, 可以查看方法实现 +!!! warning "泛型" + `tagOf(Person())` 等于 `tag(Person::class.java, Person())`, 使用泛型推断 + + 但`tag(Person())` 等于 `tag(Any::class.java, Person())`, 为OkHttp自带方法 ### 2) 拦截器中读取标签 ```kotlin hl_lines="4" @@ -52,7 +54,7 @@ class MyConvert : NetConvert {
-我们通过Request的函数可以读取/写入标签/额外数据 +通过`Request`可以读写数据 | 方法 | 描述 | |-|-| diff --git a/docs/thread.md b/docs/thread.md new file mode 100644 index 000000000..eaf79fcb1 --- /dev/null +++ b/docs/thread.md @@ -0,0 +1,49 @@ +所有`scopeXX`作用域内为主线程, 可直接更新视图 + +!!! question "调度器" + 协程中的调度器实际上为线程池, 通过切换调度器可以切换到不同线程上 + +## 切换调度器 + +```kotlin +scopeNetLife(dispatcher = Dispatchers.IO) { + binding.tvFragment.text = Get("api").await() +} +``` + +## 作用域内部切换 + +有时需开启新的线程处理耗时任务 + +=== "主线程作用域内切换子线程" + ```kotlin hl_lines="2" + scopeNetLife { + binding.tvFragment.text = withIO { + // 假设此处是一个IO读写阻塞任务 + return "读出结果" + } + } + ``` + +=== "子线程作用域内切换主线程" + ```kotlin hl_lines="2" + scopeNetLife(dispatcher = Dispatchers.IO) { + binding.tvFragment.text = withMain { + // 假设此处是一个IO读写阻塞任务 + return "读出结果" + } + } + ``` + +| 函数 | 描述 | +| ------------------------------------------------------------ | ----------------------------------------------------------- | +| [withMain](api/-net/com.drake.net.utils/with-main.html) | 切换到主线程 | +| [withIO](api/-net/com.drake.net.utils/with-i-o.html) | 切换到IO线程 | +| [withDefault](api/-net/com.drake.net.utils/with-default.html) | 切换到子线程 | +| [withUnconfined](api/-net/com.drake.net.utils/with-unconfined.html) | 切换到无限制调度器, 其取决于上一个执行的线程切换 | +| launch | 无返回值的挂起函数, 可指定线程 | +| async | 有返回值的挂起函数, 通过`await()`返回值, 可指定线程 | +| [runMain](api/-net/com.drake.net.utils/with-unconfined.html) | 切换到主线程, 该函数不属于协程可以在任何地方调用 | + +- `withXX()` 协程阻塞挂起 +- `launch()/async()` 非阻塞执行, 两者区别是有无返回值 diff --git a/docs/timing.md b/docs/timing.md index bfb0f0b28..d33bfd4a3 100644 --- a/docs/timing.md +++ b/docs/timing.md @@ -1,8 +1,10 @@ -以下仅提供一些如何实现定时/限时请求功能的思路, 并不限定只有以下描述的方式可以实现 +以下为提供实现定时/限时请求思路, 并不限定只有以下方式 ## 限时请求 +即超过指定时间后立即取消请求 + ```kotlin scopeDialog { // 当接口请求在100毫秒内没有完成会抛出异常TimeoutCancellationException @@ -41,7 +43,6 @@ scopeNetLife { ```kotlin scopeNetLife { - // 每两秒请求一次, 总共执行10次 while (true) { delay(1.toDuration(DurationUnit.SECONDS)) val data = diff --git a/docs/track.md b/docs/track.md new file mode 100644 index 000000000..15ff2d3e5 --- /dev/null +++ b/docs/track.md @@ -0,0 +1,24 @@ +Net中网络请求异常会LogCat输出, 除非开发者修改全局错误处理 + + +演示访问一个不存在的请求路径 +```kotlin +scopeNetLife { + tvFragment.text = Get("https://githuberror.com/liangjingkanji/Net/").await() +} +``` + +LogCat可以看到异常堆栈信息, 包含具体Url和代码位置 + + + + +### 关闭日志 + +可以关闭日志打印 + +```kotlin +NetConfig.initialize(Api.HOST, this) { + setDebug(false) // 关闭日志, 一般使用 BuildConfig.DEBUG +} +``` diff --git a/docs/updates.md b/docs/updates.md index 009156d46..ad79871f2 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -241,7 +241,7 @@ - 修复未知的TypeToken访问权限问题 ## 3.0.6 -- 所有Json解析框架都可以解析`List`等嵌套泛型数据结构: [特殊结构解析](convert-special.md) +- 所有Json解析框架都可以解析`List`等嵌套泛型数据结构 ## 3.0.5 - 修复Path编码问题 diff --git a/docs/upload-file.md b/docs/upload-file.md index 95f6665a5..9d0b2d19a 100644 --- a/docs/upload-file.md +++ b/docs/upload-file.md @@ -1,5 +1,3 @@ -上传文件和普通接口请求区别不大 - ```kotlin scopeNetLife { Post(Api.UPLOAD) { @@ -8,11 +6,11 @@ scopeNetLife { } ``` -使用`addUploadListener`添加上传进度监听器, 监听上传进度具体介绍在[进度监听](progress.md)中 +使用`addUploadListener`添加上传进度监听器, 介绍在[进度监听](progress.md)中 ## 指定类型 -默认会根据文件的后缀名产生MediaType. 但是如果你想自定义MediaType可以直接创建RequestBody参数 +默认会根据文件的后缀名产生MediaType, 如果像自定义MediaType可以直接创建RequestBody ```kotlin scopeNetLife { diff --git a/docs/view-model.md b/docs/view-model.md index 0a26d8c37..8477991ea 100644 --- a/docs/view-model.md +++ b/docs/view-model.md @@ -1,30 +1,44 @@ -Net支持在ViewModel中创建网络请求/异步任务, 并且在ViewModel被销毁时自动取消 +Net支持在ViewModel中创建网络请求/异步任务 +!!! Warning "不推荐" + 1. 网络请求不一定要写在ViewModel
+ 2. 网络请求不要写接口回调 + 3. 可以在Activity中直接返回请求结果 -## 使用 -和一般网络请求没有区别, 在ViewModel中使用函数`scopeLife/scopeNetLife`这两个函数创建作用域即可, 具体介绍看[作用域](scope.md) +## 自动生命周期 + +- [示例代码](https://github.com/liangjingkanji/Net/blob/c4d7c4cde6a34b9fa97a75cb357276b75432f8d1/sample/src/main/java/com/drake/net/sample/ui/fragment/ViewModelRequestFragment.kt) + +使用`scopeXXLife()`创建作用域, 在ViewModel被销毁时自动取消请求 ```kotlin class UserViewModel : ViewModel() { // 用户信息 - var userInfo: MutableLiveData = MutableLiveData() + private var userInfo: MutableLiveData = MutableLiveData() /** - * 拉取用户信息, 会自动通知页面更新, 同时页面销毁会自动取消网络请求 + * 使用LiveData接受请求结果, 将该liveData直接使用DataBinding绑定到页面上, 会在请求成功自动更新视图 */ fun fetchUserInfo() = scopeNetLife { - userInfo.value = Get("api").await() + userInfo.value = Get(Api.GAME).await() } -} -``` - -1. 不建议使用LiveData实现MVVM. 应当使用DataBinding. 但LiveData像ObservableField一样使用 -1. 简单地业务直接在Activity/Fragment中进行请求会更加方便 -1. ViewModel这个类本质是属于防止数据意外销毁或者桥接VM, 但不是每个页面都有这种需求 + /** + * 开始非阻塞异步任务 + * 返回Deferred, 调用await()才会返回结果 + */ + fun fetchList(scope: CoroutineScope) = scope.Get(Api.TEST) -
+ /** + * 开始阻塞异步任务 + * 直接返回结果 + */ + suspend fun fetchPrecessData() = coroutineScope { + val response = Get(Api.TEST).await() + response + "处理数据" + } +} +``` - diff --git a/mkdocs.yml b/mkdocs.yml index 6cdb21437..f7a29bf4a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,37 +1,76 @@ site_name: Net -site_author: drake site_description: Net document -copyright: Copyright © 2018 - 2020 劉強東 - -repo_name: GitHub repo_url: https://github.com/liangjingkanji/Net +site_author: 劉強東 +copyright: Copyright © 2018 - 2020 劉強東 +repo_name: GitHub +docs_dir: 'docs' +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/liangjingkanji + - icon: fontawesome/brands/qq + link: https://raw.githubusercontent.com/liangjingkanji/liangjingkanji/master/img/group-qrcode.png + - icon: fontawesome/brands/twitch + link: https://github.com/liangjingkanji/Net/discussions extra_css: - css/extra.css - -docs_dir: docs - theme: name: material custom_dir: docs/material + favicon: img/book-open.svg + logo: img/book-open.svg palette: - scheme: drake - primary: white + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white font: false - language: 'zh' + language: zh features: + - navigation.top + - navigation.prune + - navigation.footer + - navigation.instant - search.highlight - + - search.suggest + - search.share + - content.code.copy + - content.code.annotate +plugins: + - offline + - search: + separator: '[\s\-,:!=\[\]()"/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;' + lang: + - en + - zh markdown_extensions: - toc: permalink: true - pymdownx.tasklist: custom_checkbox: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - attr_list + - def_list + - md_in_html - admonition + - pymdownx.highlight - pymdownx.details - - pymdownx.superfences - - pymdownx.inlinehilite - - pymdownx.tabbed + - pymdownx.caret + - pymdownx.keys + - pymdownx.mark + - pymdownx.tilde + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg nav: - 使用: index.md @@ -41,25 +80,25 @@ nav: - 请求参数: request.md - 全局配置: config.md - 请求结果: - - 默认结果: default-response.md - - 自定义转换器: converter.md - - 自定义结构解析: convert-special.md + - 转换器: converter.md + - 自定义转换器: converter-customize.md + - 自定义结构解析: converter-struct.md - Kotlin-Serialization: kotlin-serialization.md - 数据类生成插件: model-generate.md - 自动化: - 自动加载框: auto-dialog.md - 自动缺省页: auto-state.md - 自动下拉刷新: auto-refresh.md - - 自动分页加载: auto-page.md - - 切换线程: switch-thread.md + - 自动分页加载: auto-pull.md + - 切换线程: thread.md - ViewModel: view-model.md - - 异常追踪: exception-track.md + - 异常追踪: track.md - 错误处理: - - 默认错误处理: error-default.md - - 单例错误捕获: error-single.md - - 全局错误捕获: error-global.md + - 错误处理: error.md + - 单例捕获: error-single.md + - 全局捕获: error-global.md - 自定义错误提示: error-tip.md - - 自定义异常抛出: error-exception.md + - 自定义异常抛出: error-throws.md - OkHttpClient: okhttp-client.md - 拦截器: interceptor.md - Https证书: https.md @@ -71,7 +110,7 @@ nav: - 进度监听: progress.md - 取消请求: cancel.md - 重复请求: repeat-request.md - - 自动搜索: debounce.md + - 搜索节流: debounce.md - 最快请求结果: fastest.md - 日志插件: log-recorder.md - 通知栏日志: log-notice.md @@ -79,7 +118,6 @@ nav: - Callback: callback.md - 轮询器/倒计时: interval.md - 社区讨论: https://github.com/liangjingkanji/Net/discussions - - 常见问题: https://github.com/liangjingkanji/Net/blob/master/docs/issues.md - - 项目实践: practice.md + - 常见问题: issues.md - 更新日志: updates.md - 3.x文档: api/index.html diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro index bc5d812e1..481bb4348 100644 --- a/sample/proguard-rules.pro +++ b/sample/proguard-rules.pro @@ -1,5 +1,5 @@ # Add project specific ProGuard rules here. -# You can control the setKalle of applied configuration files using the +# You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see diff --git a/sample/src/main/java/com/drake/net/sample/vm/UserViewModel.kt b/sample/src/main/java/com/drake/net/sample/vm/UserViewModel.kt index aa51e4254..2372f561f 100644 --- a/sample/src/main/java/com/drake/net/sample/vm/UserViewModel.kt +++ b/sample/src/main/java/com/drake/net/sample/vm/UserViewModel.kt @@ -10,37 +10,28 @@ import kotlinx.coroutines.coroutineScope /** - * 强烈建议不要封装作用域, 封装异步任务即可, 作用域灵活随用随写(Activity/Fragment都可以写作用域) - * - * 不要企图把所有逻辑代码都写在ViewModel中就觉得自己会写MVVM了, 给代码换个位置不叫架构设计, 特别是还增加一堆无效代码情况下 + * 不要将请求结果抛来抛去, 增加代码复杂度 */ class UserViewModel : ViewModel() { // 用户信息 var userInfo: MutableLiveData = MutableLiveData() - var updateTime: Long = 0 - /** - * 拉取用户信息, 只能监听返回结果, 仅当外部调用对象不在乎返回结果时使用 - * 会自动通知页面更新: 因为使用LiveData将请求结果回调出去, 建议将该liveData对象直接使用DataBinding绑定到页面上, 就会自动触发UI - * 同时页面销毁会自动取消网络请求: 因为他使用`scopeNetLife`. 生命周期跟随当前viewModel - * - * 本质上我并不推荐将Scope定义在ViewModel中(仅仅换个位置要多写很多代码), 特别是妄图在ViewModel中请求网络却要求更新UI + * 使用LiveData接受请求结果, 将该liveData直接使用DataBinding绑定到页面上, 会在请求成功自动更新视图 */ fun fetchUserInfo() = scopeNetLife { userInfo.value = Get(Api.GAME).await() - updateTime = System.currentTimeMillis() } /** - * 非阻塞异步任务 - * 返回Deferred, 调用await()才会返回结果. 调用即执行任务 + * 开始非阻塞异步任务 + * 返回Deferred, 调用await()才会返回结果 */ fun fetchList(scope: CoroutineScope) = scope.Get(Api.TEST) /** - * 阻塞异步任务 + * 开始阻塞异步任务 * 直接返回结果 */ suspend fun fetchPrecessData() = coroutineScope {