iT邦幫忙

2023 iThome 鐵人賽

DAY 16
1

今天我們將開始進行 API 串接,在開始講解之前請先開啟之前在 Day10 時建立的 api server。若你已將 server 部署至 vercel 上可以忽略此步驟。

// 請開啟之前下載的 db.json 的資料夾,並執行以下指令
npm run dev

此時於使用瀏覽器開啟 http://localhost:3000 或部署的網址,可以成功看到畫面則表示啟動成功。

API 是什麼

在串接之前讓我們先來前導一下 API 是什麼。API 為 Application Programming Interface 的簡稱,允許不同的軟體間進行交互,以實現特定的功能或是服務。

在接下來與未來篇章中,當有使用到 API 的服務時,我們都會提供該 API 的接口及參數。

新聞分類 API

API 接口 - http://localhost:3000/categories

當呼叫此 API 時,將獲得所有的新聞分類及各字的封面圖片的 JSON 格式資料。

[
    {
        "id": 1,
        "name": "政治",
        "cover": "https://images.unsplash.com/photo-1529107386315-e1a2ed48a620?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=720&q=80"
    },
   ...
]

因此當我們需要動態顯示新聞分類資料時,會先透過打此 API,並等待完成之後再進行畫面的渲染。

建立新聞分類 model

從上述的 API 可以知道我們可將獲得的資料使用 List 來存取,但尚缺少了一個專門用於表示新聞分類的型態。依我的習慣會建立一個 models 的資料夾,裡面專門用於存放自定義的資料型態。因此請於 lib 資料夾底下創建 models 資料夾,並同時建立一個 news_category.dart 的檔案於其中。
讓我們觀察每筆新聞分類的 record 結構:

  • id:數字型態,用於識別新聞分類項目
  • name :字串型態,用於標明新聞分類的名稱
  • cover:字串型態,存放該分類的封面圖片

因此我們可以在該檔案中如此定義:

class NewsCategory {
  final int id;
  final String name;
  final String imageAssetUrl;

  NewsCategory(
      {required this.id, required this.name, required this.imageAssetUrl});
}

建立新聞分類 repository

前幾天我們在 Day13 的文章中的一開始講述重整程式碼的區塊有請各位創建一個 repositories 的資料夾,建立了一個 auth_repository.dart 並說明該檔案用於放置一切與驗證相關的對外獲取資料統一接口。
這麼做的一大好處在於當進行登入的動作時,僅需透過呼叫

AuthRepository().signin();

便可以進行登入的行為,而無需理會實際該函式當中如何進行處理、驗證或錯誤處理,也就是將這些處理的行為經過一層封裝。

面對新聞分類我們也可以這樣做!請於 repositories 資料夾下建立一個 news_category.dart 的檔案:

import 'package:micro_news_tutorial/models/news_category.dart';

class NewsCategoryRepository {
  // 藉由呼叫此函式,可以取得 NewsCategory 列表
  Future<List<NewsCategory>> getCategories() { ... }
}

串接 API

讓我們把他串起來吧~在 flutter 中有提供 http 的函式庫讓我們可以獲取外部數據,請先執行下列指令安裝

flutter pub add http

因為 api 回傳的為 json 格式,需要透過 convert 函式庫中的 jsonDecode 來進行格式的轉換。

Future<List<NewsCategory>> getCategories() async {
  // 透過 get 方法取得網址資訊,但由於僅能串接 Uri 物件 因此需透過 Uri 來 parse 網址
  final response = await http.get(Uri.parse('http://localhost:3000/categories'));
  // html status code 200 表示取得成功
  if (response.statusCode == 200) {
    final List<dynamic> categories = jsonDecode(response.body);
    // 看看我們到底拿到了些什麼
    print(categories);
    // 暫時先回傳空 List 否則函式會因為無回傳值而報錯
    return [];
  } else {
    throw Exception('Failed to load categories');
  }
}

接下來我們要來引用該函式,首先請先將 BrowseScreen 改為 stateful wiget ,因為我們預期透過呼叫 getCategories() 函式便能獲得新聞分類的類別並存起來。

class _BrowseScreenState extends State<BrowseScreen> {
  List<NewsCategory> categories = [];

  @override
  void initState() {
    super.initState();
    NewsCategoryRepository().getCategories().then(
      (value) {
        setState(() {
          categories = value;
        });
      },
    );
  }
  @override
  Widget build(BuildContext context) { ... }
}

回憶一下我們在前面篇章介紹過的 stateful widget 生命週期,會先執行 initState() 來設定 state 的初始狀態,接著才會 build 來建構畫面。所以在 initState() 呼叫 api 是最適合的。此時你的終端機若有成功印出內容表示 api 呼叫成功。

不過我們還少了一個步驟,也就是這些要將這些 json 格式資料一個個的轉換成 NewsCategory 型態的資料。請於 models/news_category.dart 加入以下程式碼:

class NewsCategory {
  // 變數與建構子省略
  factory NewsCategory.fromJson(Map<String, dynamic> json) {
    return NewsCategory(
        id: json['id'],
        name: json['name'],
        imageAssetUrl: json['imageAssetUrl']);
  }
}

factory 是一種特殊的建構子,其每次回傳都回傳一個 instance,適合用於根據不同條件返回不同對象的情境。因此我們藉由上方的函式將輸入的每個 json 物件都轉換成 NewsCategory 的格式。最終我們就可以將 repository 中回傳的內容改為

return categories.map((dynamic category) => NewsCategory.fromJson(category)).toList();

使用 .map 針對每個列表中的元素轉換型態,最終再用 toList 包裝成列表進行回傳。
我們用簡單的流程圖來表示:
https://ithelp.ithome.com.tw/upload/images/20231001/20135082eR04C3tlEg.png

呈現結果

現在資料都準備好了,格式也對了,只差顯示出結果了。請試著將 GridView.count 改寫成 GridView.builder 的形式。由於 GridView.builder 需提供 itemBuilder 參數的實作,可以參考以下的程式碼:

注意:GridView.count 內建整併了原先 gridDelegate 需提供的參數,但使用 .builder 建構子時需要額外改寫。練習看看吧

itemBuilder: (BuildContext context, int index) {
  // 定義每個子元素要如何顯示
  return Stack(alignment: Alignment.bottomLeft, children: [
    Container(
      decoration: BoxDecoration(
        color: CupertinoColors.black,
        image: DecorationImage(
          opacity: 0.7,
          image: NetworkImage(categories[index].cover),
          fit: BoxFit.cover,
        ),
        borderRadius: BorderRadius.circular(10),
      ),
    ),
    Padding(
      padding: const EdgeInsets.fromLTRB(8, 0, 0, 8),
      child: Text(categories[index].name,
          style: const TextStyle(
              fontSize: 20,
              color: CupertinoColors.white,
              fontWeight: FontWeight.w700)),
    )
  ]);

這裡面使用了 Stack widget 來將子元素重疊的顯示,並使用 NetworkImage 讀取網址的圖片。一起來看看成果:
https://ithelp.ithome.com.tw/upload/images/20231001/20135082l9xYCWdUeC.png
看起來很棒!!透過上述的介紹相信你已經學會怎麼來串接 API 拉~

練習 16-1

既然我們已經學會了怎麼將 API 回傳資料經過轉換變成自定義的資料型態,那麼剩下未完成的「新聞來源」部分就交由各位來自行練習拉~

新聞來源 API 接口 - http://localhost:3000/sources

請試著仿照新聞分類的步驟來建立新聞來源的 modelrepository ,並將顯示新聞來源的 ListView 改為 ListView.builder() 的建構方法。

加油💪

今日總結

今天我們從 API 的定義開始介紹,並告訴各位如何逐步的從 API 回傳格式轉換成我們要的結果。雖然步驟繁雜,不過藉著定義資料格式與封裝邏輯使的最終呼叫 API 動作變得相當簡單。

希望今天能讓各位讀者有所收穫😊

今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day16/micro_news_app


上一篇
[Day 15] 實戰新聞 APP - 滾動式widget (ListView 、GridView與 Sliver widget)
下一篇
[Day 17] 實戰新聞 APP - FutureBuilder 與 StreamBuilder
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言