iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
Mobile Development

Flutter 從零到實戰 - 30 天の學習筆記系列 第 21

[Day 21] 實戰新聞 APP - 認識 CupertinoListSection 與 CupertinoListTile

  • 分享至 

  • xImage
  •  

我們今天要來實作個人檔案頁面。在實作之前,我們先來看看在 iOS 中的設定畫面風格。
https://ithelp.ithome.com.tw/upload/images/20231006/20135082UoEOM1PeoQ.png
我們觀察到:

  1. 背景顏色並不是純白色,而是很淺的灰色。在 Cupertino 庫中的對應顏色屬性為 CupertinoColors.systemGroupedBackground
  2. 設定頁面按照取向的不同做成了一個一個的群組,如「一般」、「輔助使用」、「隱私權與安全性」因屬性與手機內部設定有關,因此像是群組一樣的擺放在一起顯示。對應到的則是 CupertinoListSection
  3. 設定列表中每個項目樣式皆為 開頭圖標、中間顯示項目內容,最後才是導向頁面的圖標。對應到 CupertinoListTile

因此藉由本章介紹的小組件,我們便能打造出相同顯示風格的個人檔案頁面拉。今天的目標如下:
https://ithelp.ithome.com.tw/upload/images/20231006/2013508212W62TVLaC.png

讓我們開始吧!

ListTile

此部件我們在 Day17 的文章中就已經介紹過。ListTile 是可用於填充 ListView 的 widget,可容納一至三行的文字,並前置與後置 widget 搭配使用,在 Cupertino 中的則為 CupertinoListTile 。提供了兩種建構方式,不過差異並不大:

  1. CupertinoListTile - 基本款用於顯示列表項目
  2. CupertinolistTile.notched - 僅增加列表項目與邊框間距,更貼近 iOS 應用中「筆記」與「提醒事項」中列表項目的顯示風格

CupertinoListSection

CupertinoListSection 用於顯示群組的列表,通常搭配 CupertinoListTile 一起使用,可設定 header 與 footer 針對群組項目進行標示。提供了兩種建構方式:

  1. CupertinoListSection - 用於建構從邊緣至邊緣 (edge-to-edge) 的列表風格,如下:
    https://ithelp.ithome.com.tw/upload/images/20231006/20135082GsYfzA5aJd.png
  2. CupertinoListSection.insetGrouped - 建構的項目會使用圓角包裹,距離左右兩側也有預設 padding,如下:
    https://ithelp.ithome.com.tw/upload/images/20231006/201350822dOBVdYbEe.png

我們將使用第 2 種來建構我們的畫面。

首先請先開啟 profile_screen.dart 檔案,並請參考下面程式碼:

@override
Widget build(BuildContext context) {
  return const CupertinoPageScaffold(
    // 設為與設定頁面相同顏色
    backgroundColor: CupertinoColors.systemGroupedBackground,
    child: CustomScrollView(slivers: [
      CupertinoSliverNavigationBar(
        largeTitle: Text(
          '個人資料',
        ),
        // 一樣改成與設定頁面相同顏色
        backgroundColor: CupertinoColors.systemGroupedBackground,
        // 關閉原先頂端導覽列底部的邊框
        border: null, 
      ),
      SliverToBoxAdapter(child: Text('Profile Screen'))
    ]));
}

Profile Card

我們要先製作用於顯示個人資訊的 profile card,請於 widgets 資料夾底下建立 profile_card.dart ,並請參考下方程式碼:

Widget build(BuildContext context) {
  return CupertinoListSection.insetGrouped(
    children: [
      CupertinoListTile(
          title: const Text('使用者名稱',
              style: TextStyle(fontSize: 20, height: 1.2)),
          subtitle: const Text('個人簽名'),
          padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
          leadingSize: 60,
          leading: Container(
            width: 50,
            height: 50,
            decoration: const BoxDecoration(
                shape: BoxShape.circle, color: CupertinoColors.systemGrey4),
          ),
          trailing: const Icon(CupertinoIcons.right_chevron,
              size: 20, color: CupertinoColors.systemGrey),
          onTap: () async {}),
    ],
  );
}

此部分我們皆尚未與 firbase 提供的 user 資訊進行串接,因此使用者名稱、個人簽名內容與頭貼皆先暫時寫死資料,之後會再回來介紹此部分較正統的作法。

CupertinoSwitch

現在要來製作顯示「夜間模式」、「每日一報」的群組列表,從設計稿可以看到兩個項目末端皆提供一個開關可進行切換。這部分是使用 CupertinoSwitch 組件來實現的,讓我們先來看看其類別定義:

const CupertinoSwitch({
  super.key,
  required this.value,      // 為 bool 型態,表示當前狀態
  required this.onChanged,  // 當狀態改變時觸發的事件
  this.activeColor,
  this.trackColor,
  this.thumbColor,
  this.applyTheme,
  this.focusColor,
  this.dragStartBehavior = DragStartBehavior.start,
});

由於需要維護狀態來分別紀錄代表夜間模式與每日一報的開關狀態,因此首先須先將 ProfileScreen 更改為 stateful widget,並分別宣告兩個狀態的變數。舉其中一個為例:

// 表示夜間模式 switch 的狀態
bool _isDarkMode = false;

// 夜間模式 list tile 範例
CupertinoListTile.notched(
    title: const Text('夜間模式'),
    // 開頭的顯示圖標
    leading: DecoratedBox(
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(6),
            color: CupertinoColors.systemIndigo),
        child: const Padding(
          padding: EdgeInsets.all(4),
          child: Icon(CupertinoIcons.moon_fill,
              color: CupertinoColors.white, size: 20),
        )),
    // 尾端的 switch
    trailing: CupertinoSwitch(
        value: _isDarkMode,        // 初始化當前狀態
        onChanged: (bool? value) { // 當狀態發生改變時,牽動改變 switch 狀態
          setState(() {
            _isDarkMode = value!;
          });
})),

剩下的「每日一報」的 ListTile 就交由各位自行練習創建拉,參考程式碼會置於文末。

完成所有的導向頁面

目前為止我們的四個分頁都已經有畫面拉,不過我們一直還沒有將探索頁面中的「新聞分類」與「新聞來源」按鈕導向至正確的搜尋結果頁面,現在是時候把它完成了!

依新聞分類或新聞來源搜尋 API

API 接口 - http://localhost:3000/posts?category=[新聞分類]&source.name=[新聞來源]

請打開 NewsPostRepository 檔案,我們可以直接針對 getPosts 進行改寫。

Future<List<NewsPost>> getPosts(
      {String query = '',
      String source = '',
      String category = '',
      int page = 1,
      int limit = 20}) async {
  try {
    String url = 'http://localhost:3000/posts?q=$query&_page=$page&_limit=$limit';
    // 若新聞來源不為空字串時則加入 querystring
    if (source.isNotEmpty) {
      url += '&source.name=$source';
    }
    if (category.isNotEmpty) {
      url += '&category=$category';
    }
    final response = await http.get(Uri.parse(url));
    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('連線錯誤');
  }
}

接下來,由於搜尋結果頁面目前僅支援搜尋字串功能。我們可以再支援搜尋新聞分類或新聞來源功能,雖然當然可以額外寫頁面來達成效果,但我個人認為兩者做的事情都是搜尋,因此毋需額外再新增頁面。

請開啟 result_screen.dart 檔案,我們需要進行些許的修改好讓其可以兼容其他兩個的搜尋。請參考下列程式碼

class ResultScreen extends StatefulWidget {
  // 原先 query 為 required,但現在可能傳入 query / source / category 其中一種
  final String? query;
  final String? source;
  final String? category;
  const ResultScreen({super.key, this.query, this.source, this.category});

  @override
  State<ResultScreen> createState() => _ResultScreenState();
}

接下來在 initState 根據傳入參數為何,給定不同的搜尋結果

@override
void initState() {
  super.initState();
  // 若傳入 source 不為 null,則強制解封 source ! 使用文字下去搜尋
  // 其餘依此類推
  if (widget.source != null) {
    _posts = NewsPostRepository().getPosts(source: widget.source!);
  } else if (widget.category != null) {
    _posts = NewsPostRepository().getPosts(category: widget.category!);
  } else {
    _posts = NewsPostRepository().getPosts(query: widget.query!);
  }
}

最後在建構畫面時,頂端列的顯示文字也使用同樣的處理方式。若是以關鍵字搜尋時,頂端列標題顯示關鍵字名稱;以新聞分類下去搜尋時則就顯示新聞分類的名字。因此可將 CupertinoNavigationBarmiddle 參數改為承接以下函式:

Widget getNavigationBarTitle() {
  var style = CupertinoTheme.of(context)
      .textTheme
      .navLargeTitleTextStyle
      .copyWith(fontSize: 20);
  // 若搜尋字串不為空則顯示搜尋字串,其餘依此類推
  if (widget.query != null) {
    return Text(widget.query!, style: style);
  } else if (widget.category != null) {
    return Text(widget.category!, style: style);
  } else {
    // 當然你也可以弄的好看一點加上 source icon,教給各位讀者們發揮囉
    return Text(widget.source!, style: style);
  }
}

最終結果如下圖:
https://ithelp.ithome.com.tw/upload/images/20231006/20135082n9abIv8bx7.png

今日總結

今天我們使用了 CupertinoListSection 搭配 CupertinoListTile 打造了類似 iOS 設定 APP 的顯示風格的個人檔案頁面,除此之外也將其餘導向頁面的功能一併完成了。

從在個人檔案頁面中,我們看到了兩個應用程式功能中未完成的項目,夜間模式與每日一報的通知。我們將會在接下來的篇章中一一的提及~

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


上一篇
[Day20] 實戰新聞 APP - 使用彈出式視窗來顯示新聞吧 (CupertinoPopupSurface)
下一篇
[Day 22] 實戰新聞 APP - 使用 State Management 來觸發 Dark Mode
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言