Flutter 是一個使用 Dart 程式語言構建跨平臺應用程式的框架。

在構建 Flutter 應用程式時,您的 Jetpack Compose 知識和經驗非常有價值。

本文件可作為參考,您可以跳躍閱讀並查詢與您的需求最相關的問題。本指南嵌入了示例程式碼。透過使用滑鼠懸停或聚焦時出現的“在 DartPad 中開啟”按鈕,您可以在 DartPad 上開啟並執行某些示例。

概述

#

Flutter 和 Jetpack Compose 程式碼描述了 UI 的外觀和工作方式。開發人員將此類程式碼稱為宣告式框架

儘管在與舊版 Android 程式碼互動方面存在關鍵差異,但這兩個框架之間有許多共同點。

可組合項與 Widgets

#

Jetpack Compose 將 UI 元件表示為可組合函式,本文件後面將其稱為可組合項。可組合項可以透過使用修飾符物件進行更改或裝飾。

kotlin
Text("Hello, World!", 
   modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
    modifier = Modifier.padding(10.dp))

Flutter 將 UI 元件表示為widgets

可組合項和 widgets 都只存在到它們需要更改為止。這些語言將此屬性稱為不變性。Jetpack Compose 使用由 Modifier 物件支援的可選修飾符屬性修改 UI 元件屬性。相比之下,Flutter 將 widgets 用於 UI 元件及其屬性。

dart
Padding(                         // <-- This is a Widget
  padding: EdgeInsets.all(10.0), // <-- So is this
  child: Text("Hello, World!"),  // <-- This, too
)));

為了組合佈局,Jetpack Compose 和 Flutter 都將 UI 元件巢狀在彼此內部。Jetpack Compose 巢狀 Composables,而 Flutter 巢狀 Widgets

佈局流程

#

Jetpack Compose 和 Flutter 以相似的方式處理佈局。它們都一次性佈局 UI,並且父元素將其佈局約束傳遞給子元素。更具體地說:

  1. 父級遞迴地測量自身及其子級,並將父級的任何約束提供給子級。
  2. 子級嘗試使用上述方法調整自身大小,並向其自己的子級提供其約束以及可能適用於其祖先節點的任何約束。
  3. 當遇到葉節點 (沒有子節點的節點) 時,根據提供的約束確定大小和屬性,並將元素放置在 UI 中。
  4. 所有子級都已確定大小和位置後,根節點可以確定其測量、大小和位置。

在 Jetpack Compose 和 Flutter 中,父元件可以覆蓋或約束子元件的所需大小。widget 不能擁有它想要的任何大小。它通常也不能知道或決定其在螢幕上的位置,因為其父級會做出該決定。

要強制子 widget 以特定大小渲染,父級必須設定嚴格的約束。當其約束的最小大小值等於其最大大小值時,約束將變得嚴格。

要了解 Flutter 中約束的工作原理,請訪問理解約束

設計系統

#

因為 Flutter 面向多個平臺,所以您的應用程式無需遵循任何設計系統。雖然本指南介紹了 Material widgets,但您的 Flutter 應用程式可以使用許多不同的設計系統

  • 自定義 Material widgets
  • 社群構建的 widgets
  • 您自己的自定義 widgets

如果您正在尋找一個具有自定義設計系統的出色參考應用程式,請檢視Wonderous

UI 基礎

#

本節涵蓋了 Flutter 中 UI 開發的基礎知識,以及它與 Jetpack Compose 的比較。這包括如何開始開發您的應用程式、顯示靜態文字、建立按鈕、響應按壓事件、顯示列表、網格等。

入門

#

對於 Compose 應用程式,您的主要入口點將是 Activity 或其後代之一,通常是 ComponentActivity

kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SampleTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

要啟動您的 Flutter 應用程式,請將應用程式例項傳遞給 runApp 函式。

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

App 是一個 widget。它的 build 方法描述了它所代表的使用者介面部分。通常,您的應用程式會以 WidgetApp 類(例如 MaterialApp)開頭。

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomePage(),
    );
  }
}

HomePage 中使用的 widget 可能會以 Scaffold 類開頭。 Scaffold 為應用程式實現了基本的佈局結構。

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text(
          'Hello, World!',
        ),
      ),
    );
  }
}

請注意 Flutter 如何使用 Center widget。

Compose 從其祖先 Android Views 繼承了許多預設值。除非另有說明,否則大多陣列件都會將其大小“包裹”到內容,這意味著它們在渲染時只佔用所需的空間。Flutter 並非總是如此。

要使文字居中,請將其包裝在 Center widget 中。要了解不同的 widget 及其預設行為,請檢視Widget 目錄

新增按鈕

#

Compose 中,您可以使用 Button 可組合項或其變體之一來建立按鈕。使用 Material 主題時,ButtonFilledTonalButton 的別名。

kotlin
Button(onClick = {}) {
    Text("Do something")
}

要在 Flutter 中實現相同的效果,請使用 FilledButton

dart
FilledButton(
  onPressed: () {
    // This closure is called when your button is tapped.
  },
  const Text('Do something'),
),

Flutter 為您提供了各種預定義樣式的按鈕。

水平或垂直對齊元件

#

Jetpack Compose 和 Flutter 以相似的方式處理專案的水平和垂直集合。

以下 Compose 程式碼片段在 RowColumn 容器中添加了一個地球影像和文字,並使專案居中

kotlin
Row(horizontalArrangement = Arrangement.Center) {
   Image(Icons.Default.Public, contentDescription = "")
   Text("Hello, world!")
}

Column(verticalArrangement = Arrangement.Center) {
   Image(Icons.Default.Public, contentDescription = "")
   Text("Hello, world!")
}

Flutter 也使用 RowColumn,但在指定子 widget 和對齊方式方面有一些細微的差別。以下程式碼與 Compose 示例等效。

dart
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Icon(Icons.public),
    Text('Hello, world!'),
  ],
),

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Icon(MaterialIcons.globe),
    Text('Hello, world!'),
  ],
)

RowColumn 需要在 children 引數中包含一個 List<Widget>mainAxisAlignment 屬性告訴 Flutter 如何在額外空間中定位子項。MainAxisAlignment.center 將子項定位在主軸的中心。對於 Row,主軸是水平軸;對於 Column,主軸是垂直軸。

顯示列表檢視

#

Compose 中,您可以通過幾種方式根據所需列表的大小建立列表。對於可以一次性顯示的小量專案,您可以在 ColumnRow 中遍歷集合。

對於包含大量專案的列表,LazyList 具有更好的效能。它只佈局可見的元件,而不是所有元件。

kotlin
data class Person(val name: String)

val people = arrayOf(
   Person(name = "Person 1"),
   Person(name = "Person 2"),
   Person(name = "Person 3")
)

@Composable
fun ListDemo(people: List<Person>) {
   Column {
      people.forEach {
         Text(it.name)
      }
   }
}

@Composable
fun ListDemo2(people: List<Person>) {
   LazyColumn {
      items(people) { person ->
         Text(person.name)
      }
   }
}

要在 Flutter 中延遲構建列表,...

dart
class Person {
  String name;
  Person(this.name);
}

var items = [
  Person('Person 1'),
  Person('Person 2'),
  Person('Person 3'),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index].name),
          );
        },
      ),
    );
  }
}

Flutter 對列表有一些約定

  • ListView widget 有一個 builder 方法。這與 Compose LazyList 中的 item 閉包類似。

  • ListViewitemCount 引數設定 ListView 顯示的專案數量。

  • itemBuilder 有一個索引引數,其值將在零到 itemCount 減一之間。

上一個示例為每個專案返回一個 ListTile widget。ListTile widget 包含 heightfont-size 等屬性。這些屬性有助於構建列表。但是,Flutter 允許您返回幾乎任何代表您資料的 widget。

顯示網格

#

Compose 中構建網格類似於 LazyList (LazyColumnLazyRow)。您可以使用相同的 items 閉包。每種網格型別都有屬性來指定如何排列專案,是否使用自適應或固定佈局等。

kotlin
val widgets = arrayOf(
        "Row 1",
        Icons.Filled.ArrowDownward,
        Icons.Filled.ArrowUpward,
        "Row 2",
        Icons.Filled.ArrowDownward,
        Icons.Filled.ArrowUpward
    )

    LazyVerticalGrid (
        columns = GridCells.Fixed(3),
        contentPadding = PaddingValues(8.dp)
    ) {
        items(widgets) { i ->
            if (i is String) {
                Text(i)
            } else {
                Image(i as ImageVector, "")
            }
        }
    }

要在 Flutter 中顯示網格,請使用 GridView widget。此 widget 有各種建構函式。每個建構函式的目標相似,但使用不同的輸入引數。以下示例使用 .builder() 初始化器

dart
const widgets = [
  Text('Row 1'),
  Icon(Icons.arrow_downward),
  Icon(Icons.arrow_upward),
  Text('Row 2'),
  Icon(Icons.arrow_downward),
  Icon(Icons.arrow_upward),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisExtent: 40,
        ),
        itemCount: widgets.length,
        itemBuilder: (context, index) => widgets[index],
      ),
    );
  }
}

SliverGridDelegateWithFixedCrossAxisCount 代理確定網格用於佈局其元件的各種引數。這包括 crossAxisCount,它決定每行顯示的專案數。

Jetpack Compose 的 LazyHorizontalGridLazyVerticalGrid 和 Flutter 的 GridView 有些相似。GridView 使用委託來決定網格應該如何佈局其元件。LazyHorizontalGrid / LazyVerticalGrid 上的 rowscolumns 和其他相關屬性具有相同的目的。

建立滾動檢視

#

Jetpack Compose 中的 LazyColumnLazyRow 內建了滾動支援。

要建立可滾動檢視,Flutter 使用 SingleChildScrollView。在以下示例中,函式 mockPerson 模擬 Person 類的例項以建立自定義 PersonView widget。

dart
SingleChildScrollView(
  child: Column(
    children: mockPersons
        .map(
          (person) => PersonView(
            person: person,
          ),
        )
        .toList(),
  ),
),

響應式和自適應設計

#

Compose 中的自適應設計是一個複雜的主題,有許多可行的解決方案

  • 使用自定義佈局
  • 單獨使用 WindowSizeClass
  • 使用 BoxWithConstraints 根據可用空間控制顯示內容
  • 使用 Material 3 自適應庫,該庫結合 WindowSizeClass 和專門的可組合佈局來實現常見佈局

因此,建議您直接研究 Flutter 選項,看看什麼適合您的需求,而不是嘗試找到一對一的翻譯。

要在 Flutter 中建立相對檢視,您可以使用以下兩種選項之一

  • LayoutBuilder 類中獲取 BoxConstraints 物件。
  • 在您的構建函式中使用 MediaQuery.of() 獲取當前應用程式的大小和方向。

要了解更多資訊,請查閱建立響應式和自適應應用程式

狀態管理

#

Compose 使用 remember API 和 MutableState 介面的後代來儲存狀態。

kotlin
Scaffold(
   content = { padding ->
      var _counter = remember {  mutableIntStateOf(0) }
      Column(horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = Arrangement.Center,
         modifier = Modifier.fillMaxSize().padding(padding)) {
            Text(_counter.value.toString())
            Spacer(modifier = Modifier.height(16.dp))
            FilledIconButton (onClick = { -> _counter.intValue += 1 }) {
               Text("+")
            }
      }
   }
)

Flutter 使用 StatefulWidget 管理區域性狀態。透過以下兩個類實現有狀態 widget

  • StatefulWidget 的子類
  • State 的子類

State 物件儲存 widget 的狀態。要更改 widget 的狀態,請從 State 子類呼叫 setState() 以告知框架重新繪製 widget。

以下示例顯示了計數器應用程式的一部分

dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$_counter'),
            TextButton(
              onPressed: () => setState(() {
                _counter++;
              }),
              child: const Text('+'),
            ),
          ],
        ),
      ),
    );
  }
}

要了解更多管理狀態的方法,請查閱狀態管理

螢幕繪圖

#

Compose 中,您使用 Canvas 可組合項在螢幕上繪製形狀、影像和文字。

Flutter 有一個基於 Canvas 類的 API,其中有兩個類可幫助您繪製

  1. CustomPaint,它需要一個 painter

    dart
    CustomPaint(
      painter: SignaturePainter(_points),
      size: Size.infinite,
    ),
  2. CustomPainter,它實現您的演算法來繪製到畫布上。

    dart
    class SignaturePainter extends CustomPainter {
      SignaturePainter(this.points);
    
      final List<Offset?> points;
    
      @override
      void paint(Canvas canvas, Size size) {
        final Paint paint = Paint()
          ..color = Colors.black
          ..strokeCap = StrokeCap.round
          ..strokeWidth = 5;
        for (int i = 0; i < points.length - 1; i++) {
          if (points[i] != null && points[i + 1] != null) {
            canvas.drawLine(points[i]!, points[i + 1]!, paint);
          }
        }
      }
    
      @override
      bool shouldRepaint(SignaturePainter oldDelegate) =>
          oldDelegate.points != points;
    }

主題、樣式和媒體

#

您可以輕鬆地為 Flutter 應用程式設定樣式。樣式包括在亮色和深色主題之間切換、更改文字和 UI 元件的設計等。本節介紹如何為您的應用程式設定樣式。

使用深色模式

#

Compose 中,您可以透過使用 Theme 可組合項包裝元件,在任意級別控制亮色和深色模式。

Flutter 中,您可以在應用程式級別控制亮色和深色模式。要控制亮度模式,請使用 App 類的 theme 屬性

dart
const MaterialApp(
  theme: ThemeData(
    brightness: Brightness.dark,
  ),
  home: HomePage(),
);

文字樣式

#

Compose 中,您可以使用 Text 的屬性設定一個或兩個屬性,或構造一個 TextStyle 物件一次性設定多個屬性。

kotlin
Text("Hello, world!", color = Color.Green,
        fontWeight = FontWeight.Bold, fontSize = 30.sp)
kotlin
Text("Hello, world!", 
   style = TextStyle(
      color = Color.Green, 
      fontSize = 30.sp, 
      fontWeight = FontWeight.Bold
   ),
)

要在 Flutter 中設定文字樣式,請將 TextStyle widget 新增為 Text widget 的 style 引數的值。

dart
Text(
  'Hello, world!',
  style: TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
),

按鈕樣式

#

Compose 中,您可以使用 colors 屬性修改按鈕的顏色。如果未修改,它們將使用當前主題的預設值。

kotlin
Button(onClick = {},
   colors = ButtonDefaults.buttonColors().copy(
      containerColor = Color.Yellow, contentColor = Color.Blue,
       )) {
    Text("Do something", fontSize = 30.sp, fontWeight = FontWeight.Bold)
}

要在 Flutter 中設定按鈕 widget 的樣式,您可以類似地設定其子級的樣式,或修改按鈕本身的屬性。

dart
FilledButton(
  onPressed: (){},
  style: FilledButton.styleFrom(backgroundColor: Colors.amberAccent),
  child: const Text(
    'Do something',
    style: TextStyle(
      color: Colors.blue,
      fontSize: 30,
      fontWeight: FontWeight.bold,
    )
  )
)

在 Flutter 中打包資產

#

通常需要將資源打包以供應用程式使用。它們可以是動畫、向量圖形、影像、字型或其他通用檔案。

與期望在 /res/<qualifier>/ 下設定目錄結構的本機 Android 應用程式不同(其中 qualifier 可以指示檔案型別、特定方向或 Android 版本),Flutter 不需要特定位置,只要引用的檔案在 pubspec.yaml 檔案中列出即可。下面是引用了多個影像和字型檔案的 pubspec.yaml 摘錄。

yaml
flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png
  fonts:
    - family: FiraSans
      fonts:
        - asset: fonts/FiraSans-Regular.ttf

使用字型

#

Compose 中,您可以透過兩種方式在應用程式中使用字型。您可以使用執行時服務從 Google Fonts 獲取字型。或者,它們可以打包在資原始檔中。

Flutter 有類似的方法來使用字型,讓我們線上討論它們。

使用捆綁字型

#

以下是大致等效的 Compose 和 Flutter 程式碼,用於使用 /res/fonts 目錄中的字型檔案,如上所示。

kotlin
// Font files bundled with app
val firaSansFamily = FontFamily(
   Font(R.font.firasans_regular, FontWeight.Normal),
   // ...
)

// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
dart
Text(
  'Flutter',
  style: TextStyle(
    fontSize: 40,
    fontFamily: 'FiraSans',
  ),
),

使用字型提供程式 (Google Fonts)

#

一個不同點是使用來自字型提供商(如 Google Fonts)的字型。在 Compose 中,例項化是內聯完成的,使用與引用本地檔案大致相同的程式碼。

在例項化引用字型服務的特殊字串的提供程式後,您將使用相同的 FontFamily 宣告。

kotlin
// Font files bundled with app
val provider = GoogleFont.Provider(
    providerAuthority = "com.google.android.gms.fonts",
    providerPackage = "com.google.android.gms",
    certificates = R.array.com_google_android_gms_fonts_certs
)

val firaSansFamily = FontFamily(
    Font(
        googleFont = GoogleFont("FiraSans"),
        fontProvider = provider,
    )
)

// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Light)

對於 Flutter,這由 google_fonts 外掛使用字型名稱提供。

dart
import 'package:google_fonts/google_fonts.dart';
//...
Text(
  'Flutter',
  style: GoogleFonts.firaSans(),
  // or 
  //style: GoogleFonts.getFont('FiraSans')
),

使用圖片

#

Compose 中,通常將影像檔案放入資源目錄 /res/drawable 中,並使用 Image 可組合項顯示影像。資產透過使用資源定位器以 R.drawable.<檔名> 的形式引用,不帶副檔名。

Flutter 中,資源位置在 pubspec.yaml 中列出,如下面程式碼片段所示。

yaml
    flutter:
      assets:
        - images/Blueberries.jpg

新增圖片後,您可以使用 Image widget 的 .asset() 建構函式顯示它。此建構函式

要檢視完整示例,請查閱 Image 文件。