寫到目前為止,我們一直都將一個畫面的內容寫在單一檔案中,也就是說頁面上看得到的 widget全都可以在 xxx_screen.dart
的程式碼中找到。
試想一下,當你的應用程式有個組件同時會出現在 A 畫面以及 B 畫面中,按照我們目前的寫法須分別於 A、B 兩檔案中都寫下一樣的程式碼。當這個組件很複雜,動輒 2、300 行以上時,就會導致在開發上很難維護,明明是代表一樣的東西,為什麼需要寫那麼多次呢?
沒錯這也就是要適時將畫面中組件拆分成 Custom Widget 的好處,除了增加該組件的可複用性,程式碼的可維護性也可以有效提升。
從今天的內容中,我們將實作如何拆分 Custom widget 。讓我們先來看看我們今天的目標:
我們將由上而下的建構畫面,開始拉!首先請先開啟 search_screen.dart
此部分可以參考前面的實作方式,記得使用輸入框時需要使用到 TextEditingController
,因此要轉換成 stateful widget 喔!
API 接口 - http://localhost:3000/hot_keywords
當呼叫此 API 時,將獲得當前熱門關鍵字的資料,以字串列表的格式回傳。
[
"奧運",
"2023 鐵人賽",
"領袖",
"世界衛生組織",
"聯合國",
"Flutter",
"疫苗",
"新冠肺炎"
]
因此按照前面章節介紹過的處理流程:
String
型態,因此同樣也建立一個用於表示熱門關鍵字資料格式的檔案。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),
]);
},
);
我們將上方程式碼中,明顯用於表示一個 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 接口 - http://localhost:3000/posts?_page=1&_limit=5
當呼叫此 API 時,將獲得取得 5 篇新聞資料,每篇新聞格式如下:
{
"id": "新聞 id",
"title": "新聞標題",
"cover": "新聞封面圖片網址",
"category": "新聞分類",
"source": {
"id": "新聞來源 id",
"name": "新聞來源名稱",
"icon": "..."
},
"body": "新聞內文"
}
接著也是需要經過同樣的處理流程:
source
欄位與我們先前定義 NewsSource
相同,因此可直接宣告其為該型態getHotNews
函式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('搜尋結果')));
}
}
此時便可開始串接導向此頁面的流程。有兩個需要串接的地方
news_keyword_button.dart
檔案中,該按鈕的 onPressed
並參考下列程式碼onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute(
// 導向搜尋結果頁面,並因該類別定義須夾帶 query 表示搜尋的字串,因此將變數 keyword 作為參數傳遞
builder: (context) => ResultScreen(query: keyword),
),
);
},
search_screen.dart
中的 onSubmitted
並參考下列程式碼// onsubmit 為點擊送出後會觸發的事件
onSubmitted: (value) {
Navigator.of(context).push(
// 避免未輸入即按下送出的情形
if (value == '') return;
CupertinoPageRoute(
builder: (context) => ResultScreen(query: value),
),
);
},
讓我們來看看結果,如下圖:
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('連線錯誤');
}
}
完成此步驟後便可以結合 FutureBuilder
與 ListView.builder
再加上我們方才建立的 NewsPostCard
custom widget 運用於搜尋結果的頁面上拉。
各位試著練習看看,最終結果會如下:
今天我們將「熱門關鍵字按鈕」、「新聞分類按鈕」、「新聞來源按鈕」與「新聞文章卡片」四個元件改寫成 custom widget 。透過今天的內容,我們了解適時的將表示相同邏輯的程式碼進行拆分成 custom widget ,目的是為了保持程式碼的可維護性、可複用性以及可讀性,也移除了重複的程式碼。
明天我們會將應用程式的主頁完成,已經看得出應用程式的雛型了!讓我們繼續加油~
今天的參考程式碼:https://github.com/ChungHanLin/micro_news_tutorial/tree/day18/micro_news_app