learn-tech/专栏/Flutter入门教程/25网络数据的访问.md
2024-10-16 00:01:16 +08:00

484 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到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。也标志着本系列教程进入了尾声还有很多值得优化的地方希望大家再以后的路途中可以自己思考和处理。