iT邦幫忙

2023 iThome 鐵人賽

DAY 18
1

寫到目前為止,我們一直都將一個畫面的內容寫在單一檔案中,也就是說頁面上看得到的 widget全都可以在 xxx_screen.dart 的程式碼中找到。

試想一下,當你的應用程式有個組件同時會出現在 A 畫面以及 B 畫面中,按照我們目前的寫法須分別於 A、B 兩檔案中都寫下一樣的程式碼。當這個組件很複雜,動輒 2、300 行以上時,就會導致在開發上很難維護,明明是代表一樣的東西,為什麼需要寫那麼多次呢?

沒錯這也就是要適時將畫面中組件拆分成 Custom Widget 的好處,除了增加該組件的可複用性,程式碼的可維護性也可以有效提升。

從今天的內容中,我們將實作如何拆分 Custom widget 。讓我們先來看看我們今天的目標:
https://ithelp.ithome.com.tw/upload/images/20231003/20135082DtQBYYALU7.png
我們將由上而下的建構畫面,開始拉!首先請先開啟 search_screen.dart

加入輸入框

此部分可以參考前面的實作方式,記得使用輸入框時需要使用到 TextEditingController ,因此要轉換成 stateful widget 喔!

熱門關鍵字 API

API 接口 - http://localhost:3000/hot_keywords
當呼叫此 API 時,將獲得當前熱門關鍵字的資料,以字串列表的格式回傳。

[
    "奧運",
    "2023 鐵人賽",
    "領袖",
    "世界衛生組織",
    "聯合國",
    "Flutter",
    "疫苗",
    "新冠肺炎"
]

因此按照前面章節介紹過的處理流程:

  1. 建立熱門關鍵字 model : 由於每個元素皆為 String 型態,因此同樣也建立一個用於表示熱門關鍵字資料格式的檔案。
  2. 建立熱門關鍵字 repository:將獲取資料的行為進行封裝
  3. 使用 FutureBuilder 搭配 ListView.builder 建構出可水平滾動關鍵字列表

我們這裡提供 ListView.builder 的範例程式碼:

// keywords 為熱門關鍵字列表
ListView.builder(
  scrollDirection: Axis.horizontal,
  padding: const EdgeInsets.fromLTRB(16, 16, 0, 16),
  itemCount: keywords.length,
  itemBuilder: (context, index) {
    return Row(children: [
      CupertinoButton(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        color: CupertinoColors.systemGrey6,
        borderRadius: BorderRadius.circular(8),
        onPressed: () {},
        child: Text(keywords[index].keyword,
            style: const TextStyle(color: CupertinoColors.black)),
      ),
      const SizedBox(width: 16),
    ]);
  },
);

拆分 Custom Widget

我們將上方程式碼中,明顯用於表示一個 widget 概念的部分抽取出來,就我的觀點會認為 CupertinoButton 元件的內容由於是用於表示單個關鍵字的按鈕,因此很適合將其拆分出來。因此請在 lib/widgets 底下建立一個新檔案 news_keyword_button.dart

由於按鈕本身並無需要紀錄 state 狀態,因此使用 stateless widget 來進行定義。接著 custom widget 資料需由外部傳入,因此我們需要新增建構子需傳入的參數,如下:

class NewsKeywordButton extends StatelessWidget {
  final String keyword;
  const NewsKeywordButton({super.key, required this.keyword})

  // 下方 build 的部分就省略,幾乎只是貼過來而已
}

如此 custom widget 就定義完成了!剩下只要修改呼叫的地方就大功告成拉。我們一樣請看 ListView.builder 的地方

return ListView.builder(
  scrollDirection: Axis.horizontal,
  padding: const EdgeInsets.fromLTRB(16, 16, 0, 16),
  itemCount: keywords.length,
  itemBuilder: (context, index) {
    return Row(children: [
      NewsKeywordButton(keyword: keywords[index].keyword),
      const SizedBox(width: 16),
    ]);
  },
);

是不是超簡單的!! 不僅讓原先的程式碼更簡短外,也更具有可讀性。
非常建議寫到這裡的各位先在此暫停一下,可以回頭去修改我們在「探索」頁面中的新聞分類按鈕與新聞來源按鈕,試著將其修改成 custom widget 。你會發現你的 browse_screen.dart 更簡短也更乾淨囉!

熱門新聞 API

API 接口 - http://localhost:3000/posts?_page=1&_limit=5
當呼叫此 API 時,將獲得取得 5 篇新聞資料,每篇新聞格式如下:

{
    "id": "新聞 id",
    "title": "新聞標題",
    "cover": "新聞封面圖片網址",
    "category": "新聞分類",
    "source": {
        "id": "新聞來源 id",
        "name": "新聞來源名稱",
        "icon": "..."
    },
    "body": "新聞內文"
}

接著也是需要經過同樣的處理流程:

  1. 建立新聞文章 model : 按照上述格式定義變數型態及相應欄位。值得注意的是 source 欄位與我們先前定義 NewsSource 相同,因此可直接宣告其為該型態
  2. 建立新聞 repository:將獲取熱門新聞封裝成 getHotNews 函式
  3. 使用 FutureBuilder 搭配 ListView.builder 建構出縱向的列表
    此部分建議各位可以試著按照畫面去思考要怎麼建構出單篇的新聞卡片,也可以參考文末的範例程式碼。

導向搜尋結果

既然是搜尋頁面,我們也必然會有一個頁面用於顯示搜尋的結果,因此請建立一個新檔案 result_screen.dart ,並參考下列程式碼:

class ResultScreen extends StatelessWidget {
  // 此參數用於紀錄導向該頁面時的搜尋字串
  final String query;
  const ResultScreen({super.key, required this.query});

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
        navigationBar: CupertinoNavigationBar(
          middle: Text(query,
              style: CupertinoTheme.of(context)
                  .textTheme
                  .navLargeTitleTextStyle
                  .copyWith(fontSize: 20)),
          previousPageTitle: '搜尋',
        ),
        child: const Center(child: Text('搜尋結果')));
  }
}

此時便可開始串接導向此頁面的流程。有兩個需要串接的地方

  1. 點擊熱門關鍵字:請開啟 news_keyword_button.dart 檔案中,該按鈕的 onPressed 並參考下列程式碼
onPressed: () {
  Navigator.of(context).push(
    CupertinoPageRoute(
      // 導向搜尋結果頁面,並因該類別定義須夾帶 query 表示搜尋的字串,因此將變數 keyword 作為參數傳遞
      builder: (context) => ResultScreen(query: keyword),
    ),
  );
},
  1. 於搜尋框輸入,並點擊送出:請找到 search_screen.dart 中的 onSubmitted 並參考下列程式碼
// onsubmit 為點擊送出後會觸發的事件
onSubmitted: (value) {
  Navigator.of(context).push(
    // 避免未輸入即按下送出的情形
    if (value == '') return;
    CupertinoPageRoute(
      builder: (context) => ResultScreen(query: value),
    ),
  );
},

讓我們來看看結果,如下圖:
https://i.imgur.com/cCqPV8L.gif

搜尋 API

API 接口 - http://localhost:3000/posts?q=欲搜尋的字串
將搜尋新聞 API 封裝成 getNews 函式,可參考下列處理方式

// 由於 query 為可變的內容,因此使其作為函式參數
Future<List<NewsPost>> getPosts(String query) async {
  try {
    // 根據不同 query 取得不同結果
    final response = await http.get(Uri.parse('http://localhost:3000/posts?q=$query'));
    if (response.statusCode == 200) {
      final List<dynamic> posts = jsonDecode(response.body);
      return posts.map((post) => NewsPost.fromJson(post)).toList();
    }
    throw Exception('取得失敗');
  } catch (e) {
    return Future.error('連線錯誤');
  }
}

完成此步驟後便可以結合 FutureBuilderListView.builder 再加上我們方才建立的 NewsPostCard custom widget 運用於搜尋結果的頁面上拉。

各位試著練習看看,最終結果會如下:
https://i.imgur.com/B22ilCi.gif

今日總結

今天我們將「熱門關鍵字按鈕」、「新聞分類按鈕」、「新聞來源按鈕」與「新聞文章卡片」四個元件改寫成 custom widget 。透過今天的內容,我們了解適時的將表示相同邏輯的程式碼進行拆分成 custom widget ,目的是為了保持程式碼的可維護性、可複用性以及可讀性,也移除了重複的程式碼。

明天我們會將應用程式的主頁完成,已經看得出應用程式的雛型了!讓我們繼續加油~

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


上一篇
[Day 17] 實戰新聞 APP - FutureBuilder 與 StreamBuilder
下一篇
[Day 19] 實戰新聞 APP - 無限捲軸
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言