很簡單的Flutter填小坑

貳罐哈利發表於2019-05-30

1. 我們需要用Flutter麼?

FlutterRN不同的是前者和原生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的

https://user-gold-cdn.xitu.io/2019/5/30/16b08538ebc2104c?w=1370&h=456&f=jpeg&s=65695
上面這個勾推薦你勾上後再提交,因為這個指令碼其實只會對Flutter Release版本的構建有影響,

  • 說白了,如果你只是普通除錯,跑不跑這個指令碼,結果是一樣的都是Flutter Debug模式
  • 這個指令碼會增加編譯時間,所以無需flutter更新的工程師不需要關心他是不是跑了
  • 如果你是Debug的主工程想跑ReleaseFlutter,不跑這個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= falseflutter自己跑的模式

兒子模式: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. 資源圖片

很簡單的Flutter填小坑
首先這樣你肯定沒問題,但是記得外層的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: 頁面元素
		);
  }
}
複製程式碼

你會發現你的字型依舊很奇怪,雖然頁面長得沒問題

是不是很簡單,看到這裡,坑肯定還有,但至少大問題應該沒有了,你應該已經可以愉快的跑起來你的應用了~

相關文章