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

這是建立您的第一個 Flutter Web 應用程式的指南。如果您熟悉物件導向程式設計,以及變數、迴圈和條件等概念,您可以完成本教學課程。您不需要具備 Dart、行動裝置或 Web 程式設計的先前經驗。
您將建立的內容
#您將實作一個簡單的 Web 應用程式,它會顯示登入畫面。該畫面包含三個文字欄位:名字、姓氏和使用者名稱。當使用者填寫欄位時,進度列會在登入區域的頂部產生動畫。當所有三個欄位都填寫完畢時,進度列會以綠色顯示在登入區域的完整寬度上,且 註冊 按鈕會啟用。按一下 註冊 按鈕會使歡迎畫面從畫面底部產生動畫進入。
動畫 GIF 顯示此實驗室完成時應用程式的運作方式。
步驟 0:取得起始 Web 應用程式
#您將從我們為您提供的簡單 Web 應用程式開始。
- 啟用 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。如果您稍後希望此應用程式在行動裝置上運作,則需要進行額外的安裝和設定。
列出裝置。
為確保 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 開發者工具的使用。
起始應用程式顯示在下列 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'), ), ], ), ); } }
執行範例。
按一下 執行 按鈕來執行範例。請注意,您可以在文字欄位中輸入文字,但 註冊 按鈕已停用。複製程式碼。
按一下程式碼窗格右上角的剪貼簿圖示,將 Dart 程式碼複製到剪貼簿。建立新的 Flutter 專案。
從您的 IDE、編輯器或在命令列中,建立新的 Flutter 專案,並將其命名為signin_example
。將
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 的下列類別定義
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,
),
),
);
}
}
接下來,您將啟用按鈕以顯示畫面並建立方法以顯示它。
找到
_SignUpFormState
類別的build()
方法。這是建置註冊按鈕的程式碼部分。請注意按鈕的定義方式:它是一個TextButton
,具有藍色背景、白色文字,上面寫著 註冊,而且在按下時不會執行任何動作。更新
onPressed
屬性。
變更onPressed
屬性以呼叫(不存在的)方法,該方法將顯示歡迎畫面。將
onPressed: null
變更為下列內容dartonPressed: _showWelcomeScreen,
新增
_showWelcomeScreen
方法。
修正分析器回報的錯誤,即未定義_showWelcomeScreen
。直接在build()
方法上方,新增下列函式dartvoid _showWelcomeScreen() { Navigator.of(context).pushNamed('/welcome'); }
新增
/welcome
路由。
建立連線以顯示新畫面。在SignUpApp
的build()
方法中,在'/'
下方新增下列路由dart'/welcome': (context) => const WelcomeScreen(),
執行應用程式。
註冊 按鈕現在應該已啟用。按一下它以顯示歡迎畫面。請注意它是如何從底部產生動畫進入。您可以免費獲得該行為。
觀察
#_showWelcomeScreen()
函式在build()
方法中用作回呼函式。回呼函式通常在 Dart 程式碼中使用,在本例中,這表示「按下按鈕時呼叫此方法」。- 建構子前面的
const
關鍵字非常重要。當 Flutter 遇到常數 widget 時,它會在幕後簡化大部分的重建工作,使渲染更有效率。 - Flutter 只有一個
Navigator
物件。此 widget 管理 Flutter 的畫面(也稱為路由或頁面)在堆疊內部。堆疊頂端的畫面是目前顯示的檢視。將新畫面推送至此堆疊會將顯示切換到該新畫面。這就是為什麼_showWelcomeScreen
函式會將WelcomeScreen
推送至 Navigator 的堆疊。使用者按一下按鈕,然後,瞧,歡迎畫面出現。同樣地,在Navigator
上呼叫pop()
會返回上一個畫面。因為 Flutter 的導覽已整合到瀏覽器的導覽中,所以當按一下瀏覽器的返回箭頭按鈕時,這會隱含地發生。
步驟 2:啟用登入進度追蹤
#此登入畫面有三個欄位。接下來,您將能夠追蹤使用者填寫表單欄位的進度,並在表單完成時更新應用程式的 UI。
新增方法以更新
_formProgress
。在_SignUpFormState
類別中,新增一個名為_updateFormProgress()
的新方法dartvoid _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
欄位。當表單變更時呼叫
_updateFormProgress
。
在_SignUpFormState
類別的build()
方法中,將回呼新增至Form
widget 的onChanged
引數。新增下方標示為 NEW 的程式碼dartreturn Form( onChanged: _updateFormProgress, // NEW child: Column(
更新
onPressed
屬性(再次)。
在步驟 1
中,您修改了 註冊 按鈕的onPressed
屬性以顯示歡迎畫面。現在,更新該按鈕,僅當表單完全填寫時才顯示歡迎畫面dartTextButton( 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'), ),
執行應用程式。
註冊 按鈕最初會停用,但當所有三個文字欄位都包含(任何)文字時,就會啟用。
觀察
#呼叫 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 按鈕一樣,如果onPressed
和onLongPress
回呼為空值,則預設會停用 TextButton。請注意,
_updateFormProgress
將函式傳遞至setState()
。這稱為匿名函式,並且具有下列語法dartmethodName(() {...});
其中
methodName
是採用匿名回呼函式作為引數的命名函式。上一步中顯示歡迎畫面的 Dart 語法為
dart_formProgress == 1 ? _showWelcomeScreen : null
這是一個 Dart 條件式指派,並且具有下列語法:
condition ? expression1 : expression2
。如果運算式_formProgress == 1
為真,則整個運算式的結果為:
左側的值,在本例中為_showWelcomeScreen
方法。
步驟 2.5:啟動 Dart 開發者工具
#您要如何偵錯 Flutter Web 應用程式?它與偵錯任何 Flutter 應用程式沒有太大的不同。您需要使用Dart 開發者工具! (不要與 Chrome 開發者工具混淆。)
我們的應用程式目前沒有錯誤,但讓我們還是檢查一下。下列啟動開發者工具的指示適用於任何工作流程,但如果您使用 IntelliJ,則有一個捷徑。如需更多資訊,請參閱本節結尾的提示。
執行應用程式。
如果您的應用程式目前沒有執行,請啟動它。從下拉選單中選取 Chrome 裝置,並從您的 IDE 啟動它,或者從命令列使用flutter run -d chrome
。取得 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。
確認已安裝 Dart 和 Flutter 外掛程式。
如果您使用 IDE,請確保您已設定 Flutter 和 Dart 外掛程式,如 VS Code 和 Android Studio 和 IntelliJ 頁面所述。如果您在命令列工作,請如 DevTools 命令列頁面所述啟動 DevTools 伺服器。連線到 DevTools。
當 DevTools 啟動時,您應該會看到類似以下的畫面:Serving DevTools at http://127.0.0.1:9100
在 Chrome 瀏覽器中前往此 URL。您應該會看到 DevTools 啟動畫面。它應該看起來像這樣:
連線到執行中的應用程式。
在連線到執行中的網站下,貼上您在步驟 2 中複製的 Web Socket (ws) 位置,然後按一下連線。您現在應該可以在 Chrome 瀏覽器中成功執行 Dart DevTools 了。恭喜,您現在正在執行 Dart DevTools!
設定一個中斷點。
現在您已執行 DevTools,請選取頂部藍色橫條中的Debugger索引標籤。偵錯器窗格會出現,在左下角,您會看到範例中使用的程式庫列表。選取lib/main.dart
以在中央窗格中顯示您的 Dart 程式碼。設定一個中斷點。
在 Dart 程式碼中,向下捲動到更新progress
的位置。dartfor (final controller in controllers) { if (controller.value.text.isNotEmpty) { progress += 1 / controllers.length; } }
透過按一下行號左側,在 for 迴圈的行上放置一個中斷點。中斷點現在會出現在視窗左側的中斷點區段中。
觸發中斷點。
在執行中的應用程式中,按一下其中一個文字欄位以取得焦點。應用程式會觸及中斷點並暫停。在 DevTools 畫面中,您可以在左側看到progress
的值,其為 0。這是預期的,因為沒有任何欄位被填入。逐步執行 for 迴圈以查看程式執行情況。繼續執行應用程式。
按一下 DevTools 視窗中的綠色繼續按鈕來繼續執行應用程式。刪除中斷點。
再次按一下中斷點來刪除它,然後繼續執行應用程式。
這讓您對使用 DevTools 的可能性有了一個小小的了解,但還有更多!如需更多資訊,請參閱 DevTools 文件。
步驟 3:為登入進度新增動畫
#現在是時候新增動畫了!在最後一個步驟中,您將為登入區域頂部的 LinearProgressIndicator
建立動畫。動畫具有以下行為:
- 當應用程式啟動時,一個小小的紅色長條會出現在登入區域的頂部。
- 當一個文字欄位包含文字時,紅色長條會變成橘色,並在登入區域的 0.15 處進行動畫。
- 當兩個文字欄位包含文字時,橘色長條會變成黃色,並在登入區域的一半處進行動畫。
- 當所有三個文字欄位都包含文字時,橘色長條會變成綠色,並在登入區域的整個寬度上進行動畫。此外,註冊按鈕也會被啟用。
新增
AnimatedProgressIndicator
。
在檔案底部,新增此 widget:dartclass 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
。使用新的
AnimatedProgressIndicator
。
然後,將Form
中的LinearProgressIndicator
替換為這個新的AnimatedProgressIndicator
:dartchild: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedProgressIndicator(value: _formProgress), // NEW Text('Sign up', style: Theme.of(context).textTheme.headlineMedium), Padding(
這個 widget 使用
AnimatedBuilder
來將進度指示器動畫化到最新值。執行應用程式。
在三個欄位中輸入任何內容,以驗證動畫是否運作,以及按一下註冊按鈕是否會顯示歡迎畫面。
完整範例
#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 動畫的更多資訊,請參閱以下內容:
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-10-17。 檢視原始碼 或 回報問題。