iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0
Mobile Development

Flutter 30: from start to store系列 第 14

Flutter介紹:取得外部資料 - networking

  • 分享至 

  • xImage
  •  

今天和大家一起來看看在flutter中如何進行網際資料交換,分為以下部分:

  • 什麼是Future
  • Future相關功能
  • 透過http套件進行flutter networking
  • 專案實作

好的,那我們就開始吧!


Future

  • 表示未來某個時間內獲得的資料。

  • 當我們呼叫一個非同步函式時,會回傳一個「未完成的Future」來等待此函式的非同步行為執行完畢,並獲得結果,也可能在執行的過程中出現問題而回傳錯誤

  • Future<int>代表這個非同步行為回傳的資料型別是int,也可以使用自定義的類別如

    Future<List<OrderDetail>> // 資料的型別是「由OrderDetail組成的List」
    
  • 常見使用Future.delayed constructor來建立一個延遲時間的Future:`

    // 等待兩秒之後印出 Here I come!
    Future.delayed(const Duration(seconds: 2), () => print('Here I come!')); 
    

then

  • 表示執行了一個行為並獲得執行結果後,接續執行的下一個行為:

    final server = connectToServer();
    server
        // 向server要求資料回應
        .post(myUrl, fields: const {'name': 'Dash', 'profession': 'mascot'}) 
        .then(handleResponse) // 接下來,處理server的回應
    

catchError

  • 攔截非同步行為中出現的錯誤
    final server = connectToServer();
    server
        .post(myUrl, fields: const {'name': 'Dash', 'profession': 'mascot'})
        .then(handleResponse)
        .catchError(handleError) // 攔下錯誤並透過 handleError function處理
    

whenComplete

  • 當所有then和catchError都執行結束後,最後要執行的事項
    final server = connectToServer();
    server
        .post(myUrl, fields: const {'name': 'Dash', 'profession': 'mascot'})
        .then(handleResponse)
        .catchError(handleError)
        .whenComplete(server.close); // 不管是獲得response或出現error,最後都將server關閉
    

timeout

  • 設定時間,超過這個時間就介入進行某個操作
    httpRequest.timeout(const Duration (seconds:5),onTimeout :(){
        return 'timeout!'
    });
    
    如上例,此httpRequest執行超過五秒之後,會收到一個預設值:'timeout!'

FutureBuilder Widget

  • 將非同步取得的資料結合UI顯示在頁面上,可以依照當前非同步行為的狀態改變UI的呈現

    FutureBuilder({
      this.future,
      this.initialData,
      required this.builder,
    })
    
    • future指的是一個非同步行為
    • initialData: 初始的資料
    • builder: 建立UI的函式,方法如下:
      Function (BuildContext context, AsyncSnapshot snapshot) 
      
      context是當前頁面情境所包含的資訊,snapshot包含了這次非同步任務的資訊
      例如 snapshot.connectionState(目前的非同步任務執行狀態), snapshot.hasError(是否產生錯誤), ...等等
  • 寫法: 以官網案例說明

    final Future<String> _calculation = Future<String>.delayed(
        const Duration(seconds: 2),
        () => 'Data Loaded',
    ); // 在兩秒後顯示 'Data Loaded'
    
    
    FutureBuilder<String>(
        future: _calculation, // 執行_calculation 內定義的非同步任務
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          List<Widget> children;
          if (snapshot.hasData) { 
            // 若是成功執行_calculation並獲取回傳的內容,則顯示children UI如下
            children = <Widget>[
              const Icon(
                Icons.check_circle_outline,
                color: Colors.green,
                size: 60,
              ),
              Padding(
                padding: const EdgeInsets.only(top: 16),
                child: Text('Result: ${snapshot.data}'),
              ),
            ];
          } else if (snapshot.hasError) { 
            // 若是在_calculation執行過程中出現錯誤,則顯示children UI如下
            children = <Widget>[
              const Icon(
                Icons.error_outline,
                color: Colors.red,
                size: 60,
              ),
              Padding(
                padding: const EdgeInsets.only(top: 16),
                child: Text('Error: ${snapshot.error}'),
              ),
            ];
          } else { 
            // 預設顯示的children UI
            children = const <Widget>[
              SizedBox(
                width: 60,
                height: 60,
                child: CircularProgressIndicator(),
              ),
              Padding(
                padding: EdgeInsets.only(top: 16),
                child: Text('Awaiting result...'),
              ),
            ];
          }
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: children,
            ),
          );
        },
    ),
    
    

async and await

  • .then, .catchError是不同的概念,目的是將一連串的非同步行為轉為同步方式處理

  • async:將function標示為非同步函式,在function內部可以使用await

  • await: 將function內各個非同步的步驟轉為同步,會等待該步驟的執行結果,才開始下一個步驟

    Future<void> printDailyNewsDigest() async {
      try {
        var newsDigest = await gatherNewsReports();
        print(newsDigest);
      } catch (e) {
        // Handle error...
      }
    }
    

    如上例,我們已知gatherNewsReports的行為無法及時完成,需要一段時間;使用await會在這邊等到gatherNewsReport回傳資料或報錯,才會繼續執行下一步


flutter networking

  • 透過http package進行網際資料交換

http package

  • flutter 用來進行http request、從網路取得資料的套件,詳情見套件頁面
  • 安裝方式:
    1. 在terminal下指令,將http紀錄到專案中的pubspec.yaml,日後看pubspec.yaml就知道這個專案的運作需要http套件。
      $ flutter pub add http
      
    2. 下指令安裝所有需要的package
      $ flutter pub get
      

permission in android

  • 在android上要另外要求app使用網路的權限
  • android/app/src/main/AndroidManifest.xml,也就是Android app的設定檔上加入
    <manifest xmlns:android...>
     ...
    
     <!--加入這行--> 
     <uses-permission android:name="android.permission.INTERNET" /> 
    
     <application ...
    </manifest>
    

use http package

  • 使用範例:例如我要在專案中進行一個post request

    import 'package:http/http.dart' as http; // 在檔案中引用http套件
    
    var url = Uri.https('example.com', 'whatsit/create');
    var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'}); // 透過http套件進行post request
    
    print('Response status: ${response.statusCode}');
    print('Response body: ${response.body}');
    
    print(await http.read(Uri.https('example.com', 'foobar.txt')));
    

專案實作

  1. 在專案中加入http package

    $ flutter pub add http
    
    $ flutter pub get
    
  2. NASA OPEN API 官網申請自己的API KEY,並記錄下來

  3. 找到 APOD API 並詳閱下方的document

  4. 在專案中的lib 之中建立 model資料夾,存放data model

  5. 在model中新增 ApodData.dart

  6. 建立class ApodData。看過APOD api document之後,我們知道每次request會回傳以下資料

    {
        "date": "2022-10-01",
        "explanation": "Observe the Moon every night and you'll see its visible sunlit portion gradually change. In phases progressing from New Moon to Full Moon to New Moon again, a lunar cycle or lunation is completed in about 29.5 days. Top left to bottom right, this 7x4 matrix of telescopic images captures the range of lunar phases for 28 consecutive nights, from the evening of July 29 to the morning of August 26, following an almost complete lunation. No image was taken 24 hours or so just after and just before New Moon, when the lunar phase is at best a narrow crescent, close to the Sun and really hard to see. Finding mostly clear Mediterranean skies required an occasional road trip to complete this lunar cycle project, imaging in early evening for the first half and late evening and early morning for the second half of the lunation. Since all the images are registered at the same scale you can use this matrix to track the change in the Moon's apparent size during the single lunation. For extra credit, find the lunar phase that occurred closest to perigee.  Tonight: International Observe the Moon Night",
        "hdurl": "https://apod.nasa.gov/apod/image/2210/Lu20220729-0826.jpg",
        "media_type": "image",
        "service_version": "v1",
        "title": "Lunation Matrix",
        "url": "https://apod.nasa.gov/apod/image/2210/Lu20220729-0826_1050.jpg"
    }
    

    所以我們將必要的資料加入ApodData

    class ApodData {
      final String title; // 圖片標題
      final String url; // 圖片資源連結
      final String mediaType;  // 圖片類型
      final String desc;  // 圖片描述
      final String date;  // 日期
    
      ApodData(this.title, this.url, this.mediaType, this.desc, this.date);
    
      ApodData.fromJson(Map<String, dynamic> json)
          : title = json['title'],
            url = json['hdurl'],
            mediaType = json['media_type'],
            desc = json['explanation'],
            date = json['date'];
    
      Map<String, dynamic> toJson() => {
            'title': title,
            'url': url,
            'media_type': mediaType,
            'explanation': desc,
            'date': date,
          };
    }
    
  7. 在main_page建立 call api的function。記得將api key使用gitignore隱藏起來

    class _MainPageState extends State<MainPage> {
      final String apodUrl = 'https://api.nasa.gov/planetary/apod';
    
      @override
      void initState() {
        _fetchDailyApodData(); // 在頁面生成時取得APOD 資訊
        super.initState();
      }
    
      Future<ApodData?> _fetchDailyApodData() async {
        Uri url = Uri.parse('$apodUrl?api_key=$apiKey&thumbs=true');
        final response = await http.get(url, headers: {
          'Content-type': 'application/json',
          'Accept': 'application/json',
        });
    
        final parsedResponse = json.decode(response.body) as Map<String, dynamic>;
        return ApodData.fromJson(parsedResponse);
      }
    
      @override
      Widget build(BuildContext context) {
        Size deviceScreen = MediaQuery.of(context).size;
    
        return SingleChildScrollView(
          child: FutureBuilder(
            future: _fetchDailyApodData(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                ApodData? data = snapshot.data;
                return Column(
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: Text(
                        data != null ? data.title : '',
                        style: const TextStyle(
                            fontSize: 30, fontWeight: FontWeight.w500),
                      ),
                    ),
                    Stack(
                      children: [
                        SizedBox(
                          width: deviceScreen.width,
                          child: data != null
                              ? Image.network(data.url, frameBuilder:
                                  (context, child, frame, wasSynchronouslyLoaded) {
                                  if (wasSynchronouslyLoaded) {
                                    return child;
                                  }
                                  return AnimatedOpacity(
                                    opacity: frame == null ? 0 : 1,
                                    duration: const Duration(seconds: 1),
                                    curve: Curves.easeOut,
                                    child: child,
                                  );
                                })
                              : SizedBox(
                                  width: deviceScreen.width,
                                  height: deviceScreen.width,
                                  child: const Center(
                                      child: Text(
                                    '圖片載入錯誤',
                                    style:
                                        TextStyle(color: Colors.red, fontSize: 30),
                                  )),
                                ),
                        ),
                        Positioned(
                          top: 10.0,
                          right: 10.0,
                          child: ElevatedButton(
                              onPressed: () {
                                print('add to favorite');
                              },
                              child: const Text('favorite')),
                        ),
                      ],
                    ),
                    Padding(
                      padding: const EdgeInsets.all(10.0),
                      child: Text(
                        data != null ? data.desc : '',
                        style:
                            const TextStyle(fontSize: 16, color: Colors.blueGrey),
                      ),
                    ),
                  ],
                );
              }
              if (snapshot.hasError) {
                return const Center(
                    child: Text(
                  '頁面載入錯誤',
                  style: TextStyle(color: Colors.red, fontSize: 30),
                ));
              }
              return SizedBox(
                  height: deviceScreen.height,
                  width: deviceScreen.width,
                  child: const Center(child: CircularProgressIndicator()));
            },
          ),
        );
      }
    }
    
  • 本次改動的相關程式碼放在我的github,見Day14相關commit

Recap

今天大致瞭解了

  • Future
    • Future.then: 完成一個非同步行為後接著進行的動作
    • Future.catchError:攔截過程中出現的問題
    • Future.whenComplete: 無論非同步任務執行成功與否,最後要執行的動作
    • Future.timeout: 設定非同步任務的逾時時間以及對應的行為
    • FutureBuilder: 結合非同步任務變更UI
    • 透過async await處理非同步行為
  • http package的使用方式

明天一起來看看如何用Icon, TextInput, List等組件構成頁面吧~


上一篇
Flutter介紹:在App內導頁 - navigation
下一篇
Flutter頁面的建構:Icon, TextInput, ListView
系列文
Flutter 30: from start to store30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言