前言一:接下來一段時間我會陸續更新一些列Flutter文字教程
更新進度: 每週至少兩篇;
更新地點: 首發於公眾號,第二天更新於掘金、思否、開發者頭條等地方;
更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy
希望大家可以 幫忙轉發,點選在看,給我更多的創作動力。
一. 建立Flutter專案
建立Flutter專案有兩種方式:
通過命令列建立
和通過開發工具建立
1.1. 通過命令列建立
通過命令列建立非常簡單,在終端輸入以下命令即可:
- **注意:**Flutter的名稱不要包含特殊的字元,另外不可以使用駝峰標識
- 建立完之後使用自己喜歡的開發工具開啟即可
flutter create learn_flutter
複製程式碼
1.2. 通過開發工具建立
我這裡也可以直接通過Android Studio來進行建立:
- 選擇
Start a new Flutter project
,之後填寫相關的資訊即可,這裡不再贅述
1.3. 預設程式分析
我們講建立的應用起來跑在模擬器上(我這裡選擇iPhone模擬器,Android也可以),會看到如下效果:
預設專案分析:
- 我們之前已經分析過目錄結構了,在目錄下有一個
lib
資料夾,裡面會存放我們編寫的Flutter程式碼; - 開啟發現裡面有一個
main.dart
,它是我們Flutter啟動的入口檔案
,裡面有main函式
;
預設程式碼分析:
- 這是一個計數器的案例程式,點選右下角的
+
符號,上面顯示的數字會遞增; - 但是我們第一次接觸main.dart中的程式碼,可能會發現很多
不認識
的程式碼,不知道這個內容是如何編寫出來的;
作為初學者,我的建議是將其中所有的程式碼全部刪除掉,從零去建立裡面的程式碼,這樣我們才能對Flutter應用程式的結構非常清晰;
二. 開始Flutter程式碼
2.1. Hello World
2.1.1. Hello World的需求
做任何的開發,我們都是從祖傳的Hello World
開始,那麼現在我們的需求來了:
- 在介面中心位置,顯示一個Hello World;
2.1.2. Hello World的實現
下面,我們就動手開始編寫Hello World:
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. 程式碼分析
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;
2.2.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. 程式碼改進
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: 36),
),
)
);
}
複製程式碼
2.3.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;
2.3.3. 進階案例實現
我們可以讓介面中存在更多的元素:
- 寫到這裡的時候,你可能已經發現
巢狀太多
了,不要著急,我們後面會對程式碼重構的
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: 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.4. 程式碼重構
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.4.2. StatelessWidget
StatelessWidget通常是一些沒有狀態(State,也可以理解成data)需要維護的Widget:
- 它們的資料通常是直接寫死(放在Widget中的資料,必須被定義為final,為什麼呢?我在下一個章節講解StatefulWidget會講到);
- 從parent widget中傳入的而且一旦傳入就不可以修改;
- 從InheritedWidget獲取來使用的資料(這個放到後面會講解);
我們來看一下建立一個StatelessWidget的格式:
- 1、讓自己建立的Widget繼承自StatelessWidget;
- 2、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方法什麼情況下被執行呢?:
- 1、當我們的StatelessWidget第一次被插入到Widget樹中時(也就是第一次被建立時);
- 2、當我們的父Widget(parent widget)發生改變時,子Widget會被重新構建;
- 3、如果我們的Widget依賴InheritedWidget的一些資料,InheritedWidget資料發生改變時;
2.4.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),
)
],
),
);
}
}
複製程式碼
三. 案例練習
3.1. 案例最終效果
我們先來看一下案例的最終展示效果:
- 這個效果中我們會使用很多沒有接觸的Widget;
- 沒有關係,後面這些常用的Widget我會一個個講解;
- 這個案例最主要的目的還是讓大家更加熟悉Flutter的開發模式以及自定義Widget的封裝過程;
3.2. 自定義Widget
在我們的案例中,很明顯一個產品的展示就是一個大的Widget,這個Widget包含如下Widget:
- 標題的Widget:使用一個Text Widget完成;
- 描述的Widget:使用一個Text Widget完成;
- 圖片的Widget:使用一個Image Widget完成;
- 上面三個Widget要垂直排列,我們可以使用一個Column的Widget(上一個章節中我們使用了一次Row是水平排列的)
另外,三個展示的標題、描述、圖片都是不一樣的,所以我們可以讓Parent Widget來決定內容:
- 建立三個成員變數儲存父Widget傳入的資料
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
Image.network(imageURL)
],
);
}
}
複製程式碼
3.3. 列表資料展示
現在我們就可以建立三個ProductItem來讓他們展示了:
- MyApp和上一個章節是一致的,沒有任何改變;
- HomeContent中,我們使用了一個Column,因為我們建立的三個ProductItem是垂直排列的
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.blueAccent
),
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: HomeContent(),
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ProductItem("Apple1", "Macbook Product1", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
ProductItem("Apple2", "Macbook Product2", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
ProductItem("Apple3", "Macbook Product3", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
],
);
}
}
複製程式碼
執行效果如下:
- 錯誤資訊:下面出現了黃色的斑馬線;
- 這是因為在Flutter的佈局中,內容是不能超出螢幕範圍的,當超出時不會自動變成滾動效果,而是會報下面的錯誤;
如何可以解決這個問題呢?
- 我們將Column換成ListView即可;
- ListView可以讓自己的子Widget變成滾動的效果;
3.4. 案例細節調整
3.4.1. 介面整體邊距
如果我們希望整個內容距離螢幕的邊緣有一定的間距,怎麼做呢?
- 我們需要使用另外一個Widget:Padding,它有一個padding屬性用於設定邊距大小;
- 沒錯,設定內邊距也是使用Widget,這個Widget就是Padding;
3.4.2. 商品內邊距和邊框
我們現在希望給所有的商品也新增一個內邊距,並且還有邊框,怎麼做呢?
- 我們可以使用一個Container的Widget,它裡面有padding屬性,並且可以通過decoration來設定邊框;
- Container我們也會在後面詳細來講,我們先用起來;
3.4.3. 文字圖片的間距
我們希望給圖片和文字之間新增一些間距,怎麼做呢?
- 方式一:給圖片或者文字新增一個向上的內邊距或者向下的內邊距;
- 方式二:使用SizedBox的Widget,設定一個height屬性,可以增加一些距離;
3.5. 最終實現程式碼
最後,我給出最終實現程式碼:
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.blueAccent
),
home: Scaffold(
appBar: AppBar(
title: Text("CODERWHY"),
),
body: HomeContent(),
),
);
}
}
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: <Widget>[
ProductItem("Apple1", "Macbook Product1", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
ProductItem("Apple2", "Macbook Product2", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
ProductItem("Apple3", "Macbook Product3", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
],
),
);
}
}
class ProductItem extends StatelessWidget {
final String title;
final String desc;
final String imageURL;
ProductItem(this.title, this.desc, this.imageURL);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
border: Border.all()
),
child: Column(
children: <Widget>[
Text(title, style: TextStyle(fontSize: 24)),
Text(desc, style: TextStyle(fontSize: 18)),
SizedBox(height: 10,),
Image.network(imageURL)
],
),
);
}
}
複製程式碼
備註:所有內容首發於公眾號,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注