13 KiB
因收到Google相关通知,网站将会择期关闭。相关通知内容
25 网络数据的访问
一、需求介绍和准备工作
上一章介绍了本地数据的持久化,它可以让应用退出后,仍可以在启动后通过读取数据,恢复状态数据。但如果手机丢了,或者本地数据被不小心清空了,应用就又会 "失忆" 。
- 本章目的
现在移动互联网已经极度成熟了,将数据存储在远程的服务器中,通过网络来访问、操作数据对于现在的人已经是家常便饭了。比如微信应用中的聊天记录、支付宝应用中的余额、美团应用中的店铺信息、游戏里的资源装备、抖音里的视频评论… 现在的网络数据已经无处不在了。所以对于应用开发者来说,网络请求的技能是必不可少的。
但是学习网络请求有个很大的问题,一般的网络接口都是肯定不会暴露给大众使用,而自己想要搭建一个后端提供网络接口又很麻烦。所以一般会使用开放 api ,我曾建议过掘金提供一套开放 api , 以便写网络相关的教程,但目前还没什么动静。这里就选用 wanandroid 的开发 api 接口来进行测试。
本章目的是完成一个简单的应用场景:从网络中加载文章列表数据,展示在界面中。点击条目时,可以跳转到详情页,并通过 WebView 展示网页。
文章列表 文章详情
- 界面准备
现在想在底部栏添加一个网络文章的按钮,点击时切换到网络请求测试的界面。只需要在 _AppNavigationState 的 menus 增加一个 MenuData :
然后在 PageView 内增加一个 NetArticlePage 组件,用于展示网络文章的测试界面:
新建一个 net_article 的文件夹用于盛放网络文章的相关代码,其中:
views 文件夹盛放组件视图相关的文件,比如主页面、详情页等。 model 文件夹用于盛放数据模型,比如文章数据的封装类。 api 文件夹盛放网络数据请求的代码,在功能上相当于上一章的 storage , 负责读取和写入数据。只不过对于网络数据再说,是存储在服务器上的,需要提供接口来操作。
NetArticlePage 组件现在先准备一下:通过 Scaffold 构建界面结构,由于之前已经提供了 AppBar 的主题,这里直接给个 title 即可,其他配置信息会默认跟随主题。接下来最重要的任务就是对 body 主体内容的构建。
class NetArticlePage extends StatelessWidget { const NetArticlePage({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('网络请求测试'), ), body: Container(), ); } }
- 接口介绍
这里只使用一个获取文章列表的如下接口,其中 0 是个可以改变的参数,表示文章的页数:
www.wanandroid.com/article/lis…
通过浏览器可以直接看到接口提供的 json 数据:
使用 json 美化工具可以看出如下的结构,主要的文章列表数据在 data["datas"] 中 :
每条记录的数据如下,其中数据有很多,不过没有必要全都使用。这里展示文章信息,只需要标题 title 、地址 link 、 时间 niceDate 即可。
{ "adminAdd": false, "apkLink": "", "audit": 1, "author": "", "canEdit": false, "chapterId": 502, "chapterName": "自助", "collect": false, "courseId": 13, "desc": "", "descMd": "", "envelopePic": "", "fresh": true, "host": "", "id": 26411, "isAdminAdd": false, "link": "https://juejin.cn/post/7233067863500849209", "niceDate": "7小时前", "niceShareDate": "7小时前", "origin": "", "prefix": "", "projectLink": "", "publishTime": 1684220135000, "realSuperChapterId": 493, "route": false, "selfVisible": 0, "shareDate": 1684220135000, "shareUser": "张风捷特烈", "superChapterId": 494, "superChapterName": "广场Tab", "tags": [], "title": "Dart 3.0 语法新特性 | Records 记录类型 (元组)", "type": 0, "userId": 31634, "visible": 1, "zan": 0 }
4.数据模型的封装
这样,可以写出如下的 Article 类承载数据,并通过一个 formMap 构造通过 map 数据构造 Article 对象。
class Article { final String title; final String url; final String time;
const Article({ required this.title, required this.time, required this.url, });
factory Article.formMap(dynamic map) { return Article( title: map['title'] ?? '未知', url: map['link'] ?? '', time: map['niceDate'] ?? '', ); }
@override String toString() { return 'Article{title: $title, url: $url, time: $time}'; } }
二、基础功能的实现
俗话说巧妇难为无米之炊,如果说界面是一碗摆在台面上的饭,那数据就是生米,把生米煮成熟饭就是组件构建的过程。所以实现基础功能有两大步骤: 获取数据、构建界面。
- 网络数据的请求
网络请求是非常通用的能力,开发者自己来写非常复杂,所以一般使用三方的依赖库。对于 Flutter 网络请求来说,最受欢迎的是 dio , 使用前先添加依赖:
dependencies: ... dio: ^5.1.2
下面看一下最简单的使用,如下在 ArticleApi 中持有 Dio 类型的 _client 对象,构造时可以设置 baseUrl 。然后提供 loadArticles 方法,用于加载第 page 页的数据,其中的逻辑处理,就是加载网络数据的核心。
使用起来也很方便,提供 Dio#get 方法就可以异步获取数据,得到之后,从结果中拿到自己想要的数据,生成 Article 列表即可。
class ArticleApi{
static const String kBaseUrl = 'https://www.wanandroid.com';
final Dio _client = Dio(BaseOptions(baseUrl: kBaseUrl));
Future<List
- 文章内容界面展示
这里单独创建一个 ArticleContent 组件负责展示主题内容,由于需要加载网络数据,加载成功后要更新界面,使用需要使用状态类来维护数据。所以让它继承自 StatefulWidget :
class ArticleContent extends StatefulWidget { const ArticleContent({Key? key}) : super(key: key);
@override State createState() => _ArticleContentState(); }
对于状态类来说,最重要数据是 Article 列表,build 构建逻辑中通过 ListView 展示可滑动列表,其中构建条目时依赖列表中的数据:
class _ArticleContentState extends State { List
@override Widget build(BuildContext context) { return ListView.builder( itemExtent: 80, itemCount: _articles.length, itemBuilder: _buildItemByIndex, ); }
Widget _buildItemByIndex(BuildContext context, int index) { return ArticleItem( article: _articles[index], onTap: _jumpToPage, ); } }
另外这里单独封装了 ArticleItem 组件展示条目的单体,效果如下,大家可以自己处理一下,这里就不放代码了,处理不好的话可以参考源码。
最后只要在 initState 回调中通过 ArticleApi 加载网络数据即可,加载完成后通过 setState 更新界面:
ArticleApi api = ArticleApi();
@override void initState() { super.initState(); _loadData(); }
void _loadData() async{ _articles = await api.loadArticles(0); setState(() {
});
}
到这里,最基础版的网络请求数据,进行界面展示的功能就完成了。当然现在的代码还存在很大的问题,下面将逐步进行优化。
———————————————————— ————————————————————
- 在应用中展示 Web 界面
文章数据中有一个链接地址,可以通过 WebView 来展示内容。同样也是使用三方的依赖库 webview_flutter 。 使用前先添加依赖:
dependencies: ... webview_flutter: ^4.2.0
使用起来来非常简单,创建 WebViewController 请求地址,然后使用 WebViewWidget 组件展示即可:
class ArticleDetailPage extends StatefulWidget { final Article article;
const ArticleDetailPage({Key? key, required this.article}) : super(key: key);
@override State createState() => _ArticleDetailPageState(); }
class _ArticleDetailPageState extends State { late WebViewController controller; @override void initState() { super.initState(); controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)) ..loadRequest(Uri.parse(widget.article.url)); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.article.title)), body: WebViewWidget(controller: controller), ); } }
最后,在列表界面点击时挑战到 ArticleDetailPage 即可。这样就完成了 Web 界面在应用中的展示,当前代码位置 net_article:
void jumpToPage(Article article) { Navigator.of(context).push( MaterialPageRoute( builder: () => ArticleDetailPage(article: article), ), ); }
文章1 文章2
三、功能优化
现在有三个值得优化的地方:
网络加载数据的过程比较慢,加载成功之前文章列表是空的,界面展示空白页体验不好。可以在加载过程中展示 loading 界面。 当前加载数据完后,无法在重新加载,可以增加下拉刷新功能。 现在只能加载一页数据,可以在滑动到底部,加载下一页内容,也就是加载更多的功能。
- 增加 loading 状态
如下左图在网络上请求时没有任何处理,会有有一段时间的白页;如右图所示,在加载过程中给出一些界面示意,在体验上会好很多。
无 loading 状态 有 loading 状态
其实处理起来也并不复杂,由于界面需要感知加载中的状态,示意需要增加一个状态数据用于控制。比如这里在状态类中提供 _loading 的布尔值来表示,该值的维修事件也很明确:加载数据前置为 true 、加载完后置为 false 。
bool _loading = false;
void _loadData() async { _loading = true; setState(() {}); _articles = await api.loadArticles(0); _loading = false; setState(() {}); }
上面是状态数据的逻辑处理,下面来看一下界面构建逻辑。只要在 _loading 为 true 时,返回加载中对应的组件即可。如果加载中的界面比较复杂,或想要在其他地方复用,也可以单独封装成一个组件来维护。
@override Widget build(BuildContext context) { if(_loading){ return Center( child: Wrap( spacing: 10, direction: Axis.vertical, crossAxisAlignment: WrapCrossAlignment.center, children: const [ CupertinoActivityIndicator(), Text("数据加载中,请稍后...",style: TextStyle(color: Colors.grey),) ], ), ); } return ListView.builder( itemExtent: 80, itemCount: _articles.length, itemBuilder: _buildItemByIndex, ); }
这样,就完成了展示界面加载中的功能,当前代码位置 article_content.dart。
- 下拉刷新功能
如下所示,在列表下拉时,头部只可以展示加载的信息,这种效果组件手写起来非常麻烦。
这是一个通用的功能,好在我们可以依赖别人的代码,使用三方库来实现,这里用的是 easy_refresh。使用前先添加依赖:
dependencies: ... easy_refresh: ^3.3.1+2
使用方式也非常简单,将 EasyRefresh 组件套在 ListView 上即可。在 header 中可以放入头部的配置信息,通过 onRefresh 参数设置下拉刷新的回调,也就是从网络加载数据,成功后更新界面。
- 加载更多功能
上面只能展示一页的数据,如果需要展示多页怎么办? 一般来说应用在滑动到底部会加载更多,如下所示:
实现起来也非常简单 EasyRefresh 的 onLoad 参数设置下拉回调,加载下一页数据,并加入 _articles 数据中即可。这里一页数据有 20 条,下一页也就是 _articles.length ~/ 20 :
void _onLoad() async{ int nextPage = _articles.length ~/ 20; List
四、本章小结
本章主要介绍了如何访问网络数据,实现了文章列表的展示,以及通过 WebView 在应用中展示网页内容,完成简单的文章查看功能。并且基于插件实现了下拉刷新、加载更多的功能。
到这里一个最基本的网络文章数据的展示就实现完成了, 当前代码位置 article_content。也标志着本系列教程进入了尾声,还有很多值得优化的地方,希望大家再以后的路途中可以自己思考和处理。