Flutter 已經推出2年了,雖然一直在關注,但仍是想等生態成熟一點再去踩坑。近期有一個須要使用跨平臺技術的項目,在討論後,咱們選擇使用 Flutter。開發完成以後,我這裏總結一些重要的點,供你們參考。
固然,要學習的話最後仍是須要讀一遍文檔,而後本身 Coding。git
參考官方文檔github
Flutter 採用 Dart 語言,我使用以後的感覺就是: 語法基本等於 Java + Javascript + 另一些常見的語法,沒太大學習成本,也沒太大亮點,下面列一些值得一提的點。macos
全部變量都是對象json
靜態語言bash
支持閉包網絡
方法是頂級的閉包
支持反射(Flutter 不支持反射)架構
沒有可見性修飾符 屬性/類前加_
就是 privateapp
Stream : 支持 map... 各種操做符,訂閱等less
異步:Dart 的異步操做也經過 Futrue
(同 Javascript 中的 Promise
) 的方式實現,也支持 async
await
語法糖(自動包裝爲Futrue
)。這並非 Dart 特有的特性,網上有大量資料能夠參考。
賦值操做符
可選方法參數
void setUser(String name,{id = '0'});
//調用
setUser('mario',id : '01');
複製代碼
var profit = Profit()
..fund = 'fund'
..profit = 'profit'
..profitValue = 'profitValue';
複製代碼
var a = 'test';
(a as dynamic).hello();//編譯器不會報錯
複製代碼
在Flutter中幾乎全部的對象都是一個Widget。與原生開發中「控件」不一樣的是,Flutter中的Widget的概念更普遍,它不只能夠表示UI元素,也能夠表示一些功能性的組件如:用於手勢檢測的 GestureDetector widget、用於APP主題數據傳遞的Theme等等,而原生開發中的控件一般只是指UI元素。
個人理解爲 Widget 的工做 = HTML + CSS 的工做。並且不少配置樣式的屬性名字和 CSS 中的名字差很少。
Widget 分爲 StatelessWidget
StatefulWidget
兩種,他們的核心方法都是經過build()
方法返回一個 Widget 。
@protected
Widget build(BuildContext context);
複製代碼
StatelessWidget
的build()
在 Widget 中。StatefulWidget
因爲必須建立相應的 State<T extends Widget>
,因此包括build()
在內的相關生命週期方法都在State
中。State
的生命週期,因爲一個畫面也是一個 Widget 因此也是一個畫面的生命週期。上面是官方提供的全部的 Widget,能夠看到基本上全部UI相關的內容都是經過不一樣類型的 Widget 來實現,經過child/children
參數進行嵌套。
除了基礎 Widget 外,官方提供了 Material(Android) + Cupertino(ioS) 兩種視覺風格的 Widget。
例如你能夠在使用一個 Marterial 風格的RaisedButton
或是 Cuptino 風格的CupertinoButton
,不再用擔憂設計師讓 Android 照着 ioS 作成同樣了。
還有用來控制佈局的 Layout Widget ,做爲容器來使用,看名字都大概知道什麼做用了。
控制點擊、滑動等交互的 Widget。
在 Flutter 裏點擊事件並非setOnClickListener
的方式 ,而是給 Widget 外層加一層交互 Widget ,如點擊可以使用GestureDetector
。 例如給上面 Splash 畫面中的Image
加一個點擊事件。
@override
Widget build(BuildContext context) {
return Container(
height: double.infinity,
width: double.infinity,
child: Image.asset('images/logo.png'),
);
}
==>
@override
Widget build(BuildContext context) {
return Container(
height: double.infinity,
width: double.infinity,
child: GestureDetector(
onTap: () {
//點擊事件
},
child: Image.asset('images/logo.png'),
),
);
}
複製代碼
因此,一個最基本的 Widget 長什麼樣?這是一個帶有是否 login 檢查的 Splash 畫面。
class SplashPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
checkLogin();
return Container(
height: double.infinity,
width: double.infinity,
child: Image.asset('images/logo.png'),
);
}
// 使用 async 語法自動包裝爲 Futrue,也就是說這個方法是異步的。
checkLogin(BuildContext context) async {
var sp = await SharedPreferences.getInstance();
var token = sp.getString("X-Auth-Token");
if (token != null && token != "")
Navigator.pushNamedAndRemoveUntil(
context, HomePage.routeName, (_) => false);
else
Navigator.pushNamed(context, LoginRegisterPage.routeName);
}
}
複製代碼
class SplashPage extends StatefulWidget {
//建立相應的 State
@override
State createState() => _SplashState();
}
class _SplashState extends State<SplashPage> {
@override
void initState() {
super.initState();
checkLogin(context);
}
@override
Widget build(BuildContext context) {
return Container(
height: double.infinity,
width: double.infinity,
child: Image.asset('images/logo.png'),
);
}
// 使用 async 語法自動包裝爲 Futrue,也就是說這個方法是異步的。
checkLogin(BuildContext context) async {
var sp = await SharedPreferences.getInstance();
var token = sp.getString("X-Auth-Token");
if (token != null && token != "")
Navigator.pushNamedAndRemoveUntil(
context, HomePage.routeName, (_) => false);
else
Navigator.pushNamed(context, LoginRegisterPage.routeName);
}
@override
void dispose() {
super.dispose();
}
}
複製代碼
上圖是整個 Flutter App 的結構,從父節點開始分別是:
MyApp
: 整個 App 的入口在main.dart
的main()
函數中,調用 runApp(MyApp())
,而 MyApp 也是一個 Widget,只不過用來定義一些全局的內容,例如主題、多語言,路由MaterialApp
: 一個 Material 風格的主題,對應的還有 CupertinoApp。MyHomePage
MyHomePageState
: 一個畫面,也是 Widget。Scaffold
: 定義了一個畫面的一些基本效果,好比這裏 AppBar、滑動效果等採用 Material 風格,另外還有 ioS 風格的 CupertinoPageScaffold
。一個基本的 main.dart 大概長這樣:
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
static final navigatorKey = GlobalKey<NavigatorState>();
static NavigatorState get navigator => navigatorKey.currentState;
@override
Widget build(BuildContext context) {
return CupertinoApp(
title: '',
theme: CupertinoThemeData(
primaryColor: Color(0xFFFFFFFF),
barBackgroundColor: Color(0xFF515669),
scaffoldBackgroundColor: Color(0xFF3C3B45),
),
navigatorKey: navigatorKey,
routes: {
HomePage.routeName: (_) => HomePage(),
LoginRegisterPage.routeName: (_) => LoginRegisterPage(),
LoginPage.routeName: (_) => LoginPage(),
ForgetPswPage.routeName: (_) => ForgetPswPage(),
RegisterPage.routeName: (_) => RegisterPage(),
},
),
home: SplashPage(),
);
}
}
複製代碼
theme
定義了一個 ioS 風格的 CupertinoApp 主題(實際開發中可能須要同時使用 Material Cupertino 風格控件因此須要自定義主題)routes
參數註冊路由表home
參數設置首次加載的 Splash 畫面和 Web 中的路由相似,經過在路由表註冊相應的 url 和畫面。基本方法
基本使用:
// pushNamed 的定義
Future pushNamed(BuildContext context, String routeName,{Object arguments})
//打開一個畫面,傳一個00
Navigator.of(context).pushNamed("home_page", arguments: '00');
//新畫面接受參數
var arg = ModalRoute.of(context).settings.arguments);
//關閉一個畫面,返回一個01
Navigator.of(context).pop(01);
複製代碼
pushNamed
方法返回值是一個Future
,說明是一個異步操做,由於能夠接受打開的畫面pop
關閉時返回的result
,此處在pop
時返回了一個 01,那麼就能夠這樣接收到。var result = async Navigator.of(context).pushNamed("home_page", arguments: 'arg');
複製代碼
Flutter 的 網絡請求庫沒有特別完美的,目前使用的是 Dio ,大體是一個簡化版的 okhttp 。
因爲 Flutter 禁止使用反射,由於運行時反射會干擾 Dart 的 tree shaking,因此相似 Gson 這樣經過反射進行序列化的方式就行不通了。
目前大概的解決方案有兩種:
Dio
會把返回值解析爲 Map/List ,因此能夠這樣手寫:Future<Profits> requestProfits() async {
var response = await dio.get("u/profits");
var data = response.data;
print("requestProfits:$data");
var profit = Profit()
..fund = data['profit']["fund"]
..profit = data['profit']["profit"]
..profitValue = toMoney(data['profit']['profitValue']);
return Profits()
..miningProfit = data['miningProfit']
..lastMiningProfit = data['lastMiningProfit']
..profit = profit;
}
複製代碼
//user.dart
import 'package:json_annotation/json_annotation.dart';
// user.g.dart 將在咱們運行生成命令後自動生成
part 'user.g.dart';
///這個標註是告訴生成器,這個類是須要生成Model類的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
//不一樣的類使用不一樣的mixin便可
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
複製代碼
固然,仍是須要寫 fromJson
toJson
的模板代碼,也能夠經過生成的方式解決。
Flutter 主要是負責了UI部分的構建,各平臺特定的代碼仍是要經過原生實現,主要用兩種方法處理:
Platform Channel
: 大概就是 Flutter 端和原生端註冊約定好 platform_channel_name
的 Platform Channel
,而後調用方法和傳參,另外一端解析就好了。具備原生能力的 plugin 也就是這樣實現的。好比//flutter
MethodChannel('method_channel_mobile').invokeMethod('sendMobile','13000000000')
//Android MainActivity
MethodChannel(flutterView, MOBILE_CHANNEL)
.setMethodCallHandler { methodCall, result ->
when {
TextUtils.equals(methodCall.method, "mobile") -> {
mobile = methodCall.arguments.toString()
result.success("success")
}
result.notImplemented()
}
}
複製代碼
PlatformView
直接嵌套原生的 View 到 Flutter 中,但這樣作效率不高。另外須要注意的是不要傳入一個 view 到PlatformView
中,不然可能出現 Flutter 端屢次調用該PlatformView
的時候狀態會共存,以及不會銷燬。// 定義一個用於的 PlatformView 和 PlatformViewFactory 用於實例化 Native View
class ButtonFactory(
private val context: Context
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(p0: Context?, p1: Int, p2: Any?): PlatformView {
return ButtonPlatformView(context)
}
class ButtonPlatformView(
private val context: Context
) : PlatformView {
override fun getView(): Button {
return Button(context)
}
override fun dispose() {
}
}
}
//在 MainActivity 中註冊
registrarFor("native_view").platformViewRegistry()
.registerViewFactory("native_view",ButtonPlatformFactory)
複製代碼
網上對 Flutter 嵌套討論的比較多的問題就是,UI 複雜了之後,嵌套層數太多。 確實有這個問題,以前說了 Widget 不光是 View 還包括配置文件,因此一個相似 Button 這樣的 Widget 可能就須要嵌套3 4層。
下面是我寫的一個登陸畫面的登陸按鈕,感覺一下:
CupertinoButton _loginButton() {
return CupertinoButton(
padding: EdgeInsets.all(0),
child: Container(
width: double.infinity,
height: 45,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: _isLoginAvailable
? <Color>[Color(0xFF657FF8), Color(0xFF4260E8)]
: <Color>[Color(0xFFCBCFE2), Color(0xFF73788F)]),
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Center(
child: Text(
"登 錄",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: _isLoginAvailable ? Colors.white : Color(0x76FFFFFF),
fontWeight: FontWeight.bold,
),
),
)),
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
onPressed: !_isLoginAvailable ? null : _startCustomFlow,
);
}
複製代碼
實際上這仍是隻是結構+樣式部分,不包括點擊後的邏輯。
甚至你能夠看到 Button 中的文字也是經過嵌套一個 Widget 來實現的,但這也是 Flutter 的一個優點,再也不須要寫自定義 Widget 的人去提供大量像文字能不能加粗,變色、斜體等等細節的樣式,直接讓你傳一個 Widget 自行處理,相似的狀況還有不少。
另一個問題是 Widget State 的狀態可能太多,包括各個 Widget 的狀態和畫面的狀態堆在一塊兒,想起了當年原生 Android 一個 Activity 50個變量的恐懼。
但我認爲這些主要仍是由於 Flutter 處於發展的初期,尚未太成熟的架構,目前官方提供了狀態管理的庫 Provider
。 我目前的解決方案是儘可能提成方法和獨立的Widget:
StatefulWidget
。StatelessWidget
,不須要就抽成一個方法,返回 Widget,參考上面的 Button 。build()
方法中只描述整個畫面的結構。例如一個 login 畫面的build()
方法我是這樣寫的:
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: cusAppBar(context, elevation: 0),
backgroundColor: $3C3B45,
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Container(
margin: EdgeInsets.only(left: 15, right: 15),
child: Column(
children: <Widget>[
_logo(),
Form(
onChanged: _onFormChanged,
child: Column(
children: <Widget>[
_phoneRow(),
_divider(),
_passwordColumn(),
_forgetPswText(),
_loginButton(),
_registerText()
],
),
),
],
),
),
),
],
),
),
);
}
複製代碼
Flutter 的熱重載是廣受歡迎的一個特性,重要緣由則是 Debug 模式採用 JIT 編譯,release 模式採用 AOT 編譯。實際用下來效果不錯。
優勢:
Android iOs 兩端 UI 高度一致
:因爲 Flutter 使用本身的一套繪製 UI 的引擎和邏輯,徹底不使用 Native View,僅僅調用原生的繪製接口,因此幾乎能夠作到兩個平臺的 UI 如出一轍,這也是 Flutter 還要作 Web maCos 等全平臺的緣由。我在開發期間一直使用 Android 進行調試,最後在 Ios 上跑的時候,幾乎沒有什麼差異(雖然目前 UI 也不太複雜)。接入原生相對容易
:須要原生實現的功能經過PlatformChannel
和 PlatformView
也大多都能實現,還能夠經過PlatformChannel
來啓動一個原生的Activity/Fragment
實現。(好比掃一掃功能)貴族血統
:Google 的全力支持,國內大廠也都在積極嘗試。初步可用的程度
:目前已經完成了一個小項目的開發,在和原生交互很少的狀況下尚未遇到太大的坑。缺點:
基礎功能的缺失
:不少基礎的功能也須要用 plugin 經過原生來實現,好比 Webview Map 這些組件,更不要說一些 SDK ,幾乎都須要本身寫 plugin。跨平臺的通訊
:對於大量使用MethodChannel
進行通訊以及各平臺間API有差別的狀況下,設計和維護的問題。性能
:目前原生 Flutter 在幀數上接近原生,用戶使用體驗接近,但內存開銷更大,尤爲在視頻方面。總的來講:個人見解是,比較看好 Flutter 跨更多平臺的前途,目前來講適合用來開發和原平生臺 API 交互不那麼複雜的 App 。