認識你的第一個Flutter專案

上一堂課我們建立了第一個Flutter專案,這個專案非常簡單,畫面上只有一個按鈕,點擊按鈕數字就會遞增。以下是這個專案的目錄結構

專案目錄說明

畫面上會看到許多資料夾與檔案,包含 android、web、windows、lib、test 等等,那你要撰寫的原始碼到底放在哪裡?是 android 嗎?還是 windows?其實都不是,真正存放程式碼的是 lib 資料夾,打開 lib 後會看到一支 main.dart 檔案

往後所有原始碼都會放在 lib 裡,開發使用的程式語言是 Dart,不是 Java 也不是 C++,Flutter 本身就是以 Dart 語言開發。這時你可能會疑惑,為什麼專案根目錄會有 android、web、windows 這些資料夾?

其實原理很好懂,Flutter 之所以能夠跨平台編譯成不同系統的應用程式,就是靠這些平台資料夾來實現。

Flutter 是一套單一Dart程式碼、多平台執行的開發框架,lib 資料夾存放你撰寫的跨平台共用業務邏輯;而 android / windows / web / macos / linux 這些資料夾,則是各個平台的原生宿主專案,負責把 Flutter 渲染引擎嵌入對應的作業系統執行。

1 android 資料夾

這是Android 平台原生專案

負責提供 Android App 的執行容器、承載 Flutter 渲染引擎,所有 Android 底層相關設定都在這裡調整。舉例來說,App 權限設定、圖示、套件名稱等,都在此配置。同時也能在這裡設定 Android 專屬功能、呼叫 Android 原生 API(相機、藍牙、通知、定位等等)

2 windows 資料夾

這是 Windows 桌面端原生專案

屬於 C++ Win32 原生專案,負責建立 Windows 視窗、初始化 Skia 渲染引擎。Skia 是 Google 推出的圖形函式庫,Flutter 桌面應用就是透過它繪製視窗與各種介面元件。

windows 資料夾內的檔案,掌管 Windows 桌面軟體所有底層設定,例如視窗尺寸、標題列、軟體圖示、系統匣(Windows 右下角常駐圖示)

開發完成後,可以打包成 exe、msix 安裝檔,上架至微軟市集。

3. web 資料夾

Web 靜態網頁專案(HTML/JS/CSS),提供網頁進入點 index.html,你的 Dart 原始碼最後會編譯成網頁相關資源。

可設定網頁標題、圖示、靜態資源、路由導覽,編譯後輸出 HTML/CSS/JS 檔,直接部署到伺服器就能在瀏覽器開啟

其他平台資料夾

我們這份專案看不到 ios、macos、linux 資料夾,是當初建立專案時沒有勾選對應平台,不過這些資料夾的運作原理都相同。

ios:蘋果 iOS 原生 Xcode 專案,用來打包 ipa 檔上架 iPhone

macos:macOS 桌面 Cocoa 專案,開發 Mac 電腦客戶端

linux:Linux 桌面 GTK 原生專案

在 Android Studio 右上角切換不同平台,就能編譯出對應系統的應用程式。當然也可以直接使用指令列執行,事實上點擊介面上綠色執行按鈕切換平台,底層也是自動執行 Flutter 指令

# 執行Android版,讀取android資料夾設定
flutter run -d android

# 執行Windows桌面版,載入windows內的C++專案
flutter run -d windows

# 執行網頁版,載入web資料夾的html進入點
flutter run -d chromeCode language: PHP (php)

打開 windows 資料夾下的 runner / main.cpp

#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>

#include "flutter_window.h"
#include "utils.h"

int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
                      _In_ wchar_t *command_line, _In_ int show_command) {
  // Attach to console when present (e.g., 'flutter run') or create a
  // new console when running with a debugger.
  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
    CreateAndAttachConsole();
  }

  // Initialize COM, so that it is available for use in the library and/or
  // plugins.
  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

  flutter::DartProject project(L"data");

  std::vector<std::string> command_line_arguments =
      GetCommandLineArguments();

  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));

  FlutterWindow window(project);
  Win32Window::Point origin(10, 10);
  Win32Window::Size size(1280, 720);
  if (!window.Create(L"myproject", origin, size)) {
    return EXIT_FAILURE;
  }
  window.SetQuitOnClose(true);

  ::MSG msg;
  while (::GetMessage(&msg, nullptr, 0, 0)) {
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
  }

  ::CoUninitialize();
  return EXIT_SUCCESS;
}
Code language: PHP (php)

如果編譯 Windows 桌面程式,這支 main.cpp 才是程式真正的進入點,系統會先執行 main.cpp,接著才載入 lib/main.dart 內的 Flutter 程式

同樣地,打開 android 資料夾會發現,內部結構和一般使用 Kotlin、Java 開發的 Android 專案幾乎一模一樣。

可以看到熟悉的 src 目錄、AndroidManifest.xml、MainActivity 等等,是不是和原生 Android 開發很相似?沒錯,使用 Flutter 開發 Android App,底層也是從這個 android 原生專案開始執行。

切換到 web 資料夾,同樣能看見網頁程式進入點 index.html

認識 Dart 程式檔

lib 底下的 main.dart 是日後開發最常使用的檔案,你也可以在 lib 內建立子資料夾、新增其他 Dart 檔案管理程式碼。

打開 main.dart 檢視完整程式

// 匯入Material介面元件函式庫
import 'package:flutter/material.dart';

// 程式進入主函式
void main() {
  runApp(const MyApp());
}

// 應用程式根元件(無狀態元件)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      // 全域主題設定
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      // 預設首頁
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

// 首頁頁面(有狀態元件)
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  // 頁面標題參數
  final String title;

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

// 首頁狀態管理類別
class _MyHomePageState extends State<MyHomePage> {
  // 計數器變數
  int _counter = 0;

  // 計數遞增函式
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @Override
  Widget build(BuildContext context) {
    // 頁面基礎布局骨架
    return Scaffold(
      // 頂部導覽列
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      // 頁面主要內容區塊
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter', // 顯示計數變數數值
              style: Theme.of(context).textTheme.headlineMedium, // 文字樣式設定
            ),
          ],
        ),
      ),
      // 右下角浮動按鈕:onPressed綁定點擊事件、tooltip為滑鼠提示文字、child設定按鈕圖示
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}Code language: JavaScript (javascript)

1. 程式進入區段

import 'package:flutter/material.dart';
void main() {
  runApp(const MyApp());
}
Code language: JavaScript (javascript)
  • 匯入 Google 官方 Material Design 元件庫,內建按鈕、頁面、文字等全套UI元件;蘋果系統則有專屬的介面框架
  • main 是 Dart 程式唯一進入點;runApp() 負責啟動應用程式,掛載根元件 MyApp

2. MyApp 全域根元件

屬於 StatelessWidget 無狀態元件,後續章節會詳細說明,這邊先簡單認識即可

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

  // 整支應用程式的根元件
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
Code language: JavaScript (javascript)
  • 全域設定:統一設定App名稱、全域主題、預設首頁
  • MaterialApp:Material 風格應用外殼,集中管理路由、主題、頁面標題
  • home 指定程式開啟時預設載入的頁面 MyHomePage

3. MyHomePage 首頁元件

開啟軟體時預設顯示的畫面,類型為 StatefulWidget 有狀態元件,後續會完整講解。

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

  @Override
  State<MyHomePage> createState() => _MyHomePageState();
}Code language: JavaScript (javascript)
  • 頁面具備可變動資料(計數數字),因此使用有狀態元件
  • 外部傳入頁面標題 title,在狀態類別內透過 widget.title 讀取使用
  • 每一個有狀態元件都必須搭配專屬的 _MyHomePageState 類別,存放頁面變數與互動邏輯

4. _MyHomePageState 狀態類別

專門處理頁面邏輯、畫面渲染、介面更新的狀態類別

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

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

  @Override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(

        backgroundColor: Theme.of(context).colorScheme.inversePrimary,

        title: Text(widget.title),
      ),
      body: Center(

        child: Column(

          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            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),
      ),
    );
  }
}Code language: JavaScript (javascript)
  1. int _counter = 0:頁面可變動資料,也就是計數數值
  2. _incrementCounter():按鈕點擊觸發函式
    • setState() 通知 Flutter 資料已變更,重新繪製頁面介面
    • 計數器數值 + 1
  3. build():每次介面更新都會執行,回傳完整頁面布局結構

5. Scaffold 頁面骨架

屬於 _MyHomePageState 內的頁面布局模板

  • appBar:畫面頂部標題列
  • body:頁面主要內容區塊
    • Center 將元件置中、Column 讓文字垂直排列
  • floatingActionButton:右下角圓形加號按鈕,綁定計數遞增函式

完整執行流程

  1. 程式啟動 → 執行main函式 → 載入根元件MyApp
  2. MyApp渲染MaterialApp外殼 → 載入預設首頁MyHomePage
  3. 頁面繪製頂部標題、置中文字、右下角加號按鈕
  4. 點擊按鈕 → 執行_incrementCounter、呼叫setState重繪介面 → 畫面數字更新
Previous:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *