1. 我們需要用Flutter麼?
Flutter
與RN
不同的是前者和原生iOS/Android元件比如UIButton
其實沒半毛線關係,所有元素都是Flutter
自己畫的,而RN
則只是做了個橋呼叫而已,明白這個可能對你下決定/忽悠老闆有非常大的意義
以下開始,我們假設你已經有了一定的Flutter基礎概念, 比如Release/Debug版本,比如如何跑起來一個HelloWord
2. 混合棧:
谷歌寫的很清楚了大家找著1234567就可以兩者混合了,當然你還可以谷歌搜【flutter混合】
配置中有一些爸爸們沒告訴你的事,還是以iOS工程為主,畢竟Android不熟:
0.flutter create -t module xxx
中的 -t
有很多例項裡是沒有-t
的,簡單來說不加-t
,你的iOS/android的工程將會隨git一起提交,而加了後他們永遠不會被提交,別人拉新程式碼都需要flutter create
來正常跑起來。
咋看一下-t
會更高階一點,你不需要改任何原生工程全用flutter
寫就行,而且也不會因為sdk改動或者xcode改動影響到你的iOS資料夾,但是請誠實點面對這個社會:
- 預設生成的iOS資料夾的
podfile
是沒有!use_framwork
的 - 你確定不需要改plist來新增白名單什麼嘛
- 你確定appdelegate裡不需要加些配置來協助你的第三方庫比如微信嘛
所以如果-t
了,作為架構師/研究者的你請自覺做好寫指令碼去修改上面這些檔案的準備。
同時請妥善執行
flutter packages get
,因為他會重新搞一下你的iOS資料夾的內容, 而flutter build --release
會預設幫你flutter packages get
,所以如果你有自己的初始化指令碼,那執行順序應該是:
flutter packages get
- 你的指令碼
flutter build --release -no-pub
-no-pub
會忽略掉build
時的那次get
1. Build Phases
中的script
其實不是每次都需要run的
上面這個勾推薦你勾上後再提交,因為這個指令碼其實只會對Flutter Release
版本的構建有影響,
- 說白了,如果你只是普通除錯,跑不跑這個指令碼,結果是一樣的都是
Flutter Debug
模式 - 這個指令碼會增加編譯時間,所以無需
flutter
更新的工程師不需要關心他是不是跑了 - 如果你是
Debug
的主工程想跑Release
的Flutter
,不跑這個Script是會crash的,這就是因為Flutter Release
的配置需要這個指令碼來完成
2. 請老老實實按照谷歌推薦的方式整合,剛玩時候你的目標是跑起來然後迅速去熟悉dart以及flutter
佈局,並不是研究原生與flutter
的耦合以及框架解藕
3. 打包指令碼中Flutter run
是不合理的
可能你也會用jenkins或者fastlane去給QA打包,這時候如果要生成產物請走Flutter build --release
命令,因為Flutter run
會直接把程式卡住然後你就無法持續了,雖然你可以選擇後臺執行run
,但是你沒法保證同步~
4. 跳轉還是老老實實走 flutter_boost
至於為什麼,可以翻翻鹹魚寫的文章,還是從簡化的說就是:
- 谷歌給的flutter混合棧中,
FlutterViewController
肚子裡就是Flutter引擎
,所以如果純flutter專案你會發現,其實vc就只有一個,他是在單個vc中畫新頁面來完成push操作的 - 但是混合棧中你一定會
FlutterViewController
去push下一個FlutterViewController
,這樣無限增加的引擎會讓你的應用在短時間內就崩潰~ - 鹹魚的方案就是保留一個引擎,然後在push的時候把引擎單例從前一個vc拿到後一個,然後通過截圖等操作重現滑動返回或者pop等操作
當然如果你是很厲害的那種一定要自己玩,那也可以,否則請看2裡說的在這裡一樣適用~
用過的孩子一定對
query
這個key很憤怒,確實你也不明白為什麼鹹魚官方並沒有說這個key,但事實上安卓側原生接到的引數都是通過這個包著的,即{ "query": { "name" : xxx }}
, 所以iOS的也可以注意下
5. MethodChannel系列互動
但是其實這樣並不好看~雖然官方推薦,但是官方還有個很優雅的姿勢叫FlutterPlugin
說穿了MethodChannel
只是需要原生有個時機註冊進Flutter
引擎而已,至於什麼時候,隨時都行,所以我們找到了FlutterPlugin
的時機,所有第三方的flutter外掛(這些外掛多數其實也是通過channel呼叫原生來解決的),其實也是通過註冊的方式注入引擎的,如果你想看看怎麼來的,你可以在你的工程裡找GeneratedPluginRegistrant.m
這個檔案,這是個系統生成的檔案,他會幫你註冊所有你引入flutter的外掛:
@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FLTDeviceInfoPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTDeviceInfoPlugin"]];
...
}
@end
複製程式碼
所以我們有個大膽的想法,如果我們自己也搞個plugin
,裡面塞個channel
,用來做所有flutter
與我們現有主工程的進行互動,不就可以裝的很優雅了嘛?
但是問題是:系統生成的GeneratedPluginRegistrant
我怎麼在裡面加上我自己的外掛? 因為我自己的外掛在主工程,並不是在flutter裡引入的鴨...
這時候聰明的你可能已經想到了那在Swift時代被遺忘的黑魔法:
- (instancetype)init
{
if (self = [super init]) {
_viewController = [FLBFlutterViewControllerAdaptor new];
[_viewController view];
Class clazz = NSClassFromString(@"GeneratedPluginRegistrant");
if (clazz) {
if ([clazz respondsToSelector:NSSelectorFromString(@"registerWithRegistry:")]) {
[clazz performSelector:NSSelectorFromString(@"registerWithRegistry:")
withObject:_viewController];
}
}
}
return self;
}
複製程式碼
上面這段是從flutter_boost
中找到的,所以你應該明白其實你也可以進行偷換GeneratedPluginRegistrant
中的registerWithRegistry
方法來先註冊flutter自己的外掛,再註冊主工程外掛已保持優雅的姿態了吧~
6. 與原生通訊該傳些什麼
FlutterResult
類裡規定了記得傳String
,當然傳人能讀得懂的字串了,請重複這句【傳人能讀得懂的字串】,所以千萬不要耍心機把一個data
搞成字串然後傳給flutter
讓他變回data
甚至變回image
,記得,你讀不懂image
字串,flutter
也不懂。而json
字串你看得懂,所以flutter
也看得懂
所以複雜的檔案,你只要知道,flutter
雖然讀不懂data
,但是他也能訪問NSTemporaryDirectory
, 所以你可以通過暫時資料夾路徑的方式來支援雙方共享同一個資料,雖然也不太推薦,但是你好像也只能這麼幹了
private func saveToFile(image: UIImage) -> String? {
guard let data = image.jpg(compressionQuality: 1.0) else {
return nil
}
let tempDir = NSTemporaryDirectory()
let imageName = "image_picker_\(ProcessInfo().globallyUniqueString).jpg"
let filePath = tempDir.appending(imageName)
if FileManager.default.createFile(atPath: filePath, contents: data, attributes: nil) {
return filePath
} else {
return nil
}
}
複製程式碼
比如像樓上這樣
3. Dart
裡我要準備些啥
1. 抽象底層服務
如果一開始規劃是有原生的基礎服務就調原生的,比如請求登入拍照等等,那理想中其實你可以覺得flutter
本身程式碼你只需要做業務就行了。
嗯,回想一下那個RN
中編碼5分鐘,聯調5小時的你嗎?所以如果有時間,請為flutter
搭建所有用得到的基礎設施,來大幅度加快除錯速度:
下面這個模式可供大家參考,他可以最大限度保證程式碼可擴充套件性
我們通過
上帝模式:isSonMode= false
即flutter
自己跑的模式
兒子模式:isSonMode= true
即在主工程裡跑的模式
上述的區分來判斷我們需要呼叫哪種基礎服務
比如這是我們的dart端請求呼叫:
class RequestService {
static RequestService shared = RequestService();
factory RequestService() =>
GlobalConfig.isSonMode ? SonRequestService() : GodRequestService();
Future<Map> post(String url, Map para) async {
throw UnimplementedError("saveElement 方法木有實現哦");
}
Future<UploadItem> upload(UploadItem file) async {
throw UnimplementedError("saveElement 方法木有實現哦");
}
}
class GodRequestService implements RequestService {
Future<Map> post(String url, Map para) async {
// 你可以在這裡呼叫dio或者http請求
}
Future<UploadItem> upload(UploadItem file) async {
...
}
}
複製程式碼
Son
就是呼叫原生的channel,所以不貼了,所以我們根據GlobalConfig.isSonMode
來切換使用哪個具體的RequestService
實現,來隔離每個RequestService
不會由於判斷而造成的汙染,
當然還有更花哨的判斷:
static Router _route =
GlobalConfig.isAndroid ? AndroidRouteBox() : GlobalConfig.isSonMode ? FlutterBoostRouteBox() : GodRouteBox();
複製程式碼
比如路由我們在flutter
上帝模式下用的是flutter
自己跳轉, iOS 主工程下是flutter_boost
, 安卓主工程下依舊是自己的跳轉
而在dart
業務程式碼中你只需要做
RequestService.shared.post...
的呼叫,就可以正常請求,不需要關心切換的問題了。
(當然請求抽象會在下期著重講,敬請期待~)
2. Model.fromJSON
工具到處都有 稍微推薦下這個,當然極其讚美手寫毅力擔當~但是注意點:
dart
相比Swift
來說對型別的嚴格性更高int
/double
如果定義反了會炸,比如把1.0
傳給int
型屬性,在Swift
中毫無波瀾,在dart
中就是波濤洶湧,dart
中妥善點可以用num
來修飾所有數字String
型別推薦都加上toString()
額外加個優雅的,Swift中的
var isUser : Bool {
return xxx == 1 && xxxx = 2
}
這樣的計算屬性,在dart中可以寫成:
bool get isUser => xxx == 1 && xxxx = 2
或者
bool get isUser {
return xxx == 1 && xxxx = 2
}
複製程式碼
這樣可以降低你的
Widget
中過多的業務計算,這些定義還是讓Model
來做吧
3. 忘記所謂的頁面生命週期
由於引擎的繪製不同,導致flutter
的頁面生命週期並沒有合適的回撥/代理等來觸發,不要想著didUpdateWidget
或者Dependency
裡來做些奇怪的請求重新整理,那地方不是讓你用來做這個事情的~
唯一你可以做的好像就只有在initState
裡做完所有事情;
這裡就講點虛的,所以所有的操作需要嚴格從動作觸發,而不要是再從頁面級別觸發了,
舉個例子
Swift
:button點選 -> 跳轉 -> 返回 -> ViewWillAppear 重新整理全頁
Flutter
: button點選 -> 重新整理對應的Widget -> 跳轉
其實Swift
的例子也不好,但是其實我們因為懶基本都這樣幹了,但是在dart
中我們沒法知道頁面生命週期,所以請用正確的時間做正確的事,當然這裡就容忍了滿螢幕的setState
,肯定會有人告訴你這樣是不合理的了~怎麼才算合理,這裡就先不說了,畢竟這裡的目的是為了讓大家把應用跑起來能上線,至於優雅不優雅後面熟悉了自然就有感覺了。
4. 資源圖片
首先這樣你肯定沒問題,但是記得外層的1x的圖一定要放的,否則認不出來~pubspec.yaml中這樣就行了
assets:
- assets/images/
- assets/images/2.0x/
- assets/images/3.0x/
複製程式碼
有時候你會發現你新加了個圖結果位置都放對了結果沒出來,沒事你debug
多點幾次他就有了,如果確認名字沒問題的話,確實是可能有時候不會及時顯示出來的
5. 字型
比較有意思的是對於main入口:
return MaterialApp(
title: '我是個demo',
theme: ThemeData(
fontFamily: Platform.isIOS ? 'PingFang SC' : null,)
}
複製程式碼
你需要這樣設定後,在iOS手機上你的字型才會好看,否則會出現各種奇奇怪怪的樣子,不過又有個隱藏坑就是:
你的所有頁面的頂層widget
必須是material
系列的這個才會生效,這些是Material
系列,
所以如果你的頁面裡直接是:
class DemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: 頁面元素
);
}
}
複製程式碼
你會發現你的字型依舊很奇怪,雖然頁面長得沒問題
是不是很簡單,看到這裡,坑肯定還有,但至少大問題應該沒有了,你應該已經可以愉快的跑起來你的應用了~