从0开始写一个基于Flutter的开源中国客户端(7)——App网络请求和数据存储

上一篇中我记录了基于Flutter的开源中国客户端各个静态页面的实现,主要是UI的实现,没有涉及到任何网络请求,数据加载、存储等方面。本篇记录的是该项目中的网络请求和数据存储、加载的方式,但愿本身在温故知新的同时能给Flutter初学者带来帮助。python

索引 文章
1 从0开始写一个基于Flutter的开源中国客户端(1)
Flutter简介及开发环境搭建 | 掘金技术征文
2 从0开始写一个基于Flutter的开源中国客户端(2)
Dart语法基础
3 从0开始写一个基于Flutter的开源中国客户端(3)
初识Flutter & 经常使用的Widgets
4 从0开始写一个基于Flutter的开源中国客户端(4)
Flutter布局基础
5 从0开始写一个基于Flutter的开源中国客户端(5)
App总体布局框架搭建
6 从0开始写一个基于Flutter的开源中国客户端(6)
各个静态页面的实现
👉7 从0开始写一个基于Flutter的开源中国客户端(7)
App网络请求和数据存储
8 从0开始写一个基于Flutter的开源中国客户端(8)
插件的使用

Flutter中的网络请求

Flutter中已内置了网络请求库,可直接导入使用:git

import 'package:http/http.dart' as http;
复制代码

一个最简单的get请求代码以下:github

import 'package:http/http.dart' as http;

main() async {
  http.Response res = await http.get("https://cn.bing.com");
  print(res.body); // 打印出get请求返回的字符串数据
}
复制代码

控制台中会打印出请求返回的字符串数据。数据库

另外也有一些开源的网络请求库,因为笔者暂时没有用过,因此在本篇中不详细说了。json

在基于Flutter的开源中国客户端中,使用的也是Flutter内置的网络请求库,可是作了一些简单的封装,主要代码在lib/util/NetUtils.dart文件中,代码以下:api

import 'dart:async';
import 'package:http/http.dart' as http;

class NetUtils {
  // get请求的封装,传入的两个参数分别是请求URL和请求参数,请求参数以map的形式传入,会在方法体中自动拼接到URL后面
  static Future<String> get(String url, {Map<String, String> params}) async {
    if (params != null && params.isNotEmpty) {
      // 若是参数不为空,则将参数拼接到URL后面
      StringBuffer sb = new StringBuffer("?");
      params.forEach((key, value) {
        sb.write("$key" + "=" + "$value" + "&");
      });
      String paramStr = sb.toString();
      paramStr = paramStr.substring(0, paramStr.length - 1);
      url += paramStr;
    }
    http.Response res = await http.get(url);
    return res.body;
  }
  
  // post请求
  static Future<String> post(String url, {Map<String, String> params}) async {
    http.Response res = await http.post(url, body: params);
    return res.body;
  }
}
复制代码

使用该工具类的方法也很简单,以下代码所示:bash

import 'util/NetUtils.dart';

main() {
  Map<String, String> map = new Map();
  map['name'] = 'zhangsan';
  map['age'] = '20';
  NetUtils.get("http://www.baidu.com", params: map).then((res) {
    print(res);
  });
}
复制代码

Flutter中的数据存储

通常移动应用开发中的数据存储基本上都是文件、数据库等方式。Flutter没有提供直接操做数据库的API,可是有第三方的插件能够用,好比sqflite,关于这个插件的使用方法,能够查看这里,因为在基于Flutter的开源中国客户端项目中没有用到数据库,因此这几也不作详细说明了。服务器

本项目中针对token,用户信息的存储,使用的是Flutter提供的相似于Android的SharedPreferences,这个库是以插件的形式提供的,并无内置到Flutter中,因此咱们须要为项目配置插件,在pubspec.yaml文件中,加入以下配置:网络

dependencies:
  flutter:
    sdk: flutter
    
  shared_preferences: "^0.4.1"
复制代码

而后执行flutter packages get命令便可自动安装插件,若是你使用AndroidStudio做为开发工具,当pubspec.yaml文件作了修改后,页面上方会自动出现提示,点击Packages get便可。app

插件安装成功后,使用起来很容易,以下代码所示:

import 'package:shared_preferences/shared_preferences.dart';

main() async {
  SharedPreferences sp = await SharedPreferences.getInstance();
  sp.setString("name", "zhangsan");
  sp.setInt("age", 20);
  sp.setBool("isLogin", false);
  sp.setDouble("price", 100.5);
}
复制代码

要获取存储的某个数据,只须要使用sp.get(key)便可。shared_preferences插件的主页在这里

关于插件的使用方法,这里说明一下:pub.flutter-io.cn/是Flutter提供的一个插件仓库,能够发布有关dart或flutter的插件。若是咱们须要实现某个功能,而flutter又没有提供相似的功能时,能够上这个网站上搜索相关关键字,也许就有人已经发布了他写的库,正好能够实现咱们须要的功能。

上面简要说明了Flutter中的网络请求和数据存储,下面结合项目来讲明如何加载网络数据,如何保存用户信息等数据。

从网络加载资讯列表并显示

上一篇中我记录了如何显示资讯列表,可是彻底是一个静态的资讯列表,里面的数据都是测试的假数据,这一篇就记录下如何从接口获取真实的资讯数据并显示出来。

在基于Flutter的开源中国客户端项目中,因为开源中国官方的openapi提供的数据比较少,故资讯列表没有使用开源中国官方提供的接口,是笔者用python抓的网站数据,接口部署在香港的云服务器上,如有访问较慢的状况,请谅解。另外,接口没有作任何认证,请不要频繁请求接口。

显示加载中的Loading

既然是从网络上加载数据,那必然会有一个耗时的等待期,须要给加载过程展现一个Loading,这里咱们为NewsListPage添加一个listData变量,若是该变量为null,则显示Loading,不然就显示列表数据,显示Loading的同时从网络上请求数据,一旦有数据后,就经过setState更新listData,主要代码以下(NewsListPage.dart文件):

@override
  Widget build(BuildContext context) {
    // 无数据时,显示Loading
    if (listData == null) {
      return new Center(
        // CircularProgressIndicator是一个圆形的Loading进度条
        child: new CircularProgressIndicator(),
      );
    } else {
      // 有数据,显示ListView
      Widget listView = new ListView.builder(
        itemCount: listData.length * 2,
        itemBuilder: (context, i) => renderRow(i),
        controller: _controller,
      );
      // RefreshIndicator为ListView增长了下拉刷新能力,onRefresh参数传入一个方法,在下拉刷新时调用
      return new RefreshIndicator(child: listView, onRefresh: _pullToRefresh);
    }
  }
  
  @override
  void initState() {
    super.initState();
    getNewsList(false);
  }
  
  // 从网络获取数据,isLoadMore表示是不是加载更多数据
  getNewsList(bool isLoadMore) {
    String url = Api.NEWS_LIST;
    // curPage是定义在NewsListPageState中的成员变量,表示当前加载的页面索引
    url += "?pageIndex=$curPage&pageSize=10";
    NetUtils.get(url).then((data) {
      if (data != null) {
        // 将接口返回的json字符串解析为map类型,须要导入包:import 'dart:convert';
        Map<String, dynamic> map = json.decode(data);
        if (map['code'] == 0) {
          // code=0表示请求成功
          var msg = map['msg'];
          // total表示资讯总条数
          listTotalSize = msg['news']['total'];
          // data为数据内容,其中包含slide和news两部分,分别表示头部轮播图数据,和下面的列表数据
          var _listData = msg['news']['data'];
          var _slideData = msg['slide'];
          setState(() {
            if (!isLoadMore) {
              // 不是加载更多,则直接为变量赋值
              listData = _listData;
              slideData = _slideData;
            } else {
              // 是加载更多,则须要将取到的news数据追加到原来的数据后面
              List list1 = new List();
              // 添加原来的数据
              list1.addAll(listData);
              // 添加新取到的数据
              list1.addAll(_listData);
              // 判断是否获取了全部的数据,若是是,则须要显示底部的"我也是有底线的"布局
              if (list1.length >= listTotalSize) {
                list1.add(Constants.END_LINE_TAG);
              }
              // 给列表数据赋值
              listData = list1;
              // 轮播图数据
              slideData = _slideData;
            }
          });
        }
      }
    });
  }
复制代码

上面的代码中是处理显示Loading和显示数据列表的不一样逻辑,而后还有加载更多的逻辑处理,可是何时去加载更多数据呢?很显然,应该监听列表的滚动,当列表滚动到底时,主动去加载下一页数据。

加载下一页数据

在上面的代码中,咱们在建立ListView时,传入了一个controller参数,这个controller就是为了监听列表滚动事件而传入的,它是一个ScrollController对象,咱们在NewsListPageState类中定义这个变量并初始化:

ScrollController _controller = new ScrollController();
复制代码

要监听列表是否滚动到底的事件,还须要给这个controller添加Listener,在NewsListPageState类的构造方法中添加以下代码:

NewsListPageState() {
    _controller.addListener(() {
      // 表示列表的最大滚动距离 
      var maxScroll = _controller.position.maxScrollExtent;
      // 表示当前列表已向下滚动的距离
      var pixels = _controller.position.pixels;
      // 若是两个值相等,表示滚动到底,而且若是列表没有加载完全部数据
      if (maxScroll == pixels && listData.length < listTotalSize) {
        // scroll to bottom, get next page data
        curPage++; // 当前页索引加1
        getNewsList(true); // 获取下一页数据
      }
    });
  }
复制代码

给ListView加入下拉刷新能力

其实在上面的代码中已经为ListView添加了下拉刷新的能力,就是build方法返回时,为ListView包裹了一层RefreshIndicator:

return new RefreshIndicator(child: listView, onRefresh: _pullToRefresh);
复制代码

_pullToRefresh方法会在下拉刷新的时候调用,由于是下拉刷新,因此取的是第一页数据,而且不是加载更多,因此方法体以下:

Future<Null> _pullToRefresh() async {
    curPage = 1;
    getNewsList(false);
    return null;
  }
复制代码

须要注意的是,onRefresh参数须要一个Future<Null>类型的数据,因此上面的_pullToRefresh才会返回Future<Null>

改造事后的资讯列表以下gif图所示(图比较大,加载会有点慢):

保存登陆后的用户数据

因为获取动弹信息,评论动弹等,都须要调用开源中国的openapi,而这些接口都是须要AccessToken和用户id的,因此咱们必须把用户登陆后的数据保存下来,以便在须要用到这些数据时能获取到。具体的如何实现登陆将会放在下一篇——Flutter插件的使用中说明。本篇暂时忽略登陆的过程,只说明登陆后如何保存用户信息。

为了统一管理SharedPreferences,这里咱们新建一个工具类DataUtils,文件目录在lib/util/DataUtils.dart。开源中国openapi调用接口成功登陆后,会返回如下信息:

字段名 字段类型 说明
access_token String access_token值
refresh_token String refresh_token值
uid int 受权用户的uid
tokenType String access_token类型
expires_in int 超时时间(单位秒)

为了在SharedPreferences中保存以上信息,先在DataUtils中声明每一个字段对应的key,代码以下:

static final String SP_AC_TOKEN = "accessToken";
  static final String SP_RE_TOKEN = "refreshToken";
  static final String SP_UID = "uid";
  static final String SP_IS_LOGIN = "isLogin"; // SP_IS_LOGIN标记是否登陆
  static final String SP_EXPIRES_IN = "expiresIn";
  static final String SP_TOKEN_TYPE = "tokenType";
复制代码

而后提供一个静态方法用于一次性保存这些信息:

// 保存用户登陆信息,data中包含了token等信息
  static saveLoginInfo(Map data) async {
    if (data != null) {
      SharedPreferences sp = await SharedPreferences.getInstance();
      String accessToken = data['access_token'];
      await sp.setString(SP_AC_TOKEN, accessToken);
      String refreshToken = data['refresh_token'];
      await sp.setString(SP_RE_TOKEN, refreshToken);
      num uid = data['uid'];
      await sp.setInt(SP_UID, uid);
      String tokenType = data['tokenType'];
      await sp.setString(SP_TOKEN_TYPE, tokenType);
      num expiresIn = data['expires_in'];
      await sp.setInt(SP_EXPIRES_IN, expiresIn);

      await sp.setBool(SP_IS_LOGIN, true); // SP_IS_LOGIN标记是否登陆
    }
  }
复制代码

登陆成功后就能够调用开源中国的openapi获取用户信息了,跟上面相似,先定义用户信息每一个字段对应的key:

static final String SP_USER_NAME = "name";
  static final String SP_USER_ID = "id";
  static final String SP_USER_LOC = "location";
  static final String SP_USER_GENDER = "gender";
  static final String SP_USER_AVATAR = "avatar";
  static final String SP_USER_EMAIL = "email";
  static final String SP_USER_URL = "url";
复制代码

根据命名就知道每一个字段表明的什么含义,这里就不细说了,而后仍是提供一个静态方法,用于一次性保存用户信息:

// 保存用户我的信息
  static Future<UserInfo> saveUserInfo(Map data) async {
    if (data != null) {
      SharedPreferences sp = await SharedPreferences.getInstance();
      String name = data['name'];
      num id = data['id'];
      String gender = data['gender'];
      String location = data['location'];
      String avatar = data['avatar'];
      String email = data['email'];
      String url = data['url'];
      await sp.setString(SP_USER_NAME, name);
      await sp.setInt(SP_USER_ID, id);
      await sp.setString(SP_USER_GENDER, gender);
      await sp.setString(SP_USER_AVATAR, avatar);
      await sp.setString(SP_USER_LOC, location);
      await sp.setString(SP_USER_EMAIL, email);
      await sp.setString(SP_USER_URL, url);
      UserInfo userInfo = new UserInfo(
        id: id,
        name: name,
        gender: gender,
        avatar: avatar,
        email: email,
        location: location,
        url: url
      );
      return userInfo;
    }
    return null;
  }
复制代码

保存用户信息是一个异步的过程,其中UserInfo是定义在lib/model/下的一个实体类,代码以下:

// 用户信息
class UserInfo {

  String gender;
  String name;
  String location;
  num id;
  String avatar;
  String email;
  String url;

  UserInfo({this.id, this.name, this.gender, this.avatar, this.email, this.location, this.url});

}
复制代码

为了方便的拿到保存的用户信息和AccessToken数据,以及判断当前是否登陆,为DataUtils提供三个静态方法:

// 获取用户信息
  static Future<UserInfo> getUserInfo() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    bool isLogin = sp.getBool(SP_IS_LOGIN);
    if (isLogin == null || !isLogin) {
      return null;
    }
    UserInfo userInfo = new UserInfo();
    userInfo.id = sp.getInt(SP_USER_ID);
    userInfo.name = sp.getString(SP_USER_NAME);
    userInfo.avatar = sp.getString(SP_USER_AVATAR);
    userInfo.email = sp.getString(SP_USER_EMAIL);
    userInfo.location = sp.getString(SP_USER_LOC);
    userInfo.gender = sp.getString(SP_USER_GENDER);
    userInfo.url = sp.getString(SP_USER_URL);
    return userInfo;
  }

  // 是否登陆
  static Future<bool> isLogin() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    bool b = sp.getBool(SP_IS_LOGIN);
    return b != null && b;
  }

  // 获取accesstoken
  static Future<String> getAccessToken() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    return sp.getString(SP_AC_TOKEN);
  }
复制代码

若是用户注销登陆,须要清除已保存的用户信息:

// 清除登陆信息
  static clearLoginInfo() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    await sp.setString(SP_AC_TOKEN, "");
    await sp.setString(SP_RE_TOKEN, "");
    await sp.setInt(SP_UID, -1);
    await sp.setString(SP_TOKEN_TYPE, "");
    await sp.setInt(SP_EXPIRES_IN, -1);
    await sp.setBool(SP_IS_LOGIN, false);
  }
复制代码

源码

本篇相关的全部源码都在GitHub上demo-flutter-osc项目的v0.3分支

后记

本篇主要记录的是基于Flutter的开源中国客户端app中的网络请求和数据存储方式,写得不清楚的地方请多包涵,有问题能够留言告诉笔者。下一篇将记录Flutter中的插件使用。

个人开源项目

  1. 基于Google Flutter的开源中国客户端,但愿你们给个Star支持一下,源码:
  1. 基于Flutter的俄罗斯方块小游戏,但愿你们给个Star支持一下,源码:
上一篇 下一篇
从0开始写一个基于Flutter的开源中国客户端(6)
——各个静态页面的实现
从0开始写一个基于Flutter的开源中国客户端(8)——插件的使用
相关文章
相关标签/搜索