跳至主要內容

建立 Flutter 版面配置

本教學說明如何在 Flutter 中設計和建立版面配置。

如果您使用提供的範例程式碼,您可以建立下列應用程式。

The finished app.
完成的應用程式。

照片由 Dino Reichmuth 拍攝,來自 Unsplash。文字出自 瑞士觀光局

若要更了解版面配置機制,請從 Flutter 的版面配置方法開始。

繪製版面配置圖

#

在本節中,請考量您希望應用程式使用者獲得什麼樣的使用者體驗。

考量如何定位您的使用者介面元件。版面配置是由這些定位的總體最終結果所組成。考慮規劃您的版面配置,以加快您的編碼速度。使用視覺提示來了解螢幕上物件的放置位置會很有幫助。

使用您偏好的任何方法,例如介面設計工具或鉛筆和紙張。在編寫程式碼之前,請找出您想要在螢幕上放置元素的位置。這就像是程式設計版本的格言:「三思而後行」。

  1. 提出這些問題,將版面配置分解為基本元素。

    • 您能識別出列和欄嗎?
    • 版面配置是否包含格線?
    • 是否有重疊的元素?
    • UI 是否需要索引標籤?
    • 您需要對齊、填補或加框哪些項目?
  2. 識別較大的元素。在此範例中,您將圖片、標題、按鈕和說明排列成一欄。

    Major elements in the layout: image, row, row, and text block
    版面配置中的主要元素:圖片、列、列和文字區塊
  3. 繪製每一列的圖表。

    1. 第 1 列,即標題區塊,有三個子項:一個文字欄、一個星形圖示和一個數字。其第一個子項,即欄,包含兩行文字。第一個欄可能需要更多空間。

      Title section with text blocks and an icon
      具有文字區塊和圖示的標題區塊
    2. 第 2 列,即按鈕區塊,有三個子項:每個子項都包含一個欄,其中接著包含一個圖示和文字。

      The Button section with three labeled buttons
      具有三個標籤按鈕的按鈕區塊

在繪製版面配置圖之後,請考量如何編寫程式碼。

您會在一個類別中編寫所有程式碼嗎?或者,您會為版面配置的每個部分建立一個類別嗎?

為了遵循 Flutter 的最佳實務,請建立一個類別或 Widget,以包含您版面配置的每個部分。當 Flutter 需要重新繪製 UI 的一部分時,它會更新變更的最小部分。這就是為什麼 Flutter 將「所有事物都視為 widget」的原因。如果只有 Text widget 中的文字變更,則 Flutter 只會重新繪製該文字。Flutter 會盡可能變更最少的 UI 以回應使用者輸入。

在本教學中,將您識別的每個元素撰寫為自己的 widget。

建立應用程式基本程式碼

#

在本節中,請先配置基本的 Flutter 應用程式程式碼,以啟動您的應用程式。

  1. 設定您的 Flutter 環境.

  2. 建立新的 Flutter 應用程式.

  3. 以以下程式碼取代 lib/main.dart 的內容。此應用程式使用應用程式標題和應用程式 appBar 上顯示的標題的參數。這個決定簡化了程式碼。

    dart
    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        const String appTitle = 'Flutter layout demo';
        return MaterialApp(
          title: appTitle,
          home: Scaffold(
            appBar: AppBar(
              title: const Text(appTitle),
            ),
            body: const Center(
              child: Text('Hello World'),
            ),
          ),
        );
      }
    }

加入標題區塊

#

在本節中,建立一個類似以下版面配置的 TitleSection widget。

The Title section as sketch and prototype UI
標題區塊的草圖和原型 UI

加入 TitleSection Widget

#

MyApp 類別之後加入下列程式碼。

dart
class TitleSection extends StatelessWidget {
  const TitleSection({
    super.key,
    required this.name,
    required this.location,
  });

  final String name;
  final String location;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Row(
        children: [
          Expanded(
            /*1*/
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                /*2*/
                Padding(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: Text(
                    name,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  location,
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          /*3*/
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          const Text('41'),
        ],
      ),
    );
  }
}

  1. 若要使用列中所有剩餘的可用空間,請使用 Expanded widget 來延伸 Column widget。若要將欄放置在列的開頭,請將 crossAxisAlignment 屬性設定為 CrossAxisAlignment.start
  2. 若要在文字列之間加入間距,請將這些列放在 Padding widget 中。
  3. 標題列以紅色星形圖示和文字 41 結束。整個列位於 Padding widget 內,並將每個邊緣填補 32 個像素。

將應用程式主體變更為可捲動的檢視

#

body 屬性中,以 SingleChildScrollView widget 取代 Center widget。在 SingleChildScrollView widget 內,以 Column widget 取代 Text widget。

dart
body: const Center(
  child: Text('Hello World'),
body: const SingleChildScrollView(
  child: Column(
    children: [

這些程式碼更新會以以下方式變更應用程式。

  • SingleChildScrollView widget 可以捲動。這允許顯示不符合目前螢幕的元素。
  • Column widget 會依列出的順序顯示其 children 屬性中的任何元素。children 清單中列出的第一個元素會顯示在清單頂端。children 清單中的元素會依照陣列順序從上到下顯示在螢幕上。

更新應用程式以顯示標題區塊

#

TitleSection widget 加入為 children 清單中的第一個元素。這會將其放置在螢幕頂端。將提供的名稱和位置傳遞至 TitleSection 建構函式。

dart
children: [
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
],

加入按鈕區塊

#

在本節中,加入將會為您的應用程式新增功能的按鈕。

按鈕區塊包含三個使用相同版面配置的欄:一個圖示在文字列上方。

The Button section as sketch and prototype UI
按鈕區塊的草圖和原型 UI

規劃將這些欄分散在一列中,讓每一欄都佔用相同的空間。使用主要色彩繪製所有文字和圖示。

加入 ButtonSection widget

#

TitleSection widget 之後加入下列程式碼,以包含建立按鈕列的程式碼。

dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    // ···
  }
}

建立一個用於製作按鈕的 widget

#

由於每個欄的程式碼可以使用相同的語法,因此請建立一個名為 ButtonWithText 的 widget。widget 的建構函式接受按鈕的色彩、圖示資料和標籤。使用這些值,widget 會建置一個以 Icon 和樣式化的 Text widget 作為其子項的 Column。為了幫助區隔這些子項,Padding widget 會使用 Padding widget 包裝 Text widget。

ButtonSection 類別之後加入下列程式碼。

dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});
  // ···
}

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }

使用 Row widget 定位按鈕

#

將下列程式碼加入到 ButtonSection widget 中。

  1. 為每個按鈕加入三個 ButtonWithText widget 執行個體。
  2. 傳遞該特定按鈕的色彩、Icon 和文字。
  3. 使用 MainAxisAlignment.spaceEvenly 值對齊主軸的欄。Row widget 的主軸是水平的,而 Column widget 的主軸是垂直的。然後,此值會告知 Flutter 在沿著 Row 的每個欄之前、之間和之後以相等數量排列可用空間。
dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    return SizedBox(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ButtonWithText(
            color: color,
            icon: Icons.call,
            label: 'CALL',
          ),
          ButtonWithText(
            color: color,
            icon: Icons.near_me,
            label: 'ROUTE',
          ),
          ButtonWithText(
            color: color,
            icon: Icons.share,
            label: 'SHARE',
          ),
        ],
      ),
    );
  }
}

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      // ···
    );
  }
}

更新應用程式以顯示按鈕區塊

#

將按鈕區塊加入到 children 清單。

dart
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
  ButtonSection(),
],

加入文字區塊

#

在本節中,將文字說明加入到此應用程式。

The text block as sketch and prototype UI
文字區塊的草圖和原型 UI

加入 TextSection widget

#

ButtonSection widget 之後,以單獨的 widget 加入下列程式碼。

dart
class TextSection extends StatelessWidget {
  const TextSection({
    super.key,
    required this.description,
  });

  final String description;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Text(
        description,
        softWrap: true,
      ),
    );
  }
}

藉由將 softWrap 設定為 true,文字行會在文字換行邊界之前填滿欄寬。

更新應用程式以顯示文字區塊

#

將新的 TextSection widget 作為 ButtonSection 之後的子項加入。當加入 TextSection widget 時,請將其 description 屬性設定為位置說明的文字。

dart
    location: 'Kandersteg, Switzerland',
  ),
  ButtonSection(),
  TextSection(
    description:
        'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
        'Bernese Alps. Situated 1,578 meters above sea level, it '
        'is one of the larger Alpine Lakes. A gondola ride from '
        'Kandersteg, followed by a half-hour walk through pastures '
        'and pine forest, leads you to the lake, which warms to 20 '
        'degrees Celsius in the summer. Activities enjoyed here '
        'include rowing, and riding the summer toboggan run.',
  ),
],

加入圖片區塊

#

在本節中,加入影像檔案以完成您的版面配置。

設定您的應用程式以使用提供的圖片

#

若要設定您的應用程式以參考影像,請修改其 pubspec.yaml 檔案。

  1. 在專案頂端建立 images 目錄。

  2. 下載 lake.jpg 影像,並將其加入到新的 images 目錄。

  3. 若要包含影像,請在應用程式的根目錄的 pubspec.yaml 檔案中加入 assets 標籤。當您加入 assets 時,它會作為程式碼可使用的影像指標集。

    pubspec.yaml
    yaml
    flutter:
      uses-material-design: true
      assets:
        - images/lake.jpg

建立 ImageSection 小工具

#

在其他宣告之後,定義以下 ImageSection 小工具。

dart
class ImageSection extends StatelessWidget {
  const ImageSection({super.key, required this.image});

  final String image;

  @override
  Widget build(BuildContext context) {
    return Image.asset(
      image,
      width: 600,
      height: 240,
      fit: BoxFit.cover,
    );
  }
}

BoxFit.cover 值會告訴 Flutter 以兩個約束條件顯示圖片。首先,盡可能以最小尺寸顯示圖片。其次,覆蓋配置所分配的所有空間,稱為渲染框。

更新應用程式以顯示圖片區塊

#

children 列表中新增一個 ImageSection 小工具作為第一個子元素。將 image 屬性設定為您在 設定您的應用程式以使用提供的圖片 中新增的圖片路徑。

dart
children: [
  ImageSection(
    image: 'images/lake.jpg',
  ),
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',

恭喜

#

就是這樣!當您熱重載應用程式時,您的應用程式應該看起來像這樣。

The finished app
完成的應用程式

資源

#

您可以從以下位置存取本教學中使用的資源

Dart 程式碼: main.dart
圖片: ch-photo
Pubspec: pubspec.yaml

下一步

#

若要為此佈局新增互動性,請依照 互動性教學