本教程解釋瞭如何在 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 的最佳實踐,請建立一個類或元件來包含佈局的每個部分。當 Flutter 需要重新渲染 UI 的一部分時,它會更新發生變化的最小部分。這就是 Flutter 將“一切皆元件”的原因。如果只有 `Text` 元件中的文字發生變化,Flutter 只會重新繪製該文字。Flutter 會根據使用者輸入儘可能少地更改 UI。

對於本教程,將您已識別的每個元素編寫為自己的元件。

建立應用程式基礎程式碼

#

在本節中,構建基本的 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` 元件。

The Title section as sketch and prototype UI
作為草圖和原型 UI 的標題部分

新增 `TitleSection` 元件

#

在 `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` 元件來拉伸 `Column` 元件。為了將列放置在行的開頭,將 `crossAxisAlignment` 屬性設定為 `CrossAxisAlignment.start`。
  2. 為了在文字行之間新增空間,將這些行放入 `Padding` 元件中。
  3. 標題行以一個紅色星形圖示和文字 `41` 結束。整個行位於一個 `Padding` 元件內,並對每個邊緣填充 32 畫素。

將應用主體更改為可滾動檢視

#

在 `body` 屬性中,將 `Center` 元件替換為 `SingleChildScrollView` 元件。在 `SingleChildScrollView` 元件內,將 `Text` 元件替換為 `Column` 元件。

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

這些程式碼更新以以下方式更改了應用程式。

  • `SingleChildScrollView` 元件可以滾動。這允許顯示不適合當前螢幕的元素。
  • `Column` 元件按照列出的順序顯示其 `children` 屬性中的任何元素。 `children` 列表中列出的第一個元素顯示在列表的頂部。 `children` 列表中的元素在螢幕上按陣列順序從上到下顯示。

更新應用程式以顯示標題部分

#

將 `TitleSection` 元件作為 `children` 列表中的第一個元素新增。這會將其放置在螢幕頂部。將提供的名稱和位置傳遞給 `TitleSection` 建構函式。

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

新增按鈕部分

#

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

**按鈕**部分包含三個使用相同佈局的列:圖示位於一行文字上方。

The Button section as sketch and prototype UI
作為草圖和原型 UI 的按鈕部分

計劃將這些列分佈在一行中,以便每個列佔用相同的空間量。用主色繪製所有文字和圖示。

新增 `ButtonSection` 元件

#

在 `TitleSection` 元件之後新增以下程式碼,以包含構建按鈕行的程式碼。

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

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

}

建立用於生成按鈕的元件

#

由於每個列的程式碼可以使用相同的語法,因此建立一個名為 `ButtonWithText` 的元件。該元件的建構函式接受顏色、圖示資料和按鈕的標籤。使用這些值,該元件構建一個 `Column`,其中包含一個 `Icon` 和一個樣式化的 `Text` 元件作為其子項。為了幫助分隔這些子項,`Text` 元件被一個 `Padding` 元件包裹。

在 `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` 元件定位按鈕

#

將以下程式碼新增到 `ButtonSection` 元件中。

  1. 新增三個 `ButtonWithText` 元件例項,每個按鈕一個。
  2. 傳遞該特定按鈕的顏色、`Icon` 和文字。
  3. 使用 `MainAxisAlignment.spaceEvenly` 值沿主軸對齊列。 `Row` 元件的主軸是水平的,`Column` 元件的主軸是垂直的。因此,此值告訴 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` 元件

#

在 `ButtonSection` 元件之後,將以下程式碼新增為一個單獨的元件。

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`,文字行在單詞邊界處換行之前會填滿列寬。

更新應用程式以顯示文字部分

#

在 `ButtonSection` 之後新增一個新的 `TextSection` 元件作為子元件。新增 `TextSection` 元件時,將其 `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 以兩個約束條件顯示影像。首先,儘可能小地顯示影像。其次,覆蓋佈局分配的所有空間,稱為渲染框。

更新應用程式以顯示影像部分

#

將 `ImageSection` 元件作為 `children` 列表中的第一個子元件新增。將 `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`

下一步

#

要為此佈局新增互動性,請遵循 互動性教程