Flutter 應用程式佈局的最佳實踐

杭州程式設計師張張發表於2022-03-20

Flutter 中幾乎所有的東西都是一個小部件,當你編寫小部件時,你可以構建一個佈局。例如,您可以在列小部件中新增多個小部件以建立垂直佈局。隨著您繼續新增更多小部件,您的 Flutter 應用程式佈局將變得越複雜。

在本文中,我將介紹一些在佈局 Flutter 應用程式時要實施的最佳實踐。

在 Flutter 中使用 SizedBox 代替 Container

有許多使用情況下,你需要使用佔位符。讓我們看一下下面的例子:

return _isLoaded ? Container() : YourAwesomeWidget();

Container 是一個很棒的小部件,您將在 Flutter 中廣泛使用它。 Container() 擴充套件以適應父級提供的約束,並且不是 const 建構函式。

另一方面,SizedBox 是一個建構函式,建立一個固定尺寸的盒子。寬度和高度引數可以為空,以表示盒子的尺寸不應受到相應維度的限制。

因此,當我們實現佔位符時,應該使用 SizedBox 而不是 Container

return _isLoaded ? SizedBox() : YourAwesomeWidget();

使用 if 條件而不是三元運算子語法

在佈局 Flutter 應用程式時,通常需要有條件地渲染不同的小部件。您可能需要根據平臺生成一個小部件,例如:

Row(
  children: [
    Text("Majid"),
    Platform.isAndroid ? Text("Android") : SizeBox(),
    Platform.isIOS ? Text("iOS") : SizeBox(),
  ]
);

在這種情況下,您可以刪除三元運算子並利用 Dart 的內建語法在陣列中新增 if 語句。

Row(
  children: [
    Text("Majid"),
    if (Platform.isAndroid) Text("Android"),
    if (Platform.isIOS) Text("iOS"),
  ]
);

您還可以使用擴充套件運算子擴充套件此功能,並根據需要載入多個小部件。

Row(
  children: [
    Text("Majid"),
    if (Platform.isAndroid) Text("Android"),
    if (Platform.isIOS) ...[
      Text("iOS_1")
      Text("iOS_2")
    ],
  ]
);

考慮 Flutter 中 build() 方法的成本

Flutter 小部件中的 build 方法可能會在祖先小部件重建小部件時被頻繁呼叫。避免在 build() 方法中重複和昂貴的工作很重要。

例如,當您使用方法而不是在應用程式中建立小部件時。讓我詳細說明:

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _buildHeaderWidget(),
          _buildBodyWidget(context),
          _buildFooterWidget(),
        ],
      ),
    );
  }

  Widget _buildHeaderWidget() {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }

  Widget _buildBodyWidget(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }

  Widget _buildFooterWidget() {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

這種方法的缺點是,當 MyAwesomeWidget 需要再次重建時--這可能會經常發生--所有在方法中建立的widget也會被重建,導致浪費CPU週期,也可能浪費記憶體。

因此,最好通過以下方式將這些方法轉換為 StatelessWidgets

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          HeaderWidget(),
          BodyWidget(),
          FooterWidget(),
        ],
      ),
    );
  }
}

class HeaderWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }
}

class BodyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }
}

class FooterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

所有 StatefulWidgetsStatelessWidgets,基於 key、widget 型別和屬性,都有一個特殊的快取機制,只在必要時重建。我們甚至可以通過新增 const 來優化這些小部件,這將導致我們進入本文的下一部分。

儘可能使用 const 小部件

在Dart中,儘可能使用 const 建構函式是很好的做法,記住編譯器會優化你的程式碼。現在,讓我們回顧一下上面的例子。通過一個直接的步驟,我們可以使 build 方法更有效地工作。

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const HeaderWidget(),
          const BodyWidget(),
          const FooterWidget(),
        ],
      ),
    );
  }
}

class HeaderWidget extends StatelessWidget {
  const HeaderWidget();
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }
}

class BodyWidget extends StatelessWidget {
  const BodyWidget();
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }
}

class FooterWidget extends StatelessWidget {
  const FooterWidget();
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

此更改可能看起來很簡單,但它可以幫助我們避免重新構建 const 小部件。

在 ListView 中為長列表編碼 itemExtent

為了理解如何最好地使用 itemExtent,假設我們有一個有幾千個元素的列表,當一個動作被觸發時,例如點選一個按鈕,我們需要跳到最後一個元素。這時 itemExtent 可以極大地提高 ListView 的佈局效能。

指定 itemExtent 比讓 children元素 確定他們的範圍更有效,因為滾動機器可以使用對children元素範圍的預知來節省工作,如下所示:

class LongListView extends StatelessWidget {
  final _scrollController = ScrollController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed:() {
        _scrollController.jumpTo(
          _scrollController.position.maxScrollExtent,
        );
      }),
      body: ListView(
        controller: _scrollController,
        children: List.generate(10000, (index) => Text('Index: $index')),
        itemExtent: 400,
      ),
    );
  }
}

避免使用大Tree

對於何時將小部件拆分為較小的小部件,沒有硬性規定。但是,最好避免使用大樹,因為它有以下好處:

  • 促進可重用性
  • 提供更簡潔的程式碼
  • 增強可讀性
  • 啟用封裝
  • 提供快取機制

因此,你應該儘可能地將你的程式碼分割成不同的小部件。

瞭解 Flutter 中的約束

每個 Flutter 開發人員都必須知道的 Flutter 佈局的黃金法則是:約束下降,尺寸上升,父級設定位置。

讓我們來分析一下。

小部件從其父級獲得自己的約束。一個約束只是一組四個雙精度:最小和最大寬度,以及最小和最大高度。

然後,小部件會遍歷其自己的子級列表。小部件一個接一個地告訴它的孩子他們的約束是什麼(每個孩子可能不同),然後詢問每個孩子它想要的大小。

接下來,小部件將其子項(水平在 x 軸上,垂直在 y 軸上)一一定位。最後,小部件告訴它的父級它自己的大小(當然是在原始約束範圍內)。

在Flutter中,所有的小部件都是基於父級或它們的盒子約束來渲染自己。這帶來了一些限制。例如,想象一下,你在一個父部件內有一個子部件,你想決定它的大小。這個小元件不能有任何尺寸! 它的尺寸必須在它的父物件所設定的約束範圍內。

與第一個示例類似,小部件無法知道自己在螢幕中的位置,因為這是父小部件的決定。

也就是說,如果一個子部件決定了與它的父部件不同的尺寸,而父部件沒有足夠的資訊來對齊它,那麼子部件的尺寸可能被忽略。

好的,讓我們看看這個。

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyWidget();
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
       constraints: const BoxConstraints(
         maxHeight: 400,
         minHeight: 100,
         minWidth: 100,
         maxWidth: 400,
       ),
      child: Container(
        color: Colors.green,
      ),
    );
  }
}

如果您願意,您可以忽略 ConstrainedBox 並將高度和小部件新增到 Container 中。

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Container(
      height: 400, 
      width: 400,
      color: Colors.green,
    );
  }
}

您可能希望上面的程式碼渲染一個最大高度和寬度為 400 的綠色 Container。但是,當您執行此程式碼時,您會感到驚訝。

整個螢幕將是純綠色!我不會在這裡深入探討細節,但在構建 Flutter 佈局時,您可能會遇到幾個與此類似的問題。

讓我們看看這裡發生了什麼。在上面的示例中,樹如下所示:

    - `MyApp`
    - `MyWidget`
    - `ConstrainedBox`
    - `Container`

約束規則將從父控制元件傳遞給子控制元件,因此子控制元件可以在其父控制元件的給定約束範圍內決定其大小。因此,約束適用。

因此,Flutter 將嚴格約束傳遞給 MyApp(),然後 MyApp() 將其嚴格約束傳遞給 ConstrainedBox。然後,ConstrainedBox 被迫忽略它自己的約束並使用它的父級,在這種情況下,它是全屏大小,這就是為什麼你會看到一個全屏的綠色框。

通常,你會發現新增一個 Center小部件可能會解決這個問題。讓我們試一試吧。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: MyWidget()
    );
  }
}

瞧!這就對了。它已經修好了!

Center 小部件從 MyApp() 中獲取一個嚴格的約束,並將其轉換為對其子項的鬆散約束,即 ConstrainedBox。因此,Container 遵循 ConstrainedBox 給出的約束,以便 Container 應用最小和最大大小。

在我們完成本節之前,讓我快速解釋一下什麼是緊約束和鬆約束。

一個嚴格的約束提供了一種可能性——一個精確的尺寸,這意味著它的最大寬度等於它的最小寬度,它的最大高度等於它的最小高度。

如果你轉到 Flutter 的 box.dart 檔案並搜尋 BoxConstraints 建構函式,你會發現以下內容:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

另一方面,鬆散約束設定最大寬度和高度,但允許小部件儘可能小。它的最小寬度和高度都等於 0

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

如果你再看上面的例子,它告訴我們 Center 允許綠色 Container比螢幕更小,但不能更大。當然,Center通過將鬆散的約束傳遞給 Container來做到這一點。

結束

在這篇文章中,我提到了一些你在開始構建Flutter應用程式時應該實施的許多最佳實踐。然而,還有更多的--更高階的--實踐需要考慮,我建議你去看看Flutter的詳盡文件。編碼愉快。


近期文章

相關文章