[Android+JetPack] (Java实现) Retrofit2+RxJava3+Paging3+RecyclerView 实现加载网络数据例子 记录
文章目录
- 前言
- 参考链接
- 依赖库及版本
- Demo效果
- 接口及数据展示
- 各项模块
- `Retrofit2`
- Bean,对应上面的接口返回.
- Service API部分
- `Paging3`
- `PagingSource`以及 `RxPagingSource`
- `PagingDataAdapter` 适配器
- `ViewModel`
- `PublicInfoPage /Activity`
- 最后
前言
继续安卓学习之旅,本章的主要目标是:
1.完成一个无限上拉加载的列表(Paging3 + RecyclerView)
2.加载的是网络数据, 要采用主流的 Retrofit+okhttp方式
3.在了解了RxJava之后,也希望用上Rxjava
4.用到ViewModel来配合,以及一些jetpack的东西都用上
(为什么不用Paging2? 这里主要是看说3比2还要方便些,所以就偷懒没去用Paging2)
参考链接
这些是在学习和搜索中看到的比较好的文章,不过他们要么是kotlin 要么是RxJava2,都不是能直接套上去就用的,但是从文章里面总结归纳,也是有借鉴效果的.
- SmartRefreshLayout-github 这个后期再结合Paging3,完成一个有酷炫下拉及淘宝二楼效果的的demo
- Java实现)使用官方Paging3分页库实现RecyclerView加载更多(loadmore)的功能 这个较为简洁,没那么多原理的描述,方便更实战的理解借鉴
- Android paging3 使用和踩坑经验分享 这个虽然是kotlin,不过里面一些名词的解释不错, 适合快速扫盲
- Jetpack新成员,Paging3从吐槽到真香
依赖库及版本
为什么要说这个, 因为在实际百度各方面资料的时候,没仔细区分好版本,导致在练习过程中走了不少弯路,踩了坑.为避免这个情况,这里列出本Demo中的各个依赖库及版本
Retrofix2
// 引入 retrofix 网络框架(自带okhttp)// github :https://github.com/square/retrofit// 视频教学// https://www.bilibili.com/video/BV1vV411W75V?p=4&vd_source=3dc64571e08f84008d5c43796c009480implementation "com.squareup.retrofit2:retrofit:2.9.0"implementation 'com.squareup.retrofit2:converter-gson:2.9.0'implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
Rxjava3
// 支持RxJava/RxAndroidimplementation 'io.reactivex.rxjava3:rxandroid:3.0.2'implementation 'io.reactivex.rxjava3:rxjava:3.1.5'
Paging3
// 引入 paging 3// 注意, 由于上面我们用的是 retrofit2 + rxjava3// 所以,在使用 paging3的时候, 要选 支持rxjava3的 paging-rxjava3// 切记版本对应好def paging_version = "3.1.1"implementation "androidx.paging:paging-runtime:$paging_version"
// // optional - RxJava2 support
// implementation "androidx.paging:paging-rxjava2:$paging_version"// optional - RxJava3 supportimplementation "androidx.paging:paging-rxjava3:$paging_version"
这里稍微提一下, 如果用的是
RxJava3, 就使用RxJava3 support的可选项, 不然不匹配,但同时也造成另一个问题, 这里插入说一下哈
就是
包括目前官网(点击进入)那边的, 关于对RxPagingSource的示例里面, 也应该用的还是RxJava2,如果你和我一样用RxJava3,那大概率在做map的时候,会报错说, 类型转换失败, 不能用 this::toLoadResult
这个稍后再说…

Demo效果

一个简单的demo
接口及数据展示

各项模块
Retrofit2
Bean,对应上面的接口返回.
Response_public_info_bean
package retrofit.bean;import java.util.List;/*** @author: tiannan* @time: 2023/4/12.* @email: tianNanYiHao@163.com* @descripetion: 此处添加描述*/
public class Response_public_info_bean {private String msg;private String code;private Datas data;public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public Datas getData() {return data;}public void setData(Datas data) {this.data = data;}public class Datas {private int pageSize;private List<Cell> list;public int getPageSize() {return pageSize;}public void setPageSize(int pageSize) {this.pageSize = pageSize;}public List<Cell> getList() {return list;}public void setList(List<Cell> list) {this.list = list;}@Overridepublic String toString() {return "Datas{" +"list=" + list +'}';}public class Cell {private String productName;private String productTypeName;private String riskRateName;private int id;private int pageNum; // 增加两个下标 page页下标private int indexNum;// 增加两个下标 newsInfo(cell)页下标@Overridepublic String toString() {return "News{" +"productName='" + productName + '\'' +", productTypeName='" + productTypeName + '\'' +", riskRateName='" + riskRateName + '\'' +", id=" + id +", pageNum=" + pageNum +", indexNum=" + indexNum +'}';}public String getProductName() {return productName;}public void setProductName(String productName) {this.productName = productName;}public String getProductTypeName() {return productTypeName;}public void setProductTypeName(String productTypeName) {this.productTypeName = productTypeName;}public String getRiskRateName() {return riskRateName;}public void setRiskRateName(String riskRateName) {this.riskRateName = riskRateName;}public int getId() {return id;}public void setId(int id) {this.id = id;}public int getPageNum() {return pageNum;}public void setPageNum(int pageNum) {this.pageNum = pageNum;}public int getIndexNum() {return indexNum;}public void setIndexNum(int indexNum) {this.indexNum = indexNum;}}}
}
Service API部分
这里不展开说太多
Retrofit2部分的东西,这里只贴一下和本章有关的部分代码
/*** 请求公募数据列表* @param map* @return*/@GET(App_Url.admin_getPublicProductInfoPageList)Single<Response_public_info_bean> admin_getPublicProductInfoPageList(@QueryMap HashMap<String,String> map);
以上部分完成后 ,能够通过RxJava3 + Retrofit2 配合完成一次网络请求, 基本就算完成Demo一半功能了
Paging3
PagingSource以及 RxPagingSource
这里一开始我用错了
RxPagingSource的导入版本, 用成了rxjava2的,踩了些坑
当时导入了paging.rxjava2这个版本,
主要原因还是在配置依赖的时候, 把paging-rxjava2:3.1.1版本也同步了
版本问题注意好
回到 RxPagingSource
按照网上的文章的示例, 先处理 loadSingle()函数的实现,这里有坑就是上面说的, RxJava3 + Paging3的情况下,

在 return网络请求的时候,会报错

我这里没有在详细探究是否由于RxJava2的原因导致不能用 ::这种双冒号的写法
这里仅仅贴一下RxJava2下的map 和 RxJava3下的map的源码区别
RxJava2版本:

RxJava3版本:

确实有一点区别, 这个先放一放, 等后期有空再看怎么处理…
先直接看怎么去写这个 this::toLoadResult
首先,既然通过Retrofit2,我们已经定义了网络请求的返回值

那么我们在RxPagingSource的 loadSingle()中, 会去调用网络请求,得到一个 Single<Response_public_info_bean>

我们再看官网例子的这部分代码

注意看返回值其实是LoadResult<Integer, User> ,或者说,在本文章 我们要的返回值其实是 LoadResult<Integer, Response_public_info_bean.Datas.Cell>
所以.对于map操作符,在Rxjava3的下, 我们是可以自己去提供一个Function<T,R>,这里面T就是我们上面的Response_public_info_bean
R就是Response_public_info_bean.Datas.Cell>
所以代码就是

(这里要注意下prevkey, 和 nextKey的入参 , 要做好逻辑判断, 一开始我参考别人的代码, 在prevKey填的是null, 在nextKey填入的是nextPageKey+1,结果导致加载页码瞬间冲到了几百页, 其实总page数量才不过十几页)
为了更加清晰明了的展示页码和条数下标, 我又添加了一个map操作符, 是给cell这个Bean数据再添加一下当前所属的页码 和 当前的下标
Function的入参依然是 Response_public_info_bean 返回也还是 Response_public_info_bean, 相当于我们就对Response_public_info_bean数据做了个数据加工

所以,基于RxJava3的map操作符这边就可以这样返回

PagingDataAdapter 适配器
public class PublicInfoAdapter extends PagingDataAdapter<Response_public_info_bean.Datas.Cell, PublicInfoAdapter.Holder> {public PublicInfoAdapter(@NotNull DiffUtil.ItemCallback<Response_public_info_bean.Datas.Cell> diffCallback) {super(diffCallback);}@NonNull@NotNull@Overridepublic Holder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {PublicInfoCellBinding publicInfoCellBinding = PublicInfoCellBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);return new Holder(publicInfoCellBinding);}@Overridepublic void onBindViewHolder(@NonNull @NotNull Holder holder, int position) {Response_public_info_bean.Datas.Cell cell = getItem(position);Log.d("dfaddfsa", "onBindViewHolder: " + cell.getPageNum() +"."+ cell.getIndexNum() + "---" + cell.getProductName());holder.publicInfoCellBinding.textTitle.setText(cell.getProductName());holder.publicInfoCellBinding.underflag.setText("第"+cell.getPageNum() + "页.第" + cell.getIndexNum()+"条");}public class Holder extends RecyclerView.ViewHolder {/*** 给每个 Holder 实例 一个 viewBinding*/private PublicInfoCellBinding publicInfoCellBinding;public Holder(@NonNull @NotNull View itemView) {super(itemView);}public Holder(@NonNull PublicInfoCellBinding publicInfoCellBinding){super(publicInfoCellBinding.getRoot());this.publicInfoCellBinding = publicInfoCellBinding;}}
}
这里没太多可以说的,网上基本讲明白了, 我仅仅分享我遇到的一个问题
页面列表在加载完成后, 出现了一屏数据的重复渲染, 而且随着页面的滚动,该组数据的最后一条一直在渲染不同内容(但随着日志打印,数据都是正常输出的)
最后通过UI观察, 感觉最后一条数据随滚动而渲染,有点想是for循环没拦住的那种意思
就猜想,是不是 PagingDataAdapter 里面没处理好,
后来果然发现, PublicInfoCellBinding publicInfoCellBinding;一不小心写成了全局的,而不是给每个Holder一个PublicInfoCellBinding publicInfoCellBinding;, 最后修复下即可
这里还是由于对PagingDataAdapter的不够熟悉, 刚写着玩意儿,才出现的低级错误
ViewModel
vm部分,网上也大同小异 ,写demo过程中未出现过多的坎儿
public class PublicInfoViewModel extends ViewModel {// paging3 page对象Pager<Integer, Response_public_info_bean.Datas.Cell> pager;// paging3 数据源对象PublicInfoSource publicInfoSource;// rxjava3 的 obserable 可观察对象Flowable<PagingData<Response_public_info_bean.Datas.Cell>> pagingDataFlowable;public PublicInfoViewModel(Context context) {CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(this);publicInfoSource = new PublicInfoSource();// Maximum size must be at least pageSize + 2*prefetchDist, pageSize=20, prefetchDist=20, maxSize=20/*** pageSize 每页多少个条目* prefetchDistance 预加载下一页的距离,滑动到倒数第几个条目就加载下一页,无缝加载(可选)默认值是pageSize* enablePlaceholders 是否启用条目占位,当条目总数量确定的时候;列表一次性展示所有条目,* 但是没有数据;在adapter的onBindViewHolder里面绑定数据时候,是空数据,判断是空数据展示对应的占位item;可选,默认开启* initialLoadSize 第一页加载条目数量 ,可选,默认值是 3*pageSize (有时候需要第一页多点数据可用)* maxSize : 定义列表最大数量;可选,默认值是:Int.MAX_VALUE* jumpThreshold : 暂时还不知道用法,从文档注释上看,是滚动大距离导致加载失效的阈值;可选,默认值是:Int.MIN_VALUE (表示禁用此功能)**/PagingConfig pagingConfig = new PagingConfig(20,1,false,20*3);pager = new Pager<Integer, Response_public_info_bean.Datas.Cell>(pagingConfig, () -> publicInfoSource);pagingDataFlowable = PagingRx.getFlowable(pager);PagingRx.cachedIn(pagingDataFlowable, viewModelScope);}public Flowable<PagingData<Response_public_info_bean.Datas.Cell>> getPagingDataFlowable() {return pagingDataFlowable;}
}
PublicInfoPage /Activity
这里要注意的是, setLayoutManager要设置 否则啥也不展示
public class PublicInfoPage extends AppCompatActivity {ActivityNewsPageBinding newsPageBinding;PublicInfoViewModel newsViewModel;PublicInfoAdapter newsAdapter;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);newsPageBinding = ActivityNewsPageBinding.inflate(getLayoutInflater());newsAdapter = new PublicInfoAdapter(new DiffUtil.ItemCallback<Response_public_info_bean.Datas.Cell>() {@Overridepublic boolean areItemsTheSame(@NonNull @NotNull Response_public_info_bean.Datas.Cell oldItem, @NonNull @NotNull Response_public_info_bean.Datas.Cell newItem) {return oldItem.getId() == newItem.getId();}@Overridepublic boolean areContentsTheSame(@NonNull @NotNull Response_public_info_bean.Datas.Cell oldItem, @NonNull @NotNull Response_public_info_bean.Datas.Cell newItem) {return oldItem.getProductName().equals(newItem.getProductName()) && oldItem.getProductTypeName().equals(newItem.getProductTypeName());}});newsPageBinding.recicleView.setAdapter(newsAdapter);newsPageBinding.recicleView.setLayoutManager(new LinearLayoutManager(this));setContentView(newsPageBinding.getRoot());}@Overrideprotected void onResume() {super.onResume();newsViewModel = new PublicInfoViewModel(this);newsViewModel.getPagingDataFlowable().subscribe(new Consumer<PagingData<Response_public_info_bean.Datas.Cell>>() {@Overridepublic void accept(PagingData<Response_public_info_bean.Datas.Cell> newsPagingData) throws Throwable {newsAdapter.submitData(getLifecycle(), newsPagingData);}});}
}
最后
以上就是这样了.
SmartRefreshLayout 也可以结合Paging3
这个有空也看一下,
安卓的玩法确实和iOS不一样, 也和RN不一样, 但互相又看得到对方的影子
相关文章:
[Android+JetPack] (Java实现) Retrofit2+RxJava3+Paging3+RecyclerView 实现加载网络数据例子 记录
文章目录 前言参考链接依赖库及版本Demo效果接口及数据展示各项模块Retrofit2Bean,对应上面的接口返回.Service API部分 Paging3PagingSource以及 RxPagingSourcePagingDataAdapter 适配器ViewModelPublicInfoPage /Activity 最后 前言 继续安卓学习之旅,本章的主要目标是: 1.完…...
Java 解析配置文件注入到配置类属性中供全局使用【开发记录】
1、背景:假设目前有两个接口,一个是查询快递订单状态的JSF接口,一个是查询快运订单状态的JSF接口,现有一个需求,要将这两个接口统一为一个入口,发布到物流开放平台供外界调用。 注意:以下代码均…...
【Python开发手册】深入剖析Google Python开发规范:规范Python注释写作
💖 作者简介:大家好,我是Zeeland,全栈领域优质创作者。📝 CSDN主页:Zeeland🔥📣 我的博客:Zeeland📚 Github主页: Undertone0809 (Zeeland) (github.com)&…...
Python入门教程+项目实战-9.3节: 字符串的操作方法
目录 9.3.1 字符串常用操作方法 9.3.2 获取字符串长度 9.3.3 字符串的大小写操作 9.3.4 删除字符串中的空白字符 9.3.5 字符串的子串查找 9.3.6 字符串的子串统计 9.3.7 字符串的子串替换 9.3.8 字符串的拆分函数 9.3.9 字符串的前缀与后缀9.3.10 知识要点 9.3.11 系…...
ENVI 5.6软件安装教程
软件下载 [软件名称]:ENVI 5.6 [软件大小]:3.25G [安装环境]:Win7~Win11或更高 软件介绍 ENVI 5.6是一款实现遥感图像处理的工具,已经广泛应用于科研、环境保护、气象、石油矿产勘探、农业、林业、医学、地球科学、公用设施管…...
在Windbg中设置断点追踪打开C++程序远程调试开关的模块
目录 1、Windbg动态调试 2、在Windbg中设置断点 2.1、在函数入口处设置断点 2.2、在函数内部某一行上设置断点 3、设置断点跟踪对打开远程调试开关接口的调用 3.1、编写演示代码 3.2、在Windbg中设置调用SetRemoteDebugOn接口的断点进行跟踪 4、最后 VC常用功能开发汇总…...
CRM客户管理软件开发功能有哪些?
互联网技术的不断提高使得企业管理方式也发生了变化,企业CRM系统应用市场逐渐扩大,相关软件开发也引起越来越多商家企业的关注。因为企业CRM系统软件开发能够根据企业需求制作,帮助企业更好的追踪管理客户信息,实时更新并进行相关…...
C++函数式魔法之旅(Journey of Functional Magic)
C函数式魔法之旅(Journey of Functional Magic) 一、引言(Introduction)C Functional模板库简介(Overview of C Functional Template Library)Functional模板库的重要性和作用(The Importance a…...
Vue基础入门(上)
<script src"https://unpkg.com/vuenext"></script> 从面向dom编程到面向数据编程 输入显示列表 const appVue.createApp({data(){return{inputValue:,list:[]}},methods:{handleAddItem(){this.list.push(this.inputValue);this.inputValue;}},templ…...
字符串匹配—KMP算法
字符串匹配的应用非常广泛,例如在搜索引擎中,我们通过键入一些关键字就可以得到相关的搜索结果,搜索引擎在这个过程中就使用字符串匹配算法,它通过在资源中匹配关键字,最后给出符合条件的搜索结果。并且我们在使用计算…...
【微信小程序】 权限接口梳理以及代码实现
1、权限接口说明 官方权限说明 部分接口需要经过用户授权统一才能调用。我们把这些接口按使用范围分成多个scope,用户选择对scope进行授权,当授权给一个scope之后,其对应的所有接口都可以直接使用。 此类接口调用时: 如…...
【每日一词】leit-motif
1、释义 leit-motif: n. 主乐调;主题;主旨。 复数:leit-motifs 2、例句 Hence the ‘ancient’ rhyme that appears as the leit-motif of The Lord of the Rings, Three Rings for the Elven-Kings under the sky, Seven for the Dwarf-lor…...
windows 环境修改 Docker 存储目录
windows 环境修改存储目录 docker 安装时不提供指定安装路径和数据存储路径的选项,且默认是安装在C盘的。C盘比较小的,等docker运行久了,一大堆的东西放在上面容易导致磁盘爆掉。所以安装前可以做些准备,让安装的实际路径不在C盘&…...
上海市青少年算法月赛丙组—目录汇总
上海市青少年算法2023年3月月赛(丙组) T1 神奇的字母序列 T2 约数的分类 T3 循环播放 T4 数对的个数 T5 选取子段 上海市青少年算法2023年2月月赛(丙组) T1 格式改写 T2 倍数统计 T3 区间的并 T4 平分数字(一…...
手动实现promise.all
手动实现promise.all function promiseAll(promises) {return new Promise((resolve, reject) > {const results [];let count 0;promises.forEach((promise, index) > {Promise.resolve(promise).then(result > {results[index] result;count;if (count promise…...
如何搭建关键字驱动自动化测试框架?这绝对是全网天花板的教程
目录 1. 关键字驱动自动化测试介绍 2. 搭建关键字驱动自动化测试框架 步骤1:选择测试工具 步骤2:定义测试用例 步骤3:编写测试驱动引擎 步骤4:实现测试关键字库 步骤5:执行测试 3. 实现关键字驱动自动化测试的关…...
字符串反转操作
1:将字符串反转 给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。 输入格式: 测试输入包含一个测试用例,在一行内给出总长度不超过 80 的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母…...
TensorFlow 智能移动项目:1~5
原文:Intelligent mobile projects with TensorFlow 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 不要担心自己的形象,只…...
[MAUI 项目实战] 手势控制音乐播放器(四):圆形进度条
文章目录 关于图形绘制创建自定义控件使用控件创建专辑封面项目地址 我们将绘制一个圆形的音乐播放控件,它包含一个圆形的进度条、专辑页面和播放按钮。 关于图形绘制 使用MAUI的绘制功能,需要Microsoft.Maui.Graphics库。 Microsoft.Maui.Graphics 是…...
web路径专题+会话技术
目录 自定义快捷键 1. 工程路径问题及解决方案1.1 相对路径1.2 相对路径缺点1.3 base标签1.4 作业11.5 作业21.6注意细节1.7 重定向作业1.8 web工程路径优化 2. Cookie技术2.1 Cookie简单示意图2.2 Cookie常用方法2.2 Cookie创建2.3 Cookie读取2.3.1 JSESSIONID2.3.2 读取指定C…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...


