iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0
Mobile Development

從零開始以Flutter打造跨平台聊天APP系列 第 8

Day-8 建構第一個 Flutter APP

  • 分享至 

  • xImage
  •  

Generated from Stable Diffusion 3 Medium

這章不會教大家怎麼安裝 Flutter,而是會以一個比較宏觀的角度告訴大家一個 Flutter App 的架構。

本次教學使用 Flutter 3.24 進行。本章節的範例程式碼:https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/hello

但建議讀者可以自行在本地以指令建立。

安裝 Flutter

首先我們可以在 Flutter 官網中安裝 Flutter https://flutter.dev.org.tw/get-started/install

因為每個人電腦環境狀況不一樣,因此這裡不提供這方面的教學。另外,我們也可以使用 Google Project IDX 來新增一個 Flutter 專案

本地新增一個 Flutter 專案

在使用 flutter 指令前,我們可以先使用 flutter doctor 來測試 flutter 是否安裝成功

> flutter doctor

cmd with flutter doctor

像我目前的裝置,因為沒有要開發 windows app 所以可以不安裝 Visual Studio

我們可以直接利用命令列新增一個簡單的 flutter 專案,直接命名成 hello

> flutter create hello

以下是整個專案的結構,我們主要的程式在 lib 資料夾中

.
└── hello/
    ├── .dart_tool/  # Dart 編譯器和工具所使用的內部資料夾,存放暫存資料和依賴管理
    ├── .idea/       # IDE 設定檔(通常為 JetBrains 系列,如 IntelliJ 或 Android Studio)
    ├── android/     # Android 平台專用的原生程式碼與配置,當部署到 Android 時會用到
    ├── ios/         # iOS 平台專用的原生程式碼與配置,當部署到 iOS 時會用到
    ├── lib/         # 放置主要的 Dart 程式碼,包括主應用程式邏輯
    ├── linux/       # Linux 平台專用的原生程式碼與配置,當部署到 Linux 時會用到
    ├── macos/       # macOS 平台專用的原生程式碼與配置,當部署到 macOS 時會用到
    ├── test/        # 測試程式碼放置處,用來進行單元測試或其他自動化測試
    ├── web/         # Web 平台專用的原生程式碼與配置,當部署到 Web 時會用到
    ├── windows/     # Windows 平台專用的原生程式碼與配置,當部署到 Windows 時會用到
    ├── .gitignore   # 定義哪些檔案或資料夾不會被 Git 版本控制系統追蹤
    ├── .metadata    # Flutter 專案的元數據檔案,包含一些版本與配置資訊
    ├── analysis_options.yaml  
                     # Dart 分析器的配置檔案,用來控制靜態分析的行為
    ├── hello.iml     # IntelliJ IDEA 專案檔,儲存 IDE 專案設定
    ├── pubspec.lock # 鎖定依賴版本的檔案,確保每次構建使用的依賴版本一致
    ├── pubspec.yaml # Flutter 專案的配置檔案,定義專案的依賴、版本等資訊
    └── README.md    # 專案的說明文件,通常包含簡單介紹、如何使用、安裝等資訊

初始化後,我們可以馬上執行專案

> flutter run

此時會跳出幾個選項,我們可以直接使用 Chrome 或 Edge 來查看專案

cmd with flutter run

選擇 2 後,Flutter 會自動開啟一個新的 Chrome 瀏覽器運行 Flutter 專案

app running in the chrome after flutter run

為了更改 APP 的內容,我們可以更改專案中的 lib/main.dart

Flutter 架構

import 'package:flutter/material.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // ...
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // ...
  @override
  Widget build(BuildContext context) {
    // ...
  }
}

首先,Flutter 專案中一樣有個 main 函式,main 函式裡會調用 runApp()runApp() 必需要傳入一個 Widget 物件

void runApp(Widget app)

在 Flutter 中有兩大 Widget:StatelessWidget 以及 StatefulWidget

StatelessWidget

StatelessWidget 是無狀態的 widget,代表一個不會改變的 UI 元素。當 widget 被 build 之後,它的狀態不會發生變化。因此,如果 UI 的外觀或行為只需根據輸入的參數進行渲染,且不需要動態更新,StatelessWidget 是一個好的選擇。

使用場景

  • 顯示靜態的內容,例如文字、圖標、圖片等。
  • 像按鈕、標籤等不會隨時間改變狀態的元素。

StatefulWidget

StatefulWidget 是有狀態的 widget,它的 UI 可以在 widget 的生命週期中發生變化。它由兩部分組成:

  • StatefulWidget:定義 widget 是什麼,但不保存狀態。
  • State:保存與 widget 相關的狀態,並在**「狀態變更」**時重新渲染 UI。

當 widget 的狀態需要隨時間改變,例如按鈕被點擊後會有不同的反應,或是從網路載入數據時使用進度條,StatefulWidget 就派上用場。

使用場景

  • 按鈕點擊、滑動條、輸入框等有狀態的 UI 元素。
  • 需要根據用戶交互或外部數據更新 UI。

解釋架構

我們可以將 Widget 想像成一棵樹 (資料結構的樹),runApp 會所放的就是 MyApp 這個根節點

runApp → MyApp → MyHomePage

在使用 StatelessWidget 時,我們必需先建立建構函式,因為建構函式建構 MyApp 後不會再更動所以我們會加上 const,並且當 flutter 在建構這些 Widget 時,都會給予 key 值,也因此我們可以把得到的 key 值傳遞給父母類 StatelessWidget

接著我們需要 @override 一個函式 build()build 函式會在該 widget 被加入樹時,或者一些相依性更動時被呼叫。現在,我們其實只需要關注 build 函式內的 return 就好,return 需要回傳一個 Widget,在 flutter 中,我們最外層的 Widget 會使用 MaterialApp。Material 是 Android 的設計風格。除了以 MaterialApp 作為起點,我們其實也可以使用 ios 風格的 CupertinoApp

class MyApp extends StatelessWidget {
  // 建構函式,
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Notice that the counter didn't reset back to zero; the application
        // state is not lost during the reload. To reset the state, use hot
        // restart instead.
        //
        // This works for code too, not just values: Most code changes can be
        // tested with just a hot reload.
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

MateralApp 建構時有很多地方可供調整,如果要繼續往下搭建我們的畫面,就必需把我們的主畫面 Widget 放入 home 之中。MyHomePage 是一個 StatefulWidget。使用 StatefulWidget 時,也要先建立建構函式,除了提供 key 值之外,我們也可以另外設定自己需要的參數,比如 title (StatelessWidget 也可以只是這裡示範的是 StatefulWidget)。接著我們要 @override createState() 這個方法,createState 的功能是建構並返回一個 State 物件,這個 State 物件會保存 widget 的狀態並控制其重建。而這個 State 物件就是我們的關鍵。

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

我們可以看到我們自定的 State 物件相當複雜,其中最重要的就是 build() 函式,負責建構畫面。我們這裡會使用 Scaffold Widget 來建構畫面,主要包含三個部分:appBar, body 以及 floatingActionButton

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

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

introduce the widget in the running flutter app

當我們點擊 floatingActionButton 時會呼叫 _incrementCounter 進而影響 body 畫面中的數字,我們可以看到 _incrementCounter 會呼叫 setState 函式,其實 setState 的作用就是告訴 flutter 目前我們的 StatefulWidget 中的狀態已經被改變了,需要對 StatefulWidget 重新渲染。

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

setState() 中的 callback function 會先被執行,執行完畢後會將 StatefulWidget 標記為需要進行畫面更新。因此 setState() 中的 callback function 不可以是 async function

void setState(VoidCallback fn){
  assert(...);  // 檢查一些生命週期
  final Object? result = fn() as dynamic;  // 呼叫 fn
  assert(...);	// 檢查 fn 是否為 async function
  _element!.markNeedsBuild();  // 呼叫 markNeedsBuild()
  // markNeedsBuild() 會將元件設為 dirty 並將其加入全局 list 中
  // 告訴下一個 frame 需要將此元件重新建構
}

setState() 的作用範圍是 statefulWidget 下的 widget (subtree),也因此如果我們需要變更在我們之上的 widget 必需要用其他的方法!

做點微調

我們可以更改 _incrementCounter 使其每次都加 2

void _incrementCounter() {
  setState(() {
    _counter += 2;
  });
}

當更改後可以在 terminal 上按下 r ,這個會使 Flutter 熱重啟整個程式!按 q 則會關掉程式


後記:明天應該會講 Inheritedwidget


上一篇
Day-7 Dart 簡介(6):錯誤處理、套件、異步處理、Future及Isolate
下一篇
Day-9 在 Flutter 中使用 InheritedWidget 進行狀態管理
系列文
從零開始以Flutter打造跨平台聊天APP25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言