今天我們將開始進行 API 串接,在開始講解之前請先開啟之前在 Day10 時建立的 api server。若你已將 server 部署至 vercel 上可以忽略此步驟。
// 請開啟之前下載的 db.json 的資料夾,並執行以下指令
npm run dev
此時於使用瀏覽器開啟 http://localhost:3000 或部署的網址,可以成功看到畫面則表示啟動成功。
在串接之前讓我們先來前導一下 API 是什麼。API 為 Application Programming Interface
的簡稱,允許不同的軟體間進行交互,以實現特定的功能或是服務。
在接下來與未來篇章中,當有使用到 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,並等待完成之後再進行畫面的渲染。
從上述的 API 可以知道我們可將獲得的資料使用 List
來存取,但尚缺少了一個專門用於表示新聞分類的型態。依我的習慣會建立一個 models
的資料夾,裡面專門用於存放自定義的資料型態。因此請於 lib
資料夾底下創建 models
資料夾,並同時建立一個 news_category.dart
的檔案於其中。
讓我們觀察每筆新聞分類的 record 結構:
因此我們可以在該檔案中如此定義:
class NewsCategory {
final int id;
final String name;
final String imageAssetUrl;
NewsCategory(
{required this.id, required this.name, required this.imageAssetUrl});
}
前幾天我們在 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() { ... }
}
讓我們把他串起來吧~在 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
包裝成列表進行回傳。
我們用簡單的流程圖來表示:
現在資料都準備好了,格式也對了,只差顯示出結果了。請試著將 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
讀取網址的圖片。一起來看看成果:
看起來很棒!!透過上述的介紹相信你已經學會怎麼來串接 API 拉~
既然我們已經學會了怎麼將 API 回傳資料經過轉換變成自定義的資料型態,那麼剩下未完成的「新聞來源」部分就交由各位來自行練習拉~
新聞來源 API 接口 - http://localhost:3000/sources
請試著仿照新聞分類的步驟來建立新聞來源的 model
與 repository
,並將顯示新聞來源的 ListView
改為 ListView.builder()
的建構方法。
加油💪
今天我們從 API 的定義開始介紹,並告訴各位如何逐步的從 API 回傳格式轉換成我們要的結果。雖然步驟繁雜,不過藉著定義資料格式與封裝邏輯使的最終呼叫 API 動作變得相當簡單。
希望今天能讓各位讀者有所收穫😊
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day16/micro_news_app