一、創建Flutter項目
創建Flutter項目有兩種方式:通過命令行創建 和 通過開發工具創建
-
1.1、在終端通過命令行創建(全新的APP)
flutter create 項目的名字
提示:
- Flutter的名稱不要包含特殊的字符,另外不可以使用駝峰標識(不要使用大寫),名字很長可以使用
下劃線連接
- 創建完之后使用自己喜歡的開發工具打開即可,比如:
Android Sdudio
、VSCode
創建擴展: 語言的設置
flutter create -i swift -a kotlin 項目的名字
flutter create -i swift -a java 項目的名字
flutter create -i objc -a kottlin 項目的名字
flutter create -i objc -a java 項目的名字
2>、混合項目的創建(混編到已有的
Android
/iOS
工程內)flutter create --template module 組件的名字
3>、Flutter Plugin的創建(Flutter平臺插件工程,包含Dart層與Native平臺層的實現)
flutter create --template=plugin xxapp_plugin
4>、Flutter Package(Flutter純Dart插件工程,僅包含Dart層的實現,往往定義一些公共Widget)
flutter create --template=package xxapp_package
- Flutter的名稱不要包含特殊的字符,另外不可以使用駝峰標識(不要使用大寫),名字很長可以使用
-
1.2、目錄結構概述
flutter 目錄結構概述- dart_tool:記錄一些東西所在的位置以及它的一些版本信息,不需要我們去配置,不要手動去修改
- idea: 記錄當前項目的一些配置的
- iml:也是對記錄對當前項目的配置
- Android:安卓的代碼
- build:項目的構建輸出目錄
- iOS:iOS的代碼
- lib:flutter 源碼的文件夾
- test:主要是用來做一些測試的
- gitignore:記錄當前的一些忽略的
- metadate:對flutter版本的一些記錄,不需要手動去改
- packages、pubspec.lock 都是導入到三方后生成的
- pubspec.yaml:項目依賴配置文件類似于RN的
package.json
-
1.3、
Flutter Hot Reload
熱重載 和Flutter Hot Restart
熱重啟 的區別
Flutter Hot Reload 熱重載 和 Flutter Hot Restart 熱重啟- Flutter Hot Reload 熱重載:主要是執行 build 方法
- Flutter Hot Restart 熱重啟:重新運行整個app(熱啟動)
提示:冷啟動是從未啟動過app
二、Flutter代碼展示,看不懂沒關系,這篇博客僅僅是展示,下篇才開始學習
-
2.1、Hello World的實現
我們在main.dart
里面進行從 0 到 1 編寫代碼import 'package:flutter/material.dart'; main(List<String> args) { runApp(Text("Hello World", textDirection: TextDirection.ltr)); }
當然,上面的代碼我們已經實現了在界面上顯示Hello World:
但是沒有居中,字體也有點小;
這些問題,我們放到后面再來解決,先搞懂目前的幾行代碼;
上面的代碼我們有一些比較熟悉,有一些并不清楚是什么:
比如我們知道Dart程序的入口都是main函數,而Flutter是Dart編寫的,所以入口也是main函數;
但是我們導入的Material是什么呢?
另外,我們在main函數中調用了一個runApp()函數
又是什么呢?
下面,我們對不認識的代碼進行一些分析。 -
2.2、代碼分析
-
1>、runApp 和 Widget
runApp是Flutter內部提供的一個函數,當我們啟動一個Flutter應用程序時就是從調用這個函數開始的我們可以點到runApp的源碼,查看到該函數
-
我們暫時不分析具體的源碼(因為我發現過多的理論,對于初學者來說并不友好)
void runApp(Widget app) { ...省略代碼 }
該函數讓我們傳入一個東西:
Widget
?
我們先說Widget的翻譯:- Widget在國內有很多的翻譯;
- 做過Android、iOS等開發的人群,喜歡將它翻譯成控件;
- 做過Vue、React等開發的人群,喜歡將它翻譯成組件;
- 如果我們使用Google,Widget翻譯過來應該是小部件;
- 沒有說哪種翻譯一定是對的,或者一定是錯的,但是我個人更傾向于小部件或者組件;
- Widget到底什么東西呢?
- 我們學習Flutter,從一開始就可以有一個基本的認識:Flutter中萬物皆Widget(萬物皆可盤);
- 在我們iOS或者Android開發中,我們的界面有很多種類的劃分:應用(Application)、視圖控制器(View Controller)、活動(Activity)、View(視圖)、Button(按鈕)等等;
- 但是在Flutter中,這些東西都是不同的Widget而已;
- 也就是我們整個應用程序中所看到的內容幾乎都是Widget,甚至是內邊距的設置,我們也需要使用一個叫Padding的Widget來做;
- runApp函數讓我們傳入的就是一個Widget:
- 但是我們現在沒有Widget,怎么辦呢?
- 我們可以導入Flutter默認已經給我們提供的Material庫,來使用其中的很多內置Widget;
- Widget在國內有很多的翻譯;
-
2>、Material 設計風格
-
material是什么呢?
- material是Google公司推行的一套設計風格,或者叫設計語言、設計規范等;
- 里面有非常多的設計規范,比如顏色、文字的排版、響應動畫與過度、填充等等;
- 在Flutter中高度集成了Material風格的Widget;
- 在我們的應用中,我們可以直接使用這些Widget來創建我們的應用(后面會用到很多);
-
Text小部件分析:
我們可以使用Text小部件來完成文字的顯示;
我們發現Text小部件繼承自StatelessWidget,StatelessWidget繼承自Widget;
所以我們可以將Text小部件傳入到runApp函數中
-
屬性非常多,但是我們已經學習了Dart語法,所以你會發現只有this.data屬性是必須傳入的。
class Text extends StatelessWidget { const Text( this.data, { Key key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.textScaleFactor, this.maxLines, this.semanticsLabel, this.textWidthBasis, }); }
-
StatelessWidget簡單介紹:
StatelessWidget繼承自Widget;
-
后面我會更加詳細的介紹它的用法;
abstract class StatelessWidget extends Widget { // ...省略代碼 }
-
-
-
2.3、代碼改進
-
1>、改進界面樣式
我們可能希望文字居中顯示,并且可以大一些;
居中顯示
: 需要使用另外一個Widget,Center;
文字大一些
: 需要給Text文本設置一些樣式;
我們修改代碼如下:
我們在Text小部件外層包裝了一個Center部件,讓Text作為其child;
并且,我們給Text組件設置了一個屬性:style,對應的值是TextStyle類型;import 'package:flutter/material.dart'; main(List<String> args) { runApp(Center( child: Text( "Hello World", textDirection: TextDirection.ltr, style: TextStyle(fontSize: 20), ), ) ); }
-
2>、改進界面結構
目前我們雖然可以顯示HelloWorld,但是我們發現最底部的背景是黑色,并且我們的頁面并不夠結構化。
正常的App頁面應該有一定的結構,比如通常都會有導航欄
,會有一些背景顏色
等
在開發當中,我們并不需要從零去搭建這種結構化的界面,我們可以使用Material庫,直接使用其中的一些封裝好的組件來完成一些結構的搭建。
我們通過下面的代碼來實現:import 'package:flutter/material.dart'; main(List<String> args) { runApp( MaterialApp( home: Scaffold( appBar: AppBar( title: Text("CODERWHY"), ), body: Center( child: Text( "Hello World", textDirection: TextDirection.ltr, style: TextStyle(fontSize: 36), ), ), ), ) ); }
- 在最外層包裹一個MaterialApp
- 這意味著整個應用我們都會采用MaterialApp風格的一些東西,方便我們對應用的設計,并且目前我們使用了其中兩個屬性;
- title:這個是定義在Android系統中打開多任務切換窗口時顯示的標題;(暫時可以不寫)
- home:是該應用啟動時顯示的頁面,我們傳入了一個Scaffold;
- Scaffold是什么呢?
- 翻譯過來是腳手架,腳手架的作用就是搭建頁面的基本結構;
- 所以我們給MaterialApp的home屬性傳入了一個Scaffold對象,作為啟動顯示的Widget;
- Scaffold也有一些屬性,比如appBar和body;
- appBar是用于設計導航欄的,我們傳入了一個title屬性;
- body是頁面的內容部分,我們傳入了之前已經創建好的Center中包裹的一個Text的Widget;
- 在最外層包裹一個MaterialApp
-
-
2.4、代碼重構
-
1>、創建自己的Widget
很多學習Flutter的人,都會被Flutter的嵌套勸退,當代碼嵌套過多時,結構很容易看不清晰。- 這里有兩點我先說明一下:
- 1、Flutter整個開發過程中就是形成一個Widget樹,所以形成嵌套是很正常的。
- 2、關于Flutter的代碼縮進,更多開發中我們使用的是2個空格(前端開發2個空格居多,你喜歡4個也沒問題)
- 但是,我們開發一個這么簡單的程序就出現如此多的嵌套,如果應用程序更復雜呢?
- 我們可以對我們的代碼進行封裝,將它們封裝到自己的Widget中,創建自己的Widget;
- 如何創建自己的Widget呢?
- 在Flutter開發中,我們可以繼承自StatelessWidget或者StatefulWidget來創建自己的Widget類;
- StatelessWidget: 沒有狀態改變的Widget,通常這種Widget僅僅是做一些展示工作而已;
- StatefulWidget: 需要保存狀態,并且可能出現狀態改變的Widget;
在上面的案例中對代碼的重構,我們使用StatelessWidget即可,所以我們接下來學習一下如果利用StatelessWidget來對我們的代碼進行重構;
StatefulWidget我們放到后面的一個案例中來學習; - 這里有兩點我先說明一下:
-
2>、StatelessWidget
- StatelessWidget通常是一些沒有狀態(State,也可以理解成data)需要維護的Widget:
- 它們的數據通常是直接寫死(放在Widget中的數據,必須被定義為final,為什么呢?我在下一個章節講解StatefulWidget會講到);
- 從parent widget中傳入的而且一旦傳入就不可以修改;
- 從InheritedWidget獲取來使用的數據(這個放到后面會講解);
- 我們來看一下創建一個StatelessWidget的格式:
讓自己創建的Widget繼承自StatelessWidget;
-
StatelessWidget包含一個必須重寫的方法:build方法;
class MyStatelessWidget extends StatelessWidget { @override Widget build(BuildContext context) { return <返回我們的Widget要渲染的Widget,比如一個Text Widget>; } }
- build方法的解析:
- Flutter在拿到我們自己創建的StatelessWidget時,就會執行它的build方法;
- 我們需要在build方法中告訴Flutter,我們的Widget希望渲染什么元素,比如一個Text Widget;
- StatelessWidget沒辦法主動去執行build方法,當我們使用的數據發生改變時,build方法會被重新執行;
-
build方法什么情況下被執行呢 ?
- 當我們的StatelessWidget第一次被插入到Widget樹中時(也就是第一次被創建時);
- 當我們的父Widget(parent widget)發生改變時,子Widget會被重新構建;
- 如果我們的Widget依賴InheritedWidget的一些數據,InheritedWidget數據發生改變時;
- StatelessWidget通常是一些沒有狀態(State,也可以理解成data)需要維護的Widget:
-
3>、重構案例代碼
現在我們就可以通過StatelessWidget來對我們的代碼進行重構了
- 因為我們的整個代碼都是一些數據展示,沒有數據的改變,使用StatelessWidget即可;
- 另外,為了體現更好的封裝性,我對代碼進行了兩層的拆分,讓代碼結構看起來更加清晰;(具體的拆分方式,我會在后面的案例中不斷的體現出來,目前我們先拆分兩層)
重構后的代碼如下:
import 'package:flutter/material.dart'; main(List<String> args) { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text("CODERWHY"), ), body: HomeContent(), ), ) } } class HomeContent extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Checkbox( value: true, onChanged: (value) => print("Hello World") ), Text( "同意協議", textDirection: TextDirection.ltr, style: TextStyle(fontSize: 20), ) ], ), ); } }
-
-
2.5、StateFulWidget (有狀態的)
在開發中,某些Widget情況下我們展示的數據并不是一層不變的:
比如Flutter默認程序中的計數器案例,點擊了+號按鈕后,顯示的數字需要+1;
比如在開發中,我們會進行下拉刷新、上拉加載更多,這時數據也會發生變化;
而StatelessWidget通常用來展示哪些數據固定不變的,如果數據會發生改變,我們使用StatefulWidget;-
1>、StatefulWidget介紹
為什么選擇StatefulWidget呢?
在示例代碼中,當我們點擊按鈕時,界面上顯示的數據會發生改變;
這時,我們需要一個變量來記錄當前的狀態,再把這個變量顯示到某個Text Widget上;
并且每次變量發生改變時,我們對應的Text上顯示的內容也要發生改變;
但是有一個問題,我之前說過定義到Widget中的數據都是不可變的,必須定義為final,為什么呢?
這次因為Flutter在設計的時候就決定了一旦Widget中展示的數據發生變化,就重新構建整個Widget;Flutter通過一些機制來限定定義到Widget中的成員變量
必須是final
的;-
Flutter如何做到我們在開發中定義到Widget中的數據一定是final的呢?,我們來看一下Widget的源碼
@immutable abstract class Widget extends DiagnosticableTree { // ...省略代碼 }
被
@immutable
注解標明的類或者子類都必須是不可變的
提示:定義到Widget中的數據一定是不可變的,需要使用final來修飾
-
-
2>、如何存儲Widget狀態?
- 既然Widget是不可變,那么StatefulWidget如何來存儲可變的狀態呢?
- StatelessWidget無所謂,因為它里面的數據通常是直接定義完后就不修改的。
- 但StatefulWidget需要有狀態(可以理解成變量)的改變,這如何做到呢?
- Flutter將StatefulWidget設計成了兩個類:
- 也就是你創建StatefulWidget時必須創建兩個類:
- 一個類繼承自StatefulWidget,作為Widget樹的一部分;
- 一個類繼承自State,用于記錄StatefulWidget會變化的狀態,并且根據狀態的變化,構建出新的Widget;
- 創建一個StatefulWidget,我們通常會按照如下格式來做:
當Flutter在構建Widget Tree時,會獲取
State的實例
,并且它調用build方法去獲取StatefulWidget希望構建的Widget;-
那么,我們就可以將需要保存的狀態保存在MyState中,因為它是可變的;
class MyStatefulWidget extends StatefulWidget { @override State<StatefulWidget> createState() { // 將創建的State返回 return MyState(); } } class MyState extends State<MyStatefulWidget> { @override Widget build(BuildContext context) { return <構建自己的Widget>; } }
提示:Flutter這樣設計的目的是:在Flutter中,只要數據改變了Widget就需要重新構建(rebuild)
- 既然Widget是不可變,那么StatefulWidget如何來存儲可變的狀態呢?
-
-
2.6、StatefulWidget案例
我們通過一個案例來練習一下StatefulWidget,還是之前的計數器案例,但是我們按照自己的方式進行一些改進。
案例效果以及布局如下:在這個案例中,有很多布局對于我們來說有些復雜,我們后面會詳細學習,建議大家根據我的代碼一步步寫出來來熟悉Flutter開發模式;
Column小部件:之前我們已經用過,當有垂直方向布局時,我們就使用它;
Row小部件:之前也用過,當時水平方向布局時,我們就使用它;
-
RaiseButton小部件:可以創建一個按鈕,并且其中有一個onPress屬性是傳入一個回調函數,當按鈕點擊時被回調;
-
1>、 創建StatefulWidget,下面我們來看看代碼實現:
為當點擊按鈕時,數字會發生變化,所以我們需要使用一個StatefulWidget,所以我們需要創建兩個類;
MyCounterWidget繼承自StatefulWidget,里面需要實現createState方法;
-
MyCounterState繼承自State,里面實現build方法,并且可以定義一些成員變量
class MyCounterWidget extends StatefulWidget { @override State<StatefulWidget> createState() { // 將創建的State返回 return MyCounterState(); } } class MyCounterState extends State<MyCounterWidget> { int counter = 0; @override Widget build(BuildContext context) { return Center( child: Text("當前計數:$counter", style: TextStyle(fontSize: 30),), ); } }
-
2>、實現按鈕的布局
class MyCounterState extends State<MyCounterWidget> { int counter = 0; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( color: Colors.redAccent, child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),), onPressed: () { }, ), RaisedButton( color: Colors.orangeAccent, child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),), onPressed: () { }, ) ], ), Text("當前計數:$counter", style: TextStyle(fontSize: 30),) ], ), ); } }
-
3>、按鈕點擊狀態改變
- 我們現在要監聽狀態的改變,當狀態改變時要修改counter變量:
- 但是,直接修改變量可以改變界面嗎?不可以。
- 這是因為Flutter并不知道我們的數據發生了改變,需要來重新構建我們界面中的Widget;
- 如何可以讓Flutter知道我們的狀態發生改變了,重新構建我們的Widget呢?
我們需要調用一個State中默認給我們提供的setState方法;
-
可以在其中的回調函數中修改我們的變量;
onPressed: () { setState(() { counter++; }); },
- 我們現在要監聽狀態的改變,當狀態改變時要修改counter變量:
-
2.7、StatefulWidget 生命周期
-
1>、生命周期的理解
- 什么是生命周期呢?
- 客戶端開發:iOS開發中我們需要知道UIViewController從創建到銷毀的整個過程,Android開發中我們需要知道Activity從創建到銷毀的整個過程。以便在不同的生命周期方法中完成不同的操作;
- 前端開發中:Vue、React開發中組件也都有自己的生命周期,在不同的生命周期中我們可以做不同的操作;
- Flutter小部件的生命周期:
- StatelessWidget可以由父Widget直接傳入值,調用build方法來構建,整個過程非常簡單;
- 而StatefulWidget需要通過State來管理其數據,并且還要監控狀態的改變決定是否重新build整個Widget;
- 所以,我們主要討論StatefulWidget的生命周期,也就是它從創建到銷毀的整個過程;
- 什么是生命周期呢?
-
2>、生命周期
那么StatefulWidget有哪些生命周期的回調呢?它們分別在什么情況下執行呢?
atefulWidget本身由兩個類組成的:StatefulWidget和State,我們分開進行分析
首先,執行StatefulWidget中相關的方法:- 1、執行StatefulWidget的構造函數(Constructor)來創建出StatefulWidget;
- 2、執行StatefulWidget的createState方法,來創建一個維護StatefulWidget的State對象;
其次,調用createState創建State對象時,執行State類的相關方法:
- 1、執行State類的構造方法(Constructor)來創建State對象;
- 2、執行initState,我們通常會在這個方法中執行一些數據初始化的操作,或者也可能會發送網絡請求;
- 3、執行didChangeDependencies方法,這個方法在兩種情況下會調用
- 情況一:調用initState會調用;
- 情況二:從其他對象中依賴一些數據發生改變時,比如前面我們提到的InheritedWidget;
- 4、Flutter執行build方法,來看一下我們當前的Widget需要渲染哪些Widget;
- 5、當前的Widget不再使用時,會調用dispose進行銷毀;
- 6、手動調用setState方法,會根據最新的狀態(數據)來重新調用build方法,構建對應的Widgets;
- 7、執行didUpdateWidget方法是在當父Widget觸發重建(rebuild)時,系統會調用didUpdateWidget方法;
過代碼進行演示:
import 'package:flutter/material.dart'; main(List<String> args) { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text("HelloWorld"), ), body: HomeBody(), ), ); } } class HomeBody extends StatelessWidget { @override Widget build(BuildContext context) { print("HomeBody build"); return MyCounterWidget(); } } class MyCounterWidget extends StatefulWidget { MyCounterWidget() { print("執行了MyCounterWidget的構造方法"); } @override State<StatefulWidget> createState() { print("執行了MyCounterWidget的createState方法"); // 將創建的State返回 return MyCounterState(); } } class MyCounterState extends State<MyCounterWidget> { int counter = 0; MyCounterState() { print("執行MyCounterState的構造方法"); } @override void initState() { super.initState(); print("執行MyCounterState的init方法"); } @override void didChangeDependencies() { // TODO: implement didChangeDependencies super.didChangeDependencies(); print("執行MyCounterState的didChangeDependencies方法"); } @override Widget build(BuildContext context) { print("執行執行MyCounterState的build方法"); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( color: Colors.redAccent, child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),), onPressed: () { setState(() { counter++; }); }, ), RaisedButton( color: Colors.orangeAccent, child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),), onPressed: () { setState(() { counter--; }); }, ) ], ), Text("當前計數:$counter", style: TextStyle(fontSize: 30),) ], ), ); } @override void didUpdateWidget(MyCounterWidget oldWidget) { super.didUpdateWidget(oldWidget); print("執行MyCounterState的didUpdateWidget方法"); } @override void dispose() { super.dispose(); print("執行MyCounterState的dispose方法"); } }
打印結果如下:
flutter: HomeBody build flutter: 執行了MyCounterWidget的構造方法 flutter: 執行了MyCounterWidget的createState方法 flutter: 執行MyCounterState的構造方法 flutter: 執行MyCounterState的init方法 flutter: 執行MyCounterState的didChangeDependencies方法 flutter: 執行執行MyCounterState的build方法 // 注意:Flutter會build所有的組件兩次(查了GitHub、Stack Overflow,目前沒查到原因) flutter: HomeBody build flutter: 執行了MyCounterWidget的構造方法 flutter: 執行MyCounterState的didUpdateWidget方法 flutter: 執行執行MyCounterState的build方法
當我們改變狀態,手動執行setState方法后會打印如下結果:
flutter: 執行執行MyCounterState的build方法
-