ReactNative網絡fetch數據并展示在listview中

看完全文并且do it你將收獲:

  1. fetch獲取網絡數據的基本用法
  2. ListView的基本用法
  3. RN中組件的生命周期(有可能)

類似的實現其實有很多朋友已經寫過了,我也看了幾篇類似博客

這篇《react-native中listview獲取豆瓣書本信息并展示出來》確實是一個值得學習的例子,但作者只是一步一步貼代碼,沒有很好的細講

這篇《React Native從網絡拉取數據并填充列表》寫的不錯,值得一看,不過listview沒有圖片,效果看起來沒有我的有逼格(傲嬌臉)

來,先看下最終我們要實現一個怎么樣的效果吧:


實現效果

好,正題開始:

要展示出這樣一個listview的效果,首先需要有數據源,我這里私藏了兩個能返回json(帶有圖片地址)的url:

url最后的數字是返回數據的數量,可以自己修改
比如第一個url將返回30條數據,第二個url將返回3條

  1. http://www.imooc.com/api/teacher?type=4&num=30
  2. https://api.douban.com/v2/book/search?q=react&count=3

我們先看一看返回的json長啥樣,在瀏覽器中輸入url后:

我使用的是chrome瀏覽器,通過插件“JSON-handle”將json格式化并展示了出來,很友好的效果,其他瀏覽器也有類似插件,可自行搞定

在瀏覽器中看到的json數據

果然是好長一段的json!我們需要的內容在json>data中,對比預覽圖得知需要的數據段為:name、picSmall(當然用小圖啦)、description三個。

OK,既然有了數據源,那么接下來要做的就是通過網絡請求去獲取數據

RN中的網絡請求通過Fetch進行,最簡單的fetch寫法是傳入一個url參數:

fetch('http://www.imooc.com/api/teacher?type=4&num=30');

通過這一句就成功發起了一個網絡請求,當然fetch方法還有第二個可選參數,這個參數用于傳入一些HTTP請求的參數如:headers、請求方式(GET/POST)等
援引RN中文網fetch一段示例代碼:

fetch('https://mywebsite.com/endpoint/', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    firstParam: 'yourValue',
    secondParam: 'yourOtherValue',
  })
})

僅用于獲取簡單的數據則完全可以忽略第二個參數

在用fetch請求網絡成功后,會返回的是一個Promise對象,用于檢索我們需要的數據,但我們需要的是一個json對象來便于處理數據,那么怎樣將這個Promise對象轉換成json對象呢?fetch有非常方便的鏈式調用方法來搞定:

    fetch('https://mywebsite.com/endpoint/')
    .then((response) => response.json());//response就是fetch返回的Promise對象

    //.then()中傳入一個回調函數,該回調函數接收fetch返回的對象
    //你可以在這個時候將Promise對象轉換成json對象:response.json()
    //轉換成json對象后return,給下一步的.then處理

沒錯!.then可以一直接下去!并且每一個.then都接收一個回調函數,這個回調函數都接收上一個.then返回的數據,這樣做數據處理豈不是美滋滋!

ok,既然這樣,那我們在下一個.then中接收到這個由上一個.then返回的json對象,并將他設置給this.state以便于后面的使用:

  constructor(props){
    super(props);

    this.state = {
      data: null,
    };
  }

  componentDidMount(){
    fetch('http://www.imooc.com/api/teacher?type=4&num=30')
    .then((response) => response.json())
    .then((jsonData) => {        //jsonData就是上一步的response.json()
      this.setState({
        data: jsonData.data,     //data是一個對象數組
      });
    })                           //你后面還可以繼續瘋狂的接.then并且傳遞數據,盡管試試吧!
    .catch((error) => {          //注意尾部添加異常回調
      alert(error);
    });
  }
//上一個.then的回調函數返回的數據,由下一個.then的回調函數接收

為啥把fetch寫在componentDidMount()中呢,因為這是一個RN中組件的生命周期回調函數,會在組件渲染后回調
關于更多組件生命周期戳React Native 中組件的生命周期

那么我們最終得到的this.state.data到底長啥樣的?根據json結構來分析一下,json.data是一個對象數組,現在賦給了this.state.data,那么this.state.data應該就是一個對象數組,我們來檢測一下:

  componentDidMount(){
    fetch('http://www.imooc.com/api/teacher?type=4&num=30')
    .then((response) => response.json())
    .then((jsonData) => {
      this.setState({
        data: jsonData.data,
      });
      let n = this.state.data[0].name;
      let d = this.state.data[0].description;
      alert("n:" + n + ",d:" + d);
    })
    .catch((error) => {
      alert(error);
    });
  }

double r:

提取data中的數據

很好,這說明data是一個對象數組,通過數組取值方法取到了值,結構看json那邊,這樣就很好的可以取到其中的值

data中的第一條數據
關于更多詳細的fetch介紹可以自行G/B,也墻裂建議認真看看、理解fetch

拿到了數據,接下來開始使用listview了

首先得導入ListView組件(我很容易忘記導入組件),接下來就是把它寫在布局中
使用<ListView>的時候,有兩個必須參數:dataSource、renderRow

<ListView
  dataSource={}      //必須寫此屬性并且傳入dataSource對象
  renderRow={}>      //必須寫此屬性并且傳入一個返回component的回調函數
</ListView>

這個很容易理解,listview要展示數據,必須得有數據源(dataSource),之后listview必須知道他每一行長啥樣,他才能去渲染(renderRow)

dataSource

dataSource是ListView下的一個類,新建一個dataSource:

let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
//DataSource()可以接收四種參數
//rowHasChanged就是渲染每一行的依據,r1是上一行的數據,r2是下一行的數據
//此處定義:r1和r2不同時,需要渲染新的一行

關于DataSource可接收四種參數:

  1. getRowData(dataBlob, sectionID, rowID):表明我們將以何種方式從dataBlob(數據源)中提取出rowData,sectionID用于指定每一個section的標題名(在renderRow,renderHeader等方法中會默認拆開并作為參數傳入)
  2. getSectionHeaderData(dataBlob, sectionID):表明我們將以何種方式從dataBlob(數據源)中提取出HeaderData。HeaderData用于給每一個sectionHeader賦值
  3. rowHasChanged(prevRowData, nextRowData):指定我們更新row的策略,一般來說都是prevRowData和nextRowData不相等時更新row
  4. sectionHeaderHasChanged(prevSectionData, nextSectionData):指定我們更新sectionHeader的策略,一般如果用到sectionData的時候才指定

引自:React-Native組件用法詳解之ListView
==========本次Demo實現只需要用到rowHasChanged

墻裂建議認真學習更多listview詳細內容

這個時候我們創建出來的ds還并不擁有任何數據,在傳給listview之前還要進行一個步驟:

let lvData = ds.cloneWithRows(this.state.data);
//可以粗暴理解為:將this.state.data克隆到lvData,只有經過這一步的數據才能給listview使用

我們希望網絡請求到數據時立即進行cloneWithRows這一步,這樣this.state.data在網絡請求完成后就直接被賦值成為可以傳遞給listview的對象:

    fetch('http://www.imooc.com/api/teacher?type=4&num=30')
    .then((response) => response.json())
    .then((jsonData) => {
      this.setState({
        data: new ListView.DataSource({rowHasChanged: (r1,r2) => r1!==r2 }).cloneWithRows(jsonData.data),
      });
    })
    .catch((error) => {
      alert(error);
    });
renderRow

<listview>的renderRow屬性接收一個“返回每一行的組件的回調函數”,該回調函數接收一個分配給該行的數據,在本例中即為json.data中的子項
OK,既然知道renderRow需要啥,并且知道每一行長啥樣:

listview的一行的樣式

我們就可以根據這個樣式來寫一個需要的回調函數:

export default class lvtest extends Component {

………………
  // 返回listview的一行
  renderRow(rowData){//參數為接收的每一行的數據,理解:數組data的子項
    return(
      <View                                //最外層包裹的View
        style = {styles.lvRow}>
          <Image                           //左側顯示的圖片
            style = { styles.img }
            source = { { uri: rowData.picSmall } }/>//source為rowData中的picSmall,
                                                    //致于你換rowData.picBig也行,更消耗流量加載慢就是
        <View                              //包裹文本的View
          style = { styles.textView }>
          <Text                            //標題
            style = { styles.textTitle }
            numberOfLines = { 1 }>         //只顯示一行
            { rowData.name }               //取rowData中的name字段,理解:data[某行].name
                                  //注意:this.state.data不能直接當做數組使用,他是一個dataSource對象
          </Text>
          <Text                //詳細內容文本
            style = { styles.textContent }>
            { rowData.description }        //取rowData中的description字段,理解:data[某行].description
          </Text>
        </View>
      </View>
    )
  }
}
//樣式,
const styles = StyleSheet.create({
  lvRow: {
    flex: 1,
    flexDirection: 'row',
    padding: 10,
  },
  textView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 5,
  },

  textTitle: {
    flex: 1,
    textAlign: 'center',
    color: '#f00',
  },

  textContent: {
    flex: 1,
    fontSize: 11,
    color: '#000',
    textAlign: 'center',
  },

  img: {
    height: 55,
    width: 100,
  }
});

除了rowData.name、rowData.description、rowData.picSmall,我相信大家都很容易看明白這個布局的實現,至此我們所有的代碼如下:(還有個坑呢,繼續看完!)

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  View,
  Text,
  ListView
} from 'react-native';


export default class lvtest extends Component {

  constructor(props){
    super(props);
    this.state = {
      data: null,
    };
  }

  render() {
    return (
      <ListView
        dataSource={this.state.data}
        renderRow={(rowData) => this.renderRow(rowData)}>
      </ListView>
    );
  }


// 生命周期回調函數,在其中進行網絡請求
  componentDidMount(){
    fetch('http://www.imooc.com/api/teacher?type=4&num=30')
    .then((response) => response.json())
    .then((jsonData) => {
      this.setState({
        data: new ListView.DataSource({rowHasChanged: (r1,r2) => r1!==r2 }).cloneWithRows(jsonData.data),
      });
    })
    .catch((error) => {
      alert(error);
    });
  }
  // 返回listview的一行
  renderRow(rowData){
    return(
      <View 
        style = {styles.lvRow}>
          <Image 
            style = { styles.img }
            source = { { uri: rowData.picSmall } }/>
        <View 
          style = { styles.textView }>
          <Text
            style = { styles.textTitle }
            numberOfLines = { 1 }>
            { rowData.name }
          </Text>
          <Text
            style = { styles.textContent }>
            { rowData.description }
          </Text>
        </View>
      </View>
    )
  }
}



const styles = StyleSheet.create({
  lvRow: {
    flex: 1,
    flexDirection: 'row',
    padding: 10,
  },
  textView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 5,
  },

  textTitle: {
    flex: 1,
    textAlign: 'center',
    color: '#f00',
  },

  textContent: {
    flex: 1,
    fontSize: 11,
    color: '#000',
    textAlign: 'center',
  },

  img: {
    height: 55,
    width: 100,
  }
});

AppRegistry.registerComponent('lvtest', () => lvtest);

當我懷著無比期待的心情double r之后:

瘋狂報錯

仔細一看,咦,貌似說dataSource為空,我不是通過fetch獲取到了數據并且clone給了this.state.data嗎?怎么會是空呢?

通過你不懈的排查發現,原來是這里的問題:
  render() {
    return (
      <ListView
        dataSource={this.state.data}
        renderRow={(rowData) => this.renderRow(rowData)}>
      </ListView>
    );
  }

APP一啟動,馬上就要渲染ListView,而fetch數據是一個耗時操作,在啟動APP的一瞬間this.state.data肯定是沒有數據的!所以在渲染listview時提取數據,發現你傳給他的dataSource居然是個空的,那他當然不干,要給你報紅了!

既然知道哪里錯了,那就來解決一下:

  render() {
    if(!this.state.data){//如果this.state.data沒有數據(即網絡請求未完成),則返回一個加載中的文本
      return(
          <Text>loading...</Text>
      );
    } else {//當this.state.data有了數據,則渲染ListView
      return (
        <ListView
          dataSource={this.state.data}
          renderRow={(rowData) => this.renderRow(rowData)}>
        </ListView>
      );
    }
  }

OK,這個時候再來double r跑一下:

究極形態

到此,我們就很完美的實現了這樣一個小demo,致于最頂端那個預覽有點擊效果,你可以在renderRow中的布局里,通過包裹<Touchable...>來實現,還有很多可玩性等你do it!

下面附上整個【index.android.js】代碼:

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  View,
  Text,
  ListView,
  Image
} from 'react-native';


export default class lvtest extends Component {

  constructor(props){
    super(props);

    this.state = {
      data: null,
    };
  }

  render() {
    if(!this.state.data){
      return(
          <Text>loading...</Text>
      );
    } else {
      return (
        <ListView
          dataSource={this.state.data}
          renderRow={(rowData) => this.renderRow(rowData)}>
        </ListView>
      );
    }
  }

// 生命周期回調函數
  componentDidMount(){
    fetch('http://www.imooc.com/api/teacher?type=4&num=30')
    .then((response) => response.json())
    .then((jsonData) => {
      this.setState({
        data: new ListView.DataSource({rowHasChanged: (r1,r2) => r1!==r2 }).cloneWithRows(jsonData.data),
      });
    })
    .catch((error) => {
      alert(error);
    });
  }


  // 返回listview的一行
  renderRow(rowData){
    return(
      <View 
        style = {styles.lvRow}>
          <Image 
            style = { styles.img }
            source = { { uri: rowData.picSmall } }/>
        <View 
          style = { styles.textView }>
          <Text
            style = { styles.textTitle }
            numberOfLines = { 1 }>
            { rowData.name }
          </Text>
          <Text
            style = { styles.textContent }>
            { rowData.description }
          </Text>
        </View>
      </View>
    )
  }
}



const styles = StyleSheet.create({
  lvRow: {
    flex: 1,
    flexDirection: 'row',
    padding: 10,
  },
  textView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 5,
  },

  textTitle: {
    flex: 1,
    textAlign: 'center',
    color: '#f00',
  },

  textContent: {
    flex: 1,
    fontSize: 11,
    color: '#000',
    textAlign: 'center',
  },

  img: {
    height: 55,
    width: 100,
  }
});

AppRegistry.registerComponent('lvtest', () => lvtest);

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容