484 lines
13 KiB
Markdown
484 lines
13 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
25 网络数据的访问
|
||
一、需求介绍和准备工作
|
||
|
||
上一章介绍了本地数据的持久化,它可以让应用退出后,仍可以在启动后通过读取数据,恢复状态数据。但如果手机丢了,或者本地数据被不小心清空了,应用就又会 "失忆" 。
|
||
|
||
1. 本章目的
|
||
|
||
现在移动互联网已经极度成熟了,将数据存储在远程的服务器中,通过网络来访问、操作数据对于现在的人已经是家常便饭了。比如微信应用中的聊天记录、支付宝应用中的余额、美团应用中的店铺信息、游戏里的资源装备、抖音里的视频评论… 现在的网络数据已经无处不在了。所以对于应用开发者来说,网络请求的技能是必不可少的。
|
||
|
||
但是学习网络请求有个很大的问题,一般的网络接口都是肯定不会暴露给大众使用,而自己想要搭建一个后端提供网络接口又很麻烦。所以一般会使用开放 api ,我曾建议过掘金提供一套开放 api , 以便写网络相关的教程,但目前还没什么动静。这里就选用 wanandroid 的开发 api 接口来进行测试。
|
||
|
||
本章目的是完成一个简单的应用场景:从网络中加载文章列表数据,展示在界面中。点击条目时,可以跳转到详情页,并通过 WebView 展示网页。
|
||
|
||
|
||
|
||
|
||
文章列表
|
||
文章详情
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
2. 界面准备
|
||
|
||
现在想在底部栏添加一个网络文章的按钮,点击时切换到网络请求测试的界面。只需要在 _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(),
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
3. 接口介绍
|
||
|
||
这里只使用一个获取文章列表的如下接口,其中 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}';
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
二、基础功能的实现
|
||
|
||
俗话说巧妇难为无米之炊,如果说界面是一碗摆在台面上的饭,那数据就是生米,把生米煮成熟饭就是组件构建的过程。所以实现基础功能有两大步骤: 获取数据、构建界面。
|
||
|
||
1. 网络数据的请求
|
||
|
||
网络请求是非常通用的能力,开发者自己来写非常复杂,所以一般使用三方的依赖库。对于 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<Article>> loadArticles(int page) async {
|
||
String path = '/article/list/$page/json';
|
||
var rep = await _client.get(path);
|
||
if (rep.statusCode == 200) {
|
||
if(rep.data!=null){
|
||
var data = rep.data['data']['datas'] as List;
|
||
return data.map(Article.formMap).toList();
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
2. 文章内容界面展示
|
||
|
||
这里单独创建一个 ArticleContent 组件负责展示主题内容,由于需要加载网络数据,加载成功后要更新界面,使用需要使用状态类来维护数据。所以让它继承自 StatefulWidget :
|
||
|
||
class ArticleContent extends StatefulWidget {
|
||
const ArticleContent({Key? key}) : super(key: key);
|
||
|
||
@override
|
||
State<ArticleContent> createState() => _ArticleContentState();
|
||
}
|
||
|
||
|
||
对于状态类来说,最重要数据是 Article 列表,build 构建逻辑中通过 ListView 展示可滑动列表,其中构建条目时依赖列表中的数据:
|
||
|
||
class _ArticleContentState extends State<ArticleContent> {
|
||
List<Article> _articles = [];
|
||
|
||
@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(() {
|
||
|
||
});
|
||
}
|
||
|
||
|
||
到这里,最基础版的网络请求数据,进行界面展示的功能就完成了。当然现在的代码还存在很大的问题,下面将逐步进行优化。
|
||
|
||
|
||
|
||
|
||
————————————————————
|
||
————————————————————
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
3. 在应用中展示 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<ArticleDetailPage> createState() => _ArticleDetailPageState();
|
||
}
|
||
|
||
class _ArticleDetailPageState extends State<ArticleDetailPage> {
|
||
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 界面。
|
||
当前加载数据完后,无法在重新加载,可以增加下拉刷新功能。
|
||
现在只能加载一页数据,可以在滑动到底部,加载下一页内容,也就是加载更多的功能。
|
||
|
||
|
||
|
||
|
||
1. 增加 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。
|
||
|
||
|
||
|
||
2. 下拉刷新功能
|
||
|
||
如下所示,在列表下拉时,头部只可以展示加载的信息,这种效果组件手写起来非常麻烦。
|
||
|
||
|
||
|
||
这是一个通用的功能,好在我们可以依赖别人的代码,使用三方库来实现,这里用的是 easy_refresh。使用前先添加依赖:
|
||
|
||
dependencies:
|
||
...
|
||
easy_refresh: ^3.3.1+2
|
||
|
||
|
||
|
||
|
||
|
||
|
||
使用方式也非常简单,将 EasyRefresh 组件套在 ListView 上即可。在 header 中可以放入头部的配置信息,通过 onRefresh 参数设置下拉刷新的回调,也就是从网络加载数据,成功后更新界面。
|
||
|
||
|
||
|
||
|
||
|
||
3. 加载更多功能
|
||
|
||
上面只能展示一页的数据,如果需要展示多页怎么办? 一般来说应用在滑动到底部会加载更多,如下所示:
|
||
|
||
|
||
|
||
实现起来也非常简单 EasyRefresh 的 onLoad 参数设置下拉回调,加载下一页数据,并加入 _articles 数据中即可。这里一页数据有 20 条,下一页也就是 _articles.length ~/ 20 :
|
||
|
||
void _onLoad() async{
|
||
int nextPage = _articles.length ~/ 20;
|
||
List<Article> newArticles = await api.loadArticles(nextPage);
|
||
_articles = _articles + newArticles;
|
||
setState(() {});
|
||
}
|
||
|
||
|
||
|
||
|
||
四、本章小结
|
||
|
||
本章主要介绍了如何访问网络数据,实现了文章列表的展示,以及通过 WebView 在应用中展示网页内容,完成简单的文章查看功能。并且基于插件实现了下拉刷新、加载更多的功能。
|
||
|
||
到这里一个最基本的网络文章数据的展示就实现完成了, 当前代码位置 article_content。也标志着本系列教程进入了尾声,还有很多值得优化的地方,希望大家再以后的路途中可以自己思考和处理。
|
||
|
||
|
||
|
||
|