跳至主要內容

在 Web 上撰寫您的第一個 Flutter 應用程式

The web app that you'll be building.

這是建立您的第一個 Flutter Web 應用程式的指南。如果您熟悉物件導向程式設計,以及變數、迴圈和條件等概念,您可以完成本教學課程。您不需要具備 Dart、行動裝置或 Web 程式設計的先前經驗。

您將建立的內容

#

您將實作一個簡單的 Web 應用程式,它會顯示登入畫面。該畫面包含三個文字欄位:名字、姓氏和使用者名稱。當使用者填寫欄位時,進度列會在登入區域的頂部產生動畫。當所有三個欄位都填寫完畢時,進度列會以綠色顯示在登入區域的完整寬度上,且 註冊 按鈕會啟用。按一下 註冊 按鈕會使歡迎畫面從畫面底部產生動畫進入。

動畫 GIF 顯示此實驗室完成時應用程式的運作方式。

步驟 0:取得起始 Web 應用程式

#

您將從我們為您提供的簡單 Web 應用程式開始。

  1. 啟用 Web 開發。
    在命令列中,執行下列命令以確保您已正確安裝 Flutter。
    flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.24.5, on macOS darwin-arm64, locale en)
    [✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    [✓] Xcode - develop for iOS and macOS (Xcode 16)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2024.2)
    [✓] VS Code (version 1.95)
    [✓] 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 裝置會自動啟動 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: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.white;
                  }),
                  backgroundColor: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.blue;
                  }),
                ),
                onPressed: null,
                child: const Text('Sign up'),
              ),
            ],
          ),
        );
      }
    }
    
  4. 執行範例。
    按一下 執行 按鈕來執行範例。請注意,您可以在文字欄位中輸入文字,但 註冊 按鈕已停用。

  5. 複製程式碼。
    按一下程式碼窗格右上角的剪貼簿圖示,將 Dart 程式碼複製到剪貼簿。

  6. 建立新的 Flutter 專案。
    從您的 IDE、編輯器或在命令列中,建立新的 Flutter 專案,並將其命名為 signin_example

  7. lib/main.dart 的內容替換為剪貼簿的內容。

觀察

#
  • 此範例的整個程式碼都位於 lib/main.dart 檔案中。
  • 如果您知道 Java,Dart 語言應該會非常熟悉。
  • 應用程式的所有 UI 都是在 Dart 程式碼中建立的。如需更多資訊,請參閱宣告式 UI 簡介
  • 應用程式的 UI 符合Material Design,這是一種在任何裝置或平台上運作的視覺設計語言。您可以自訂 Material Design widget,但如果您偏好其他東西,Flutter 也提供 Cupertino widget 程式庫,它實作了目前的 iOS 設計語言。或者,您可以建立自己的自訂 widget 程式庫。
  • 在 Flutter 中,幾乎所有東西都是 Widget。甚至應用程式本身也是一個 widget。應用程式的 UI 可以描述為 widget 樹狀結構。

步驟 1:顯示歡迎畫面

#

SignUpForm 類別是一個狀態ful 的 widget。這僅表示 widget 會儲存可能會變更的資訊,例如使用者輸入或來自來源的資料。由於 widget 本身是不可變的(一旦建立就無法修改),Flutter 會將狀態資訊儲存在一個稱為 State 類別的 companion 類別中。在本實驗室中,您所有的編輯都會在私有的 _SignUpFormState 類別中進行。

首先,在您的 lib/main.dart 檔案中,在 SignUpScreen 類別之後新增 WelcomeScreen widget 的下列類別定義

dart
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() 方法。這是建置註冊按鈕的程式碼部分。請注意按鈕的定義方式:它是一個 TextButton,具有藍色背景、白色文字,上面寫著 註冊,而且在按下時不會執行任何動作。

  2. 更新 onPressed 屬性。
    變更 onPressed 屬性以呼叫(不存在的)方法,該方法將顯示歡迎畫面。

    onPressed: null 變更為下列內容

    dart
    onPressed: _showWelcomeScreen,
  3. 新增 _showWelcomeScreen 方法。
    修正分析器回報的錯誤,即未定義 _showWelcomeScreen。直接在 build() 方法上方,新增下列函式

    dart
    void _showWelcomeScreen() {
      Navigator.of(context).pushNamed('/welcome');
    }
  4. 新增 /welcome 路由。
    建立連線以顯示新畫面。在 SignUpAppbuild() 方法中,在 '/' 下方新增下列路由

    dart
    '/welcome': (context) => const WelcomeScreen(),
  5. 執行應用程式。
    註冊 按鈕現在應該已啟用。按一下它以顯示歡迎畫面。請注意它是如何從底部產生動畫進入。您可以免費獲得該行為。

觀察

#
  • _showWelcomeScreen() 函式在 build() 方法中用作回呼函式。回呼函式通常在 Dart 程式碼中使用,在本例中,這表示「按下按鈕時呼叫此方法」。
  • 建構子前面的 const 關鍵字非常重要。當 Flutter 遇到常數 widget 時,它會在幕後簡化大部分的重建工作,使渲染更有效率。
  • Flutter 只有一個 Navigator 物件。此 widget 管理 Flutter 的畫面(也稱為路由頁面)在堆疊內部。堆疊頂端的畫面是目前顯示的檢視。將新畫面推送至此堆疊會將顯示切換到該新畫面。這就是為什麼 _showWelcomeScreen 函式會將 WelcomeScreen 推送至 Navigator 的堆疊。使用者按一下按鈕,然後,瞧,歡迎畫面出現。同樣地,在 Navigator 上呼叫 pop() 會返回上一個畫面。因為 Flutter 的導覽已整合到瀏覽器的導覽中,所以當按一下瀏覽器的返回箭頭按鈕時,這會隱含地發生。

步驟 2:啟用登入進度追蹤

#

此登入畫面有三個欄位。接下來,您將能夠追蹤使用者填寫表單欄位的進度,並在表單完成時更新應用程式的 UI。

  1. 新增方法以更新 _formProgress。在 _SignUpFormState 類別中,新增一個名為 _updateFormProgress() 的新方法

    dart
    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 的程式碼

    dart
    return Form(
      onChanged: _updateFormProgress, // NEW
      child: Column(
  3. 更新 onPressed 屬性(再次)。
    步驟 1 中,您修改了 註冊 按鈕的 onPressed 屬性以顯示歡迎畫面。現在,更新該按鈕,僅當表單完全填寫時才顯示歡迎畫面

    dart
    TextButton(
      style: ButtonStyle(
        foregroundColor: WidgetStateProperty.resolveWith((states) {
          return states.contains(WidgetState.disabled)
              ? null
              : Colors.white;
        }),
        backgroundColor: WidgetStateProperty.resolveWith((states) {
          return states.contains(WidgetState.disabled)
              ? null
              : Colors.blue;
        }),
      ),
      onPressed:
          _formProgress == 1 ? _showWelcomeScreen : null, // UPDATED
      child: const Text('Sign up'),
    ),
  4. 執行應用程式。
    註冊 按鈕最初會停用,但當所有三個文字欄位都包含(任何)文字時,就會啟用。

觀察

#
  • 呼叫 widget 的 setState() 方法會告知 Flutter widget 需要在畫面上更新。然後,架構會處置先前不可變的 widget(及其子項),建立一個新的 widget(及其隨附的子 widget 樹狀結構),並將其渲染到畫面。為了使此作業無縫運作,Flutter 需要快速。新的 widget 樹狀結構必須在不到 1/60 秒內建立並渲染到畫面,才能建立平滑的視覺轉換,特別是針對動畫。幸好 Flutter 確實很快。

  • progress 欄位定義為浮點值,並在 _updateFormProgress 方法中更新。當所有三個欄位都填寫完畢時,_formProgress 會設定為 1.0。當 _formProgress 設定為 1.0 時,onPressed 回呼會設定為 _showWelcomeScreen 方法。現在其 onPressed 引數為非空值,按鈕已啟用。與 Flutter 中的大多數 Material Design 按鈕一樣,如果 onPressedonLongPress 回呼為空值,則預設會停用 TextButton

  • 請注意,_updateFormProgress 將函式傳遞至 setState()。這稱為匿名函式,並且具有下列語法

    dart
    methodName(() {...});

    其中 methodName 是採用匿名回呼函式作為引數的命名函式。

  • 上一步中顯示歡迎畫面的 Dart 語法為

    dart
    _formProgress == 1 ? _showWelcomeScreen : null

    這是一個 Dart 條件式指派,並且具有下列語法:condition ? expression1 : expression2。如果運算式 _formProgress == 1 為真,則整個運算式的結果為 : 左側的值,在本例中為 _showWelcomeScreen 方法。

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

#

您要如何偵錯 Flutter Web 應用程式?它與偵錯任何 Flutter 應用程式沒有太大的不同。您需要使用Dart 開發者工具! (不要與 Chrome 開發者工具混淆。)

我們的應用程式目前沒有錯誤,但讓我們還是檢查一下。下列啟動開發者工具的指示適用於任何工作流程,但如果您使用 IntelliJ,則有一個捷徑。如需更多資訊,請參閱本節結尾的提示。

  1. 執行應用程式。
    如果您的應用程式目前沒有執行,請啟動它。從下拉選單中選取 Chrome 裝置,並從您的 IDE 啟動它,或者從命令列使用 flutter run -d chrome

  2. 取得 DevTools 的 Web 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 <b>ws://127.0.0.1:54998/pJqWWxNv92s=</b>

    複製以粗體顯示的偵錯服務位址。您需要它來啟動 DevTools。

  3. 確認已安裝 Dart 和 Flutter 外掛程式。
    如果您使用 IDE,請確保您已設定 Flutter 和 Dart 外掛程式,如 VS CodeAndroid Studio 和 IntelliJ 頁面所述。如果您在命令列工作,請如 DevTools 命令列頁面所述啟動 DevTools 伺服器。

  4. 連線到 DevTools。
    當 DevTools 啟動時,您應該會看到類似以下的畫面:

    Serving DevTools at http://127.0.0.1:9100

    在 Chrome 瀏覽器中前往此 URL。您應該會看到 DevTools 啟動畫面。它應該看起來像這樣:

    Screenshot of the DevTools launch screen

  5. 連線到執行中的應用程式。
    連線到執行中的網站下,貼上您在步驟 2 中複製的 Web Socket (ws) 位置,然後按一下連線。您現在應該可以在 Chrome 瀏覽器中成功執行 Dart DevTools 了。

    Screenshot of DevTools running screen

    恭喜,您現在正在執行 Dart DevTools!

  1. 設定一個中斷點。
    現在您已執行 DevTools,請選取頂部藍色橫條中的Debugger索引標籤。偵錯器窗格會出現,在左下角,您會看到範例中使用的程式庫列表。選取 lib/main.dart 以在中央窗格中顯示您的 Dart 程式碼。

    Screenshot of the DevTools debugger

  2. 設定一個中斷點。
    在 Dart 程式碼中,向下捲動到更新 progress 的位置。

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

    透過按一下行號左側,在 for 迴圈的行上放置一個中斷點。中斷點現在會出現在視窗左側的中斷點區段中。

  3. 觸發中斷點。
    在執行中的應用程式中,按一下其中一個文字欄位以取得焦點。應用程式會觸及中斷點並暫停。在 DevTools 畫面中,您可以在左側看到 progress 的值,其為 0。這是預期的,因為沒有任何欄位被填入。逐步執行 for 迴圈以查看程式執行情況。

  4. 繼續執行應用程式。
    按一下 DevTools 視窗中的綠色繼續按鈕來繼續執行應用程式。

  5. 刪除中斷點。
    再次按一下中斷點來刪除它,然後繼續執行應用程式。

這讓您對使用 DevTools 的可能性有了一個小小的了解,但還有更多!如需更多資訊,請參閱 DevTools 文件

步驟 3:為登入進度新增動畫

#

現在是時候新增動畫了!在最後一個步驟中,您將為登入區域頂部的 LinearProgressIndicator 建立動畫。動畫具有以下行為:

  • 當應用程式啟動時,一個小小的紅色長條會出現在登入區域的頂部。
  • 當一個文字欄位包含文字時,紅色長條會變成橘色,並在登入區域的 0.15 處進行動畫。
  • 當兩個文字欄位包含文字時,橘色長條會變成黃色,並在登入區域的一半處進行動畫。
  • 當所有三個文字欄位都包含文字時,橘色長條會變成綠色,並在登入區域的整個寬度上進行動畫。此外,註冊按鈕也會被啟用。
  1. 新增 AnimatedProgressIndicator
    在檔案底部,新增此 widget:

    dart
    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(AnimatedProgressIndicator 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
    然後,將 Form 中的 LinearProgressIndicator 替換為這個新的 AnimatedProgressIndicator

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

    這個 widget 使用 AnimatedBuilder 來將進度指示器動畫化到最新值。

  3. 執行應用程式。
    在三個欄位中輸入任何內容,以驗證動畫是否運作,以及按一下註冊按鈕是否會顯示歡迎畫面。

完整範例

#
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: WidgetStateProperty.resolveWith((states) {
                return states.contains(WidgetState.disabled)
                    ? null
                    : Colors.white;
              }),
              backgroundColor: WidgetStateProperty.resolveWith((states) {
                return states.contains(WidgetState.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(AnimatedProgressIndicator 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 食譜

如需有關 Flutter Web 應用程式、Dart DevTools 或 Flutter 動畫的更多資訊,請參閱以下內容: