# 数据请求的实现
前面在实现信息流的时候，使用的是本地写死的数据，这个肯定是不行的，所以本节介绍如何去请求数据并将数据显示出来。

## 豆瓣电影的API
豆瓣电影的API参考的是 [Github](https://github.com/koudle/The-Guide-to-the-Flutter/blob/master/flutter_doubanmovie/douban_api.md) 上的资料，API包括：

* 正在热映的电影 
* 获取电影Top250
* 获取即将上映电影
* 电影搜索
* 电影详情

本节使用正在热映的电影的API来获取信息流页面的数据。

## 给 Flutter 工程添加第三方库
请求数据需要用到 http，所以首先需要找到一个第三方的 http库，然后在 Flutter 工程里添加第三方库的依赖声明。

#### 找第三方库
可以在 [https://pub.dartlang.org/](https://pub.dartlang.org/) 网站上查找想要的库。

比如，我想要一个 http 库，那在这个网站上搜索 http，并且选中 Flutter，表示要查找的是 Flutter 的库：

![](https://user-gold-cdn.xitu.io/2019/4/13/16a158845dcdfd93?w=1986&h=1242&f=jpeg&s=270535)

然后在搜索结果里选择想要的库使用。

#### 添加依赖声明
我选择第一个搜索结果，点进去之后，选择 `Installing` 页面：

![](https://user-gold-cdn.xitu.io/2019/4/13/16a158b01c7b00bb?w=2160&h=1608&f=jpeg&s=322166)

这个页面会告诉你如何使用第三方库。

* pubspec.yaml

    在 pubspec.yaml 的 dependencies 添加:
    ```
    dependencies:
      flutter:
        sdk: flutter
    
      # The following adds the Cupertino Icons font to your application.
      # Use with the CupertinoIcons class for iOS style icons.
      cupertino_icons: ^0.1.2
      http: ^0.12.0+2
    ```
* flutter packages get

    在添加完依赖之后，在当前工程目录下还要运行：
    ```
    flutter packages get
    ```
    这一步的目的是在本地安装第三方库。
    
    在 VS Code 里，选择面板的 TERMINAL 运行：
    ![](https://user-gold-cdn.xitu.io/2019/4/13/16a159598cf150d5?w=1114&h=286&f=jpeg&s=68624)
    
## 使用 http 发起请求
因为 http 请求比较耗时，所以为了不阻塞 UI，http 的请求使用 async异步函数，async异步函数是 Flutter 的异步编程，下节会详细介绍 Flutter 的异步编程。

#### async 
使用 async 发起请求：
```
    List<HotMovieData> serverDataList = new List();
    var response = await http.get(
        'https://api.douban.com/v2/movie/in_theaters?city=%E6%B7%B1%E5%9C%B3&start=0&count=10');
    //成功获取数据
    if (response.statusCode == 200) {
      print(response.body);
    } 
  }
```

这里 await 的作用是，等待 http.get() 的数据返回后再执行下一步。因为下一步要对数据进行解析，所以一定要等 http.get() 返回数据。

#### json数据解析
`response.body` 是返回的数据，为了方便数据解析，可以讲 String 转换成 json 来解析，就需要用到 `dart:convert` 库里的 `json.decode()` 方法。`json.decode()` 方法会将 String 解析成 Map数据结构： `Map<String, dynamic>`,那么取数据的格式就是 `object[key]`。

解析的方法如下：
```
var responseJson = json.decode(response.body);
```

下面是正在上映的电影的数据格式如下：

![](https://user-gold-cdn.xitu.io/2019/4/13/16a166f877afe8f1?w=660&h=690&f=jpeg&s=64195)

`subjects` 里的就是具体的正在上映的电影的信息，而 `subjects` 是一个数组，解析数组数据的方法如下：
```
for (dynamic data in responseJson['subjects']) {
    HotMovieData hotMovieData = HotMovieData.fromJson(data);
    serverDataList.add(hotMovieData);
}
```
这里我们按照 json 的数据格式，创建一个数据类  HotMovieData：
```
import 'dart:convert';

class HotMovieData {
  Rating rating;
  List<String> genres;
  String title;
  List<Cast> casts;
  int collectCount;
  List<Cast> directors;
  Avatars images;
  String id;

  HotMovieData();
}

class Rating {
  int max;
  double average;
  int min;

  Rating(this.max,this.average,this.min);
}

class Cast {
  String alt;
  Avatars avatars;
  String name;
  String id;

  Cast(this.alt,this.avatars,this.name,this.id);
}

class Avatars {
  String small;
  String large;
  String medium;

  Avatars(this.small,this.large,this.medium);
}
```

这里我们把电影信息的解析放在了 HotMovieData 里来解析，给 HotMovieData 增加一个 fromJson 的工厂构造函数：
```
factory HotMovieData.fromJson(Map<String, dynamic> movieDataJson) {
    HotMovieData hotMovieData = HotMovieData();

    Rating rating = Rating(movieDataJson['rating']['max'], double.parse(movieDataJson['rating']['average'].toString()), movieDataJson['rating']['min']);
    hotMovieData.rating = rating;

    List<String> genres =new List();
    for(String genre in movieDataJson['genres']){
      genres.add(genre);
    }
    hotMovieData.genres = genres;

    hotMovieData.title = movieDataJson['title'];

    List<Cast> casts = new List();
    for (dynamic castData in movieDataJson['casts']) {
      Avatars avatars = Avatars(castData['avatars']['small'], castData['avatars']['large'], castData['avatars']['medium']);
      Cast cast= new Cast(castData['alt'],avatars,castData['name'],castData['id']);
      casts.add(cast);
    }
    hotMovieData.casts = casts;

    hotMovieData.collectCount = movieDataJson['collect_count'];

    List<Cast> directors = new List();
    for (dynamic castData in movieDataJson['directors']) {
      Avatars avatars = Avatars(castData['avatars']['small'], castData['avatars']['large'], castData['avatars']['medium']);
      Cast cast= new Cast(castData['alt'],avatars,castData['name'],castData['id']);
      directors.add(cast);
    }
    hotMovieData.directors = directors;

    hotMovieData.images = Avatars(movieDataJson['images']['small'],movieDataJson['images']['large'],movieDataJson['images']['medium']);

    hotMovieData.id = movieDataJson['id'];
    return hotMovieData;
  }
```

解析完后的数据要保存起来，然后复制给变量 hotMovies，解析数据到复制的完整代码如下：
```
void _getData() async {
    List<HotMovieData> serverDataList = new List();
    var response = await http.get(
        'https://api.douban.com/v2/movie/in_theaters?apikey=0b2bdeda43b5688921839c8ecb20399b&city=%E6%B7%B1%E5%9C%B3&start=0&count=10&client=&udid=');
    //成功获取数据
    if (response.statusCode == 200) {
      var responseJson = json.decode(response.body);
      for (dynamic data in responseJson['subjects']) {
        HotMovieData hotMovieData = HotMovieData.fromJson(data);
        serverDataList.add(hotMovieData);
      }
      setState(() {
        hotMovies = serverDataList;
      });
    } 
  }
```

#### 修改 initState()
将 initState() 里的代码修改为：
```
 @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _getData();
  }
```

然后运行程序，运行的效果为：

![](https://user-gold-cdn.xitu.io/2019/4/13/16a167aa4b4cd81f?w=413&h=768&f=jpeg&s=80606)

## 添加刷新界面
因为 http 请求是异步的，所以这里加一个刷新界面：
```
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    if (hotMovies == null || hotMovies.isEmpty) {
      return Center(
        child: CircularProgressIndicator(),
      );
    } else {
      return MediaQuery.removePadding(
        ...
      );
    }
  }
```
效果如下图：

![](https://user-gold-cdn.xitu.io/2019/4/29/16a69bc7a15d7d9b?w=320&h=640&f=gif&s=135474)

## 优化
在加了刷新界面后，发现 TabBar 每次切换，Widget 都会重新加载，会重新请求数据，是因为 DefaultTabController 在切换 Widget 的时候，为了回收内存，会将不显示的 Widget 回收掉，但是这个不是我们想见到的，我们要切换 Widget 的时候，不会回收不显示的 Widget，这就需要让 HotMoviesListWidgetState mixin  AutomaticKeepAliveClientMixin。

代码如下：
```
class HotMoviesListWidgetState extends State<HotMoviesListWidget> with AutomaticKeepAliveClientMixin {
  ....

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true; //返回 true，表示不会被回收
}
```


