Flutter 入門與實戰(五):來一個圖文並茂的列表

島上碼農發表於2021-05-22

列表在 App 中是最常見的形式了,在 Flutter 中提供了 ListView 這個元件來實現列表,本篇將通過 ListView 實現一個圖文並茂的列表。

實現效果

介面佈局分析

本篇要實現的列表如上圖所示。我們拿到介面設計稿之後,在 UI 開發工作第一件事就是考慮介面的元素和佈局。以上面的介面為例,實際的介面元素包括了列表和列表元素,而列表元素是關鍵,列表元素包括了左邊的一張圖片,圖片右側的標題和檢視次數(帶前置圖示)。列表的元素的佈局如下圖所示。 image.png 縱向上,列表元素的佈局高度由圖片決定。圖示和瀏覽數的高度固定,剩餘的空間由標題佔據。考慮介面的美觀,標題最大行數為2行,超出部分使用...替代。 橫向上,為保持圖片的固定長寬比,圖片寬度固定。寬度在圖片固定後,剩餘的空間(除了間距留白外)即標題的空間。 基於上述的描述,可以得到大致的佈局:

  • 整個列表元素使用一個 Container 包裹,以便設定四周的外邊距;
  • 橫向上,使用 Row 元件完成橫向佈局。
  • 右側區域使用 Column 元件完成縱向佈局。
  • 右側的瀏覽數的圖示和文字也需要使用 Row 完成橫向佈局。
  • 為保持左側圖片和右側區域的間距,右側統一使用 Container 在外面包裹,以便控制間距。

ListView 簡介

ListView 用於生成列表,,通常使用 builder靜態方法構建一個列表,其中關鍵的引數為:

  • itemCount:列表元素的數量。
  • itemBuilder:列表元素構建函式,通過指定元素的下標返回對應的列表元素元件。

如果要使用分隔元件的列表,也可以使用 ListView.seperate 方法構建列表,這個方法多了一個 seperateBuilder 引數,用於返回列表元素間對應的分隔元件。

因此,列表的關鍵是列表元素元件的實現。

編碼實現

我們還是基於上一個工程,在 dynamic.dart 中實現列表。在原始碼目錄新增兩個檔案,分別是 dynamic_item.dart 用於構建列表元素,dynamic_mock_data .dart用於模擬後臺介面資料。其中 dynamic_mock_data 的資料比較簡單,用一個list 靜態方法返回分頁資料,如下所示:

class DynamicMockData {
  static List<Map<String, Object>> list(int page, int size) {
    return List<Map<String, Object>>.generate(size, (index) {
      return {
        'title': '標題${index + (page - 1) * size + 1}:這是一個列表標題,最多兩行,多處部分將會被擷取',
        'imageUrl':
            'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3331308357,177638268&fm=26&gp=0.jpg',
        'viewCount': 180,
      };
    });
  }
}
複製程式碼

其中 page 和 size 用於模擬分頁情況,方便後續實現上拉和下拉重新整理。 注意這裡使用了 List 的 generate 方法來構建陣列,該方法用於構建指定大小的陣列, 可以通過帶index輸入的回撥函式構建對飲 index 下標的陣列元素。

dynamic_item.dart的實現程式碼如下所示:

import 'package:flutter/material.dart';

class DynamicItem extends StatelessWidget {
  final String title;
  final String imageUrl;
  final int viewCount;
  static const double ITEM_HEIGHT = 100;
  static const double TITLE_HEIGHT = 80;
  static const double MARGIN_SIZE = 10;
  const DynamicItem(this.title, this.imageUrl, this.viewCount, {Key key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(MARGIN_SIZE),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _imageWrapper(this.imageUrl),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _titleWrapper(context, this.title),
                _viewCountWrapper(this.viewCount.toString()),
              ],
            ),
          )
        ],
      ),
    );
  }

  Widget _titleWrapper(BuildContext context, String text) {
    return Container(
      height: TITLE_HEIGHT,
      margin: EdgeInsets.fromLTRB(MARGIN_SIZE, 0, 0, 0),
      child: Text(
        this.title,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        style: Theme.of(context).textTheme.headline6,
      ),
    );
  }

  Widget _viewCountWrapper(String text) {
    return Container(
      margin: EdgeInsets.fromLTRB(MARGIN_SIZE, 0, 0, 0),
      height: ITEM_HEIGHT - TITLE_HEIGHT,
      child: Row(children: [
        Icon(
          Icons.remove_red_eye_outlined,
          size: 14.0,
          color: Colors.grey,
        ),
        SizedBox(width: 5),
        Text(
          this.viewCount.toString(),
          style: TextStyle(color: Colors.grey, fontSize: 14.0),
        ),
      ]),
    );
  }

  Widget _imageWrapper(String imageUrl) {
    return SizedBox(
      width: 150,
      height: ITEM_HEIGHT,
      child: Image.network(imageUrl),
    );
  }
}

複製程式碼

首先定義了title、imageUrl和 viewCount 幾個final 型別的成員屬性,使用 final 的約束是方便外部其他類可以直接訪問,但不可以做修改。如果這些屬性本身外部不可訪問,也可以定義為私有成員。

其次是使用建構函式直接完成成員屬性的初始化,這是 Dart 語言的一種簡寫方法,只要傳參次序一致就可以不需要函式體自動完成成員的初始化,通常會用在 final 修飾的成員屬性。

在 build 方法中完成了整個介面的構建。其中注意這裡使用了 Expanded 包裹右側區域,Expanded元件是表示橫向佈局中,該元件將自動佔據剩餘的空間。如果沒有這個包裹,會導致右側內容過寬時無法顯示完全而報警。

圖片、標題和瀏覽數均通過單獨的構建元件方法獲取,這一方面是降低UI巢狀層級,另一方面如果日後有同樣的元件,則可以抽離單獨的構建方法提高複用性。下面對關鍵的幾個設定進行解讀:

  • crossAxisAlignment: CrossAxisAlignment.start:這個用於標記Row行佈局元件的元素在列方向上從起始位置開始對齊(即縱向上右側和圖片上沿對齊)。
  • Container 的 margin:用於設定距離上下左右的間距,如果四個方向間距一致,就可以使用 EdgeInsets.all 方法設定。如果不一致就是要 EdgeInsets.fromLTRB 單獨設定四個方向的間距。
  • 在瀏覽陣列件中使用了一個 SizedBox 元件,這個元件本身沒什麼內容,僅僅是為了將圖示和瀏覽數字之間拉開一定的間距,提高美觀度。

用到的元件

除了 ListView 之外,本篇涉及到的元件如下:

  • Text:文字元件,相當於是 label。除了文字內容外,可以使用 style 設定文字樣式。這裡標題使用了 maxLines 約束標題最大2行,使用了 overflow 設定了文字溢位後處理方式。
  • Image:圖片元件,這裡使用了 Image.network 從網路載入圖片,這個 Image.network 是很初級的用法,後續會使用 cached_image_network 外掛替換。
  • Icon:圖示元件,在 Flutter 中內建了很多字型圖示,對於大部分場景都能夠滿足,圖示可以使用 Icons 類定義的圖示名稱來找到想要的圖示。
  • Row:行佈局元件,其子元件 children 都是按先後順序從左到右在同一行依次排列。
  • Column:列布局元件,其子元件 children 都是按從先後順序從上到下在同一列依次排列。

以上元件在本篇示例中都是基本應用,更多設定可以在 IDE 中檢視原始碼或閱讀官方文件瞭解。

結語:本篇講述了使用 ListView 完成列表的構建,重點講述了列表元素如何佈局,具體的佈局元件和實現方法。介面實現的關鍵工作實際是佈局子元素的拆分。剩下的實現方式存在多種,看各人喜好。但是,需要注意避免過多巢狀導致程式碼可讀性降低,以及提高 UI 元件的可複用性。

相關文章