上一章介绍了本地数据的持久化,它可以让应用退出后,仍可以在启动后通过读取数据,恢复状态数据。但如果手机丢了,或者本地数据被不小心清空了,应用就又会 "失忆"
。
现在移动互联网已经极度成熟了,将数据存储在远程的服务器中,通过网络来访问、操作数据对于现在的人已经是家常便饭了。比如微信应用中的聊天记录、支付宝应用中的余额、美团应用中的店铺信息、游戏里的资源装备、抖音里的视频评论… 现在的网络数据已经无处不在了。所以对于应用开发者来说,网络请求的技能是必不可少的。
但是学习网络请求有个很大的问题,一般的网络接口都是肯定不会暴露给大众使用,而自己想要搭建一个后端提供网络接口又很麻烦。所以一般会使用开放 api ,我曾建议过掘金提供一套开放 api , 以便写网络相关的教程,但目前还没什么动静。这里就选用 wanandroid 的开发 api 接口来进行测试。
本章目的是完成一个简单的应用场景:从网络中加载文章列表数据,展示在界面中。点击条目时,可以跳转到详情页,并通过 WebView 展示网页。
文章列表 | 文章详情 |
---|---|
现在想在底部栏添加一个网络文章的按钮,点击时切换到网络请求测试的界面。只需要在 _AppNavigationState
的 menus 增加一个 MenuData :
然后在 PageView 内增加一个 NetArticlePage 组件,用于展示网络文章的测试界面:
新建一个 net_article 的文件夹用于盛放网络文章的相关代码,其中:
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 是个可以改变的参数,表示文章的页数:
通过浏览器可以直接看到接口提供的 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
}
这样,可以写出如下的 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<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 [];
}
}
这里单独创建一个 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(() {
});
}
到这里,最基础版的网络请求数据,进行界面展示的功能就完成了。当然现在的代码还存在很大的问题,下面将逐步进行优化。
———————————————————— | ———————————————————— |
文章数据中有一个链接地址,可以通过 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 状态 | 有 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<Article> newArticles = await api.loadArticles(nextPage);
_articles = _articles + newArticles;
setState(() {});
}
本章主要介绍了如何访问网络数据,实现了文章列表的展示,以及通过 WebView 在应用中展示网页内容,完成简单的文章查看功能。并且基于插件实现了下拉刷新、加载更多的功能。
到这里一个最基本的网络文章数据的展示就实现完成了, 当前代码位置 article_content。也标志着本系列教程进入了尾声,还有很多值得优化的地方,希望大家再以后的路途中可以自己思考和处理。