編寫你的第一個 Flutter 網頁應用

The web app that you'll be building

本教程可以幫助你你完成第一個 Flutter Web 應用,如果你熟悉面對物件、變數、迴圈以及條件判斷等概念,就可以完成本教程,而無需要 Dart、行動開發和 Web 開發經驗。

內容概覽

你將實現一個只顯示登入頁面的簡單 Web 應用,這個頁面包含了三個文字輸入框:名字、姓氏和使用者名。當用戶向輸入框輸入內容時,在登入區域頂部顯示一個進度條動畫效果。當用戶完成輸入時,綠色的進度條將會跟隨著充滿整個登入區域的頂部,而且 Sign up 按鈕狀態變成可點選,點選 Sign up 按鈕從螢幕下方彈出一個歡迎頁面。

右側的動圖展示了完成該教程後程序的執行效果。

第 0 步: 建立初始化 Web 應用

你將從我們為你提供的簡單 Web 應用開始學習。

  1. Enable web development.
    啟用 Web 開發。

    在指令行觀察輸出內容,你應該可以看到如下類似的內容,說明 Flutter 安裝的沒問題:

    $ flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel master, 3.4.0-19.0.pre.254, on macOS 12.6 21G115
        darwin-arm64, locale en)
    [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    [✓] Xcode - develop for iOS and macOS (Xcode 14.0)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2021.2)
    [✓] VS Code (version 1.71.1)
    [✓] Connected device (4 available)
    [✓] HTTP Host Availability
    
    • No issues found!
    

    如果你看到提示是 “flutter: command not found”,那麼就需要確保 Flutter SDK 已經正確地安裝,並且在環境變數中做好了設定。

    如上所示,顯示我們缺少 Android 工具、Android Studio 和 Xcode,如果我們只用於 Web 開發,這些都不是必要的。後續如果你想用於行動端開發,你將需要安裝設定這些工具。

  2. 查詢裝置清單。
    透過查詢裝置清單來驗證已支援 Web 開發。你將看到如下的類似內容:

    $ flutter devices
    4 connected devices:
    
    sdk gphone64 arm64 (mobile) • emulator-5554                        •
    android-arm64  • Android 13 (API 33) (emulator)
    iPhone 14 Pro Max (mobile)  • 45A72BE1-2D4E-4202-9BB3-D6AE2601BEF8 • ios
    • com.apple.CoreSimulator.SimRuntime.iOS-16-0 (simulator)
    macOS (desktop)             • macos                                •
    darwin-arm64   • macOS 12.6 21G115 darwin-arm64
    Chrome (web)                • chrome                               •
    web-javascript • Google Chrome 105.0.5195.125
    
    

    Chrome 瀏覽器會自動啟動並啟用 Flutter 開發者工具。

  3. 執行程式將在 DartPad 中顯示。
    import 'package:flutter/material.dart';
    
    void main() => runApp(const SignUpApp());
    
    class SignUpApp extends StatelessWidget {
      const SignUpApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          routes: {
            '/': (context) => const SignUpScreen(),
          },
        );
      }
    }
    
    class SignUpScreen extends StatelessWidget {
      const SignUpScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.grey[200],
          body: const Center(
            child: SizedBox(
              width: 400,
              child: Card(
                child: SignUpForm(),
              ),
            ),
          ),
        );
      }
    }
    
    class SignUpForm extends StatefulWidget {
      const SignUpForm({super.key});
    
      @override
      State<SignUpForm> createState() => _SignUpFormState();
    }
    
    class _SignUpFormState extends State<SignUpForm> {
      final _firstNameTextController = TextEditingController();
      final _lastNameTextController = TextEditingController();
      final _usernameTextController = TextEditingController();
    
      double _formProgress = 0;
    
      @override
      Widget build(BuildContext context) {
        return Form(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              LinearProgressIndicator(value: _formProgress),
              Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _firstNameTextController,
                  decoration: const InputDecoration(hintText: 'First name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _lastNameTextController,
                  decoration: const InputDecoration(hintText: 'Last name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _usernameTextController,
                  decoration: const InputDecoration(hintText: 'Username'),
                ),
              ),
              TextButton(
                style: ButtonStyle(
                  foregroundColor: MaterialStateProperty.resolveWith((states) {
                    return states.contains(MaterialState.disabled)
                        ? null
                        : Colors.white;
                  }),
                  backgroundColor: MaterialStateProperty.resolveWith((states) {
                    return states.contains(MaterialState.disabled)
                        ? null
                        : Colors.blue;
                  }),
                ),
                onPressed: null,
                child: const Text('Sign up'),
              ),
            ],
          ),
        );
      }
    }
  4. 執行程式碼範例。
    點選 Run 按鈕來執行範例程式碼。你就可以在文字框中輸入內容,但是 Sign up 按鈕是停用狀態的。

  5. 複製程式碼。
    點選程式碼區域右上角的複製圖示複製 Dart 程式碼。

  6. 建立一個新的 Flutter 工程。
    使用 IDE、編輯器或者指令行,建立一個名稱為 signin_example 的新專案,更多內容可以參考文件 Flutter 開發體驗初探

  7. 使用上面我們複製的內容替換 lib/main.dart 檔案的內容。

觀察和分析

  • 完整的範例程式碼都位於 lib/main.dart 檔案中。

  • 如果你瞭解 Java ,那 Dart 也會給你一種熟悉的感覺。

  • 應用程式的所有的 UI 的都是透過 Dart 建立的。你可以透過文件 宣告式 UI 介紹 瞭解到更多的訊息。

  • 應用的 UI 遵循 Material Design 的設計規範,這是一種在任何裝置和平台都可以執行的視覺化設計語言。而且你也有其他選擇,Flutter 也提供了一款 iOS 設計風格的 Cupertino widget 函式庫。當然你也可以建立自己的自定義 widget 函式庫。

  • 在 Flutter 的世界,萬物皆 Widget,甚至連應用本身都是 widget。應用的 UI 可以看作為 widget 樹。

第 1 步:顯示歡迎頁面

SignUpForm 類是一個 Stateful widget。這代表著 widget 的儲存訊息可動態改變,例如使用者輸入,或者傳遞的資料。由於 widget 本身是不可變的(一旦建立不可修改),所有 Flutter 的狀態訊息儲存在一種叫 State 的附加類別中。在這個程式碼範例中,所有的編輯將在一個 _SignUpFormState 的私有類別中實現。

首先,在 lib/main.dart 檔案中,在 SignUpScreen 類後面新增下面 WelcomeScreen widget 的定義類:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          'Welcome!',
          style: Theme.of(context).textTheme.displayMedium,
        ),
      ),
    );
  }
}

接下來,你需要建立一個顯示方法,然後使用按鈕透過方法控制頁面的顯示。

  1. 找到 _SignUpFormState 類別的 build() 方法。這部分程式碼是用來建立註冊按鈕的。注意,按鈕是如何定義:它是一個背景為藍色, Sign up 文字為白色的 TextButton 按鈕,當我們點選它時,並未執行任何操作。

  2. 修改按鈕的 onPressed 屬性。
    將按鈕的 onPressed 屬性改為呼叫顯示歡迎頁面的方法(該方法在下一步建立)。

    onPressed: null 改為以下內容:

    onPressed: _showWelcomeScreen,
  3. 新增 _showWelcomeScreen 方法。
    修復上述程式碼導致的編譯器提示錯誤: _showWelcomeScreen is not defined. (未定義 _showWelcomeScreen)。在 build() 方法上方新增下面的方法:

    void _showWelcomeScreen() {
      Navigator.of(context).pushNamed('/welcome');
    }
  4. 新增 /welcome 頁面路由。
    為新的頁面新增跳轉路由。在 SignUpApp 類別的 build() 方法中,在 '/' 下面新增如下路由:

    '/welcome': (context) => const WelcomeScreen(),
  5. 執行該應用程式。
    Sign up 按鈕現在應該可以點選了。單擊註冊按鈕跳轉到歡迎頁面。注意,歡迎頁面顯示是有一個從底部彈出的動畫。你可以很簡單的實現它。

觀察和分析

  • _showWelcomeScreen() 函式被當成回呼函式在 build() 方法中被呼叫。在 Dart 中你會經常使用回呼函式,在這裡意味著『點選按鈕時調用該方法』。

  • 建構式函式前面的 const 關鍵字至關重要,當 Flutter 遇到一個靜態 widget 時,它就會縮短引擎下的大部分重建工作,從而提高渲染效率。

  • Flutter 中僅存在一個 Navigator 物件。這個 widget 用來管理 Flutter 堆疊中的頁面(也可以被稱為路由 (routes) 或者頁面管理器 (pages))。當前顯示的頁面是堆疊中最上面的頁面,透過往堆疊中 push 新的頁面來切換新的頁面。這也是 _showWelcomeScreen 函式向 Navigator 堆疊中新增 WelcomeScreen 頁面的原因。使用者點選按鈕,然後出現歡迎頁面。同樣,可以透過呼叫 Navigatorpop() 方法來回傳上一個頁面。因為 Flutter 的 navigation 已經整合到瀏覽器的導向中,所以當點選瀏覽器的回傳箭頭也會回傳到上一個頁面。

第 2 步:實現輸入進度監聽

在這個頁面有三個文字框。下一步,我們將實現監聽使用者輸入表單的進度,並且在表單完成後更新應用的 UI 。

  1. 新增一個用於更新進度 _formProgress 屬性的方法。在 _SignUpFormState 類,新增一個名為 _updateFormProgress() 的新方法:

    void _updateFormProgress() {
      var progress = 0.0;
      final controllers = [
        _firstNameTextController,
        _lastNameTextController,
        _usernameTextController
      ];
    
      for (final controller in controllers) {
        if (controller.value.text.isNotEmpty) {
          progress += 1 / controllers.length;
        }
      }
    
      setState(() {
        _formProgress = progress;
      });
    }

    這個方法根據非空輸入框的數量來更新 _formProgress 屬性。

  2. 表單改變時調用 _updateFormProgress 方法。
    _SignUpFormState 類別的 build() 方法中,為 Form widget 的 onChanged 引數新增回呼函式。注意註解為 NEW 的那行新新增的程式碼:

    return Form(
      onChanged: _updateFormProgress, // NEW
      child: Column(
  3. 再次更改按鈕的 onPressed 屬性。
    還記得我們在第一步中,我們透過修改 onPressed 屬性實現了點選 Sign up 按鈕跳轉到歡迎頁面嗎?現在,將它改成只有完成表單輸入時才可以點選按鈕跳轉到歡迎頁面。

    TextButton(
      style: ButtonStyle(
        foregroundColor: MaterialStateProperty.resolveWith((states) {
          return states.contains(MaterialState.disabled)
              ? null
              : Colors.white;
        }),
        backgroundColor: MaterialStateProperty.resolveWith((states) {
          return states.contains(MaterialState.disabled)
              ? null
              : Colors.blue;
        }),
      ),
      onPressed:
          _formProgress == 1 ? _showWelcomeScreen : null, // UPDATED
      child: const Text('Sign up'),
    ),
  4. 執行應用。
    剛開啟頁面時 Sign up 按鈕是停用狀態,當為三個欄位輸入內容(任意內容)時將會變成可點選狀態。

觀察和分析

  • 呼叫 widget 的 setState() 方法通知 Flutter 頁面上的 widget 需要重新建立。框架將銷燬之前的不可變 widget (上面說過 widget 一旦建立不可更改)(包含它的子級 widget),然後建立一個新的 widget (包含他的子級 widget 樹)並將新的 widget 渲染到頁面上。為了使應用執行順暢, Flutter 需要快速的銷燬和建立 widget。新創建的 widget 必須在不到 1/60 秒的時間渲染到頁面上,才能建立一個流暢的動畫效果。幸運的是 Flutter 就是這麼快。當然如果你願意的話,也可以使用文字編輯器。

  • progress 屬性定義為浮點值,並在 _updateFormProgress 方法中更新。當三個輸入框都被輸入後, _formProgress 設定為 1.0 。當 _formProgress 設定為 1.0 後, onPressed 的回呼函式將設定為 _showWelcomeScreen 方法。當 onPressed 引數變為非空時按鈕將會變成可點選。所有的 TextButtononPressedonLongPress 回呼為空時,預設也是無法點選的,與 Flutter 中其他 Material Design 的按鈕一致。

  • 請注意, _updateFormProgress 是透過傳遞一個函式呼叫 setState() 。這種被稱為匿名函式,語法如下所示:

    methodName(() {...});
    

    名為 methodName 的函式把匿名回呼函式作為引數。

  • 最後一步顯示歡迎頁面的 Dart 語法如下所示:

    _formProgress == 1 ? _showWelcomeScreen : null

    Dart 三目運算語法如下: condition ? expression1 : expression2 。如果 _formProgress == 1 是正確的,則會取 : 左側的值,在這個範例中會取 _showWelcomeScreen 方法。

第 2.5 步:啟動 Dart 開發者工具

如何除錯 Flutter Web 應用?所有的 Flutter 應用除錯方法沒有很大的區別。你應該使用 Dart DevTools!(不要和 Chrome 開發者工具搞混淆了)

雖然我們的應用現在沒有 bug ,但是我們依然來驗證一下。下面的指引講明瞭 DevTools 使用的場景,如果你使用的是 IntelliJ 編輯器則會有更好的方式。可以透過檢視文件末尾的提示訊息獲取更多的訊息。

  1. 執行應用。
    如果應用未啟動,啟動應用。從下拉選項中選擇 Chrome 裝置然後使用 IDE 啟動,或者在指令行中使用 flutter run -d chrome

  2. 獲取開發者工具(DevTools)的 socket 訊息。
    在指令行或者 IDE 中你應該可以看下如下所示內容的訊息:

    Launching lib/main.dart on Chrome in debug mode...
    Building application for the web...                                11.7s
    Attempting to connect to browser instance..
    Debug service listening on ws://127.0.0.1:54998/pJqWWxNv92s=
    

    複製粗體顯示的除錯服務的地址,你可以用這個地址啟動 DevTools 。

  3. 確認開發工具已被安裝。
    你是否 已經安裝 DevTools 了呢?如果你使用的是編輯器 (IDE) ,先確認已經用 VS CodeAndroid Studio and IntelliJ 文件描述的方式安裝 Flutter 和 Dart 外掛。如果你使用的是指令行的方式,用 DevTools command line 文件說明的方式啟動開發者工具服務(DevTools server)。

  4. 連線到 DevTools。
    當 DevTools 啟動時,你應該會看到如下類似的內容:

    Serving DevTools at http://127.0.0.1:9100
    

    在 Chrome 瀏覽器中開啟上面 URL,你應該可以看到 DevTools 執行頁面。如下所示:

    Screenshot of the DevTools launch screen

  5. 連線到執行的應用。
    Connect to a running site 下面貼上你在上面第 2 步中複製的 ws 地址,然後點選連線。現在你應該可以看到 Dart DevTools 成功的執行在你的 Chrome 瀏覽器中,如下所示:

    Screenshot of DevTools running screen

    恭喜,你已經成功執行 Dart 開發者工具!

  1. 設定斷點。
    現在你以前啟動了開發者工具,在上面的藍色工具欄中選擇 Debugger 選項。在左下角出現除錯面板,可以檢視範例中使用的類別庫。選擇 lib/main.dart 將在頁面中間顯示 Dart 程式碼。

    Screenshot of the DevTools debugger

  2. 設定斷點。
    在 Dart 程式碼中,向下拉找到被修改的 progress,如下所示:

    for (final controller in controllers) {
      if (controller.value.text.isNotEmpty) {
        progress += 1 / controllers.length;
      }
    }

    在 for 迴圈行的行數前面單擊設定斷點。這個斷點將顯示在視窗左側的 Breakpoints 欄中。

  3. 觸發斷點。
    在正在執行的應用中,點選任意一個輸入框獲取焦點。應用會遇到斷點並暫停。在開發者工具頁面,你可以在左側看到 progress 的值是 0 。這是正常的,因為你沒有輸入任何內容,遍歷 for 迴圈觀察應用的執行。

  4. 恢復應用程式。
    在開發者工具視窗點選綠色的 Resume 按鈕來恢復應用程式。

  5. 刪除斷點。
    再次點選斷點來刪除斷點和恢復程式。

這裡只是粗略的介紹開發者工具的使用方式,還有更多沒有講到。請參考 DevTools 文件 學習更多的內容。

第3步:為輸入進度新增動畫效果

是時候新增動畫效果了!在最後一步,我們將在登入區域上方建立一個進度條動畫,特效如下所述:

  • 剛啟動時,登入區域的頂部顯示一條紅色的進度條。

  • 當一個文字框被鍵入內容時,進度條從紅色變成橙色,並且進度條前進到距登入區域頂部 1/3 的位置。

  • 當第二個文字框被鍵入內容時,進度條從橙色變為黃色,並且進度條前進到距登入區域頂部 2/3 的位置。

  • 當三個文字框全部被輸入內容時,進度條從橙色變成綠色,並且逐漸充滿整個登入區域頂部。除此之外, Sign up 按鈕的狀態也變成可點選。

  1. 新增進度條動畫效果 (AnimatedProgressIndicator)
    在檔案的下面,新增下面的 widget:

    class AnimatedProgressIndicator extends StatefulWidget {
      final double value;
    
      const AnimatedProgressIndicator({
        super.key,
        required this.value,
      });
    
      @override
      State<AnimatedProgressIndicator> createState() {
        return _AnimatedProgressIndicatorState();
      }
    }
    
    class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
        with SingleTickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<Color?> _colorAnimation;
      late Animation<double> _curveAnimation;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
          duration: const Duration(milliseconds: 1200),
          vsync: this,
        );
    
        final colorTween = TweenSequence([
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.red, end: Colors.orange),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.yellow, end: Colors.green),
            weight: 1,
          ),
        ]);
    
        _colorAnimation = _controller.drive(colorTween);
        _curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
      }
    
      @override
      void didUpdateWidget(oldWidget) {
        super.didUpdateWidget(oldWidget);
        _controller.animateTo(widget.value);
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (context, child) => LinearProgressIndicator(
            value: _curveAnimation.value,
            valueColor: _colorAnimation,
            backgroundColor: _colorAnimation.value?.withOpacity(0.4),
          ),
        );
      }
    }

    didUpdateWidget 方法會在 AnimatedProgressIndicator 變化時更新 AnimatedProgressIndicatorState

  2. 使用新的進度條。
    然後,使用新的 AnimatedProgressIndicator widget 替換表單中的 LinearProgressIndicator widget,如下所示:

    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        AnimatedProgressIndicator(value: _formProgress), // NEW
        Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
        Padding(

    該 widget 使用 AnimatedBuilder 為最新值實現了進度的動畫顯示。

  3. 執行應用。
    在三個輸入框中輸入任意值來驗證動畫效果是否正常顯示,然後點選 Sign up 按鈕將彈出歡迎頁面。

完整的範例

import 'package:flutter/material.dart';

void main() => runApp(const SignUpApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (context) => const SignUpScreen(),
        '/welcome': (context) => const WelcomeScreen(),
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[200],
      body: const Center(
        child: SizedBox(
          width: 400,
          child: Card(
            child: SignUpForm(),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          'Welcome!',
          style: Theme.of(context).textTheme.displayMedium,
        ),
      ),
    );
  }
}

class SignUpForm extends StatefulWidget {
  const SignUpForm({super.key});

  @override
  State<SignUpForm> createState() => _SignUpFormState();
}

class _SignUpFormState extends State<SignUpForm> {
  final _firstNameTextController = TextEditingController();
  final _lastNameTextController = TextEditingController();
  final _usernameTextController = TextEditingController();

  double _formProgress = 0;

  void _updateFormProgress() {
    var progress = 0.0;
    final controllers = [
      _firstNameTextController,
      _lastNameTextController,
      _usernameTextController
    ];

    for (final controller in controllers) {
      if (controller.value.text.isNotEmpty) {
        progress += 1 / controllers.length;
      }
    }

    setState(() {
      _formProgress = progress;
    });
  }

  void _showWelcomeScreen() {
    Navigator.of(context).pushNamed('/welcome');
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      onChanged: _updateFormProgress,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedProgressIndicator(value: _formProgress),
          Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
          Padding(
            padding: const EdgeInsets.all(8),
            child: TextFormField(
              controller: _firstNameTextController,
              decoration: const InputDecoration(hintText: 'First name'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: TextFormField(
              controller: _lastNameTextController,
              decoration: const InputDecoration(hintText: 'Last name'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: TextFormField(
              controller: _usernameTextController,
              decoration: const InputDecoration(hintText: 'Username'),
            ),
          ),
          TextButton(
            style: ButtonStyle(
              foregroundColor: MaterialStateProperty.resolveWith((states) {
                return states.contains(MaterialState.disabled)
                    ? null
                    : Colors.white;
              }),
              backgroundColor: MaterialStateProperty.resolveWith((states) {
                return states.contains(MaterialState.disabled)
                    ? null
                    : Colors.blue;
              }),
            ),
            onPressed: _formProgress == 1 ? _showWelcomeScreen : null,
            child: const Text('Sign up'),
          ),
        ],
      ),
    );
  }
}

class AnimatedProgressIndicator extends StatefulWidget {
  final double value;

  const AnimatedProgressIndicator({
    super.key,
    required this.value,
  });

  @override
  State<AnimatedProgressIndicator> createState() {
    return _AnimatedProgressIndicatorState();
  }
}

class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _curveAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1200),
      vsync: this,
    );

    final colorTween = TweenSequence([
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.red, end: Colors.orange),
        weight: 1,
      ),
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
        weight: 1,
      ),
      TweenSequenceItem(
        tween: ColorTween(begin: Colors.yellow, end: Colors.green),
        weight: 1,
      ),
    ]);

    _colorAnimation = _controller.drive(colorTween);
    _curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
  }

  @override
  void didUpdateWidget(oldWidget) {
    super.didUpdateWidget(oldWidget);
    _controller.animateTo(widget.value);
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) => LinearProgressIndicator(
        value: _curveAnimation.value,
        valueColor: _colorAnimation,
        backgroundColor: _colorAnimation.value?.withOpacity(0.4),
      ),
    );
  }
}

觀察和分析

  • 你可以使用 AnimationController 控制任何動畫效果。

  • Animation 的值改變時 AnimatedBuilder 將重新建立 widget 樹。

  • 使用動畫 Tween ,你還可以使用很多值,像這個範例中的 Color

下一步,我們該做什麼?

恭喜!你已經使用 Flutter 建立了第一個 Web 應用!

如果你想繼續完善這個範例,或許你可以新增表單驗證。如何繼續的建議,請參考 Flutter cookbook 中的 Building a form with validation

有關 Web 應用、Dart 開發者工具以及 Flutter 動畫的更多訊息,請參考下面文件: