移動端開發(fā)新趨勢Flutter

該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯


介紹

FlutterGoogle開發(fā)的新一代跨平臺方案,Flutter可以實現(xiàn)寫一份代碼同時運行在iOS和Android設(shè)備上,并且提供很好的性能體驗。Flutter使用Dart作為開發(fā)語言,這是一門簡潔、強(qiáng)類型的編程語言。Flutter對于iOS和Android設(shè)備,提供了兩套視覺庫,可以針對不同的平臺有不同的展示效果。

Flutter原本是為了解決Web開發(fā)中的一些問題,而開發(fā)的一套精簡版Web框架,擁有獨立的渲染引擎和開發(fā)語言,但后來逐漸演變?yōu)橐苿佣碎_發(fā)框架。正是由于Dart當(dāng)初的定位是為了替代JS成為Web框架,所以Dart的語法更接近于JS語法。例如定義對象構(gòu)建方法,以及實例化對象的方式等。

Google剛推出Flutter時,其發(fā)展很緩慢,終于在18年發(fā)布第一個Bate版之后迎來了爆發(fā)性增長,發(fā)布第一個Release版時增長速度更快??梢詮腉ithub上Star數(shù)據(jù)看出來這個增長的過程。在19年最新的Flutter 1.2版本中,已經(jīng)開放Web支持的Beta版。

增長趨勢

目前已經(jīng)有不少大型項目接入Flutter,阿里的咸魚、頭條的抖音、騰訊的NOW直播,都將Flutter當(dāng)做應(yīng)用程序的開發(fā)語言。除此之外,還有一些其他中小型公司也在做。

整體架構(gòu)

Flutter可以理解為開發(fā)SDK或者工具包,其通過Dart作為開發(fā)語言,并且提供MaterialCupertino兩套視覺控件,視圖或其他和視圖相關(guān)的類,都以Widget的形式表現(xiàn)。Flutter有自己的渲染引擎,并不依賴原生平臺的渲染。Flutter還包含一個用C++實現(xiàn)的Engine,渲染也是包含在其中的。

Flutter整體架構(gòu)

Engine

Flutter是一套全新的跨平臺方案,Flutter并不像React Native那樣,依賴原生應(yīng)用的渲染,而是自己有自己的渲染引擎,并使用Dart當(dāng)做Flutter的開發(fā)語言。Flutter整體框架分為兩層,底層是通過C++實現(xiàn)的引擎部分,SkiaFlutter的渲染引擎,負(fù)責(zé)跨平臺的圖形渲染。Dart作為Flutter的開發(fā)語言,在C++引擎上層是DartFramework

Flutter不僅僅提供了一套視覺庫,在Flutter整體框架中包含各個層級階段的庫。例如實現(xiàn)一個游戲功能,上面一些游戲控件可以用上層視覺庫,底層游戲可以直接基于Flutter的底層庫進(jìn)行開發(fā),而不需要調(diào)用原生應(yīng)用的底層庫。Flutter的底層庫是基于Open GL實現(xiàn)的,所以Open GL可以做的Flutter都可以。

視覺庫

在上層Framework中包含兩套視覺庫,符合Android風(fēng)格的Material,和符合iOS風(fēng)格的Cupertino。也可以在此基礎(chǔ)上,封裝自己風(fēng)格的系統(tǒng)組件。Cupertino是一套iOS風(fēng)格的視覺庫,包含iOS的導(dǎo)航欄、button、alertView等。

Flutter對不同硬件平臺有不同的兼容,例如同樣的Material代碼運行在iOS和Android不同平臺上,有一些平臺特有的顯示和交互,Flutter依然對其進(jìn)行了區(qū)分適配。例如滑動ScrollView時,iOS平臺是有回彈效果的,而Android平臺則是阻尼效果。例如iOS的導(dǎo)航欄標(biāo)題是居中的,Android導(dǎo)航欄標(biāo)題是向左的,等等。這些Flutter都做了區(qū)分兼容。

除了Flutter為我們做的一些適配外,有一些控件是需要我們自己做適配的,例如AlertView,在Android和iOS兩個平臺下的表現(xiàn)就是不同的。這些iOS特性的控件都定義在Cupertino中,所以建議在進(jìn)行App開發(fā)時,對一些控件進(jìn)行上層封裝。

例如AlertView則對其進(jìn)行一個二次封裝,控件內(nèi)部進(jìn)行設(shè)備判斷并選擇不同的視覺庫,這樣可以保證各個平臺的效果。

iOS風(fēng)格
Android風(fēng)格

雖然Flutter對于iOS和Android兩個平臺,開發(fā)有cupertinomaterial兩個視覺庫,但實際開發(fā)過程中的選擇,應(yīng)該使用material當(dāng)做視覺庫。因為Flutter對iOS的支持并不是很好,主要對Android平臺支持比較好,material中的UI控件要比cupertino多好幾倍。

Dart

DartGoogle在2011年推出的一款應(yīng)用于Web開發(fā)的編程語言,Dart剛推出的時候,定位是替代JS做前端開發(fā),后來逐步擴(kuò)展到移動端和服務(wù)端。

Dart語言

DartFlutter的開發(fā)語言,Flutter必須遵循Dart的語言特性。在此基礎(chǔ)上,也會有自己的東西,例如Flutter的上層Framework,自己的渲染引擎等??梢哉f,Dart只是Flutter的一部分。

Dart是強(qiáng)類型的,對定義的變量不需要聲明其類型,Flutter會對其進(jìn)行類型推導(dǎo)。如果不想使用類型推導(dǎo),也可以自己聲明指定的類型。

Hot Reload

Flutter支持亞秒級熱重載,Android StudioVSCode都支持Hot Reload的特性。但需要區(qū)分的是,熱重載和熱更新是不同的兩個概念,熱重載是在運行調(diào)試狀態(tài)下,將新代碼直接更新到執(zhí)行中的二進(jìn)制。而熱更新是在上線后,通過Runtime或其他方式,改變現(xiàn)有執(zhí)行邏輯。

AOT、JIT

Flutter支持AOT(Ahead of time)和JIT(Just in time)兩種編譯模式,JIT模式支持在運行過程中進(jìn)行Hot Reload。刷新過程是一個增量的過程,由系統(tǒng)對本次和上次的代碼做一次snapshot,將新的代碼注入到DartVM中進(jìn)行刷新。但有時會不能進(jìn)行Hot Reload,此時進(jìn)行一次全量的Hot Reload即可。

AOT模式則是在運行前預(yù)先編譯好,這樣在每次運行過程中就不需要進(jìn)行分析、編譯,此模式的運行速度是最快的。Flutter同時采用了兩種方案,在開發(fā)階段采用JIT模式進(jìn)行開發(fā),在release階段采用AOT模式,將代碼打包為二進(jìn)制進(jìn)行發(fā)布。

在開發(fā)原生應(yīng)用時,每次修改代碼后都需要重新編譯,并且運行到硬件設(shè)備上。由于Flutter支持Hot Reload,可以進(jìn)行熱重載,對項目的開發(fā)效率有很大的提升。

由于Flutter實現(xiàn)機(jī)制支持JIT的原因,理論上來說是支持熱更新以及服務(wù)器下發(fā)代碼的??梢詮姆?wù)器。但是由于這樣會使性能變差,而且還有審核的問題,所以Flutter并沒有采用這種方案。

實現(xiàn)原理

Flutter的熱重載是基于State的,也就是我們在代碼中經(jīng)常出現(xiàn)的setState方法,通過這個來修改后,會執(zhí)行相應(yīng)的build方法,這就是熱重載的基本過程。

Flutterhot reload的實現(xiàn)源碼在下面路徑中,在此路徑中包含run_cold.dartrun_hot.dart兩個文件,前者負(fù)責(zé)冷啟動,后者負(fù)責(zé)熱重載。

~/flutter/packages/flutter_tools/lib/src/run_hot.dart

熱重載的代碼實現(xiàn)在run_hot.dart文件中,有HotRunner來負(fù)責(zé)具體代碼執(zhí)行。當(dāng)Flutter進(jìn)行熱重載時,會調(diào)用restart函數(shù),函數(shù)內(nèi)部會傳入一個fullRestartbool類型變量。熱重載分為全量和非全量,fullRestart參數(shù)就是表示是否全量。以非全量熱重載為例,函數(shù)的fullRestart會傳入false,根據(jù)傳入false參數(shù),下面是部分核心代碼。

Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
    if (fullRestart) {
        // .....
    } else {
        final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
        final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
        final Status status = logger.startProgress(
            '$progressPrefix hot reload...',
            progressId: 'hot.reload'
        );
        OperationResult result;
        try {
            result = await _reloadSources(pause: pauseAfterRestart, reason: reason);
        } finally {
            status.cancel();
        }
    }
}

調(diào)用restart函數(shù)后,內(nèi)部會調(diào)用_reloadSources函數(shù),去執(zhí)行內(nèi)部邏輯。下面是大概邏輯執(zhí)行流程。

執(zhí)行流程

_reloadSources函數(shù)內(nèi)部,會調(diào)用_updateDevFS函數(shù),函數(shù)內(nèi)部會掃描修改的文件,并將文件修改前后進(jìn)行對比,隨后會將被改動的代碼生成一個kernel files文件。

隨后會通過HTTP Server將生成的kernel files文件發(fā)送給Dart VM虛擬機(jī),虛擬機(jī)拿到kernel文件后會調(diào)用_reloadSources函數(shù)進(jìn)行資源重載,將kernel文件注入正在運行的Dart VM中。當(dāng)資源重載完成后,會調(diào)用RPC接口觸發(fā)Widgets的重繪。

跨平臺方案對比

現(xiàn)在市面上RN、Weex的技術(shù)方案基本一樣,所以這里就以RN來代表類似的跨平臺方案。Flutter是基于GPU進(jìn)行渲染的,而RN則將渲染交給原生平臺,而自己只是負(fù)責(zé)通過JSCore將視圖組織起來,并處理業(yè)務(wù)邏輯。所以在渲染效果和性能這塊,Flutter的性能比RN要強(qiáng)很多。

跨平臺方案一般都需要對各個平臺進(jìn)行平臺適配,也就是創(chuàng)建各自平臺的適配層,RN的平臺適配層要比Flutter要大很多。因為從技術(shù)實現(xiàn)來說,RN是通過JSCore引擎進(jìn)行原生代碼調(diào)用的,和原生代碼交互很多,所以需要更多的適配。而Flutter則只需要對各自平臺獨有的特性進(jìn)行適配即可,例如調(diào)用系統(tǒng)相冊、粘貼板等。

Flutter技術(shù)實現(xiàn)是基于更底層實現(xiàn)的,對平臺依賴度不是很高,相對來說,RN對平臺的依賴度是很高的。所以RN未來的技術(shù)升級,包括擴(kuò)展之類的,都會受到很大的限制。而Flutter未來的潛力將會很大,可以做很多技術(shù)改進(jìn)。

Widget

Flutter中將顯示以及和顯示相關(guān)的部分,都統(tǒng)一定義為widget,下面列舉一些widget包含的類型:

  1. 用于顯示的視圖,例如ListView、TextContainer等。
  2. 用來操作視圖,例如Transform等動畫相關(guān)。
  3. 視圖布局相關(guān),例如CenterExpanded、Column等。

Flutter中,所有的視圖都是由Widget組成,Label、AppBar、ViewController等。在Flutter的設(shè)計中,組合的優(yōu)先級要大于繼承,整體視圖類結(jié)構(gòu)繼承層級很淺但單層很多類。如果想定制或封裝一些控件,也應(yīng)該以組合為主,而不是繼承。

在iOS開發(fā)中,我也經(jīng)常采用這種設(shè)計方案,組合大于繼承。因為如果繼承層級過多的話,一個是不便于閱讀代碼,還有就是不好維護(hù)代碼。例如底層需要改一個通用的樣式,但這個類的繼承層級比較復(fù)雜,這樣改動的話影響范圍就比較大,會將一些不需要改的也改掉,這時候就會發(fā)現(xiàn)繼承很雞肋。但在iOS中有Category的概念,這也是一種組合的方式,可以通過將一些公共的東西放在Category中,使繼承的方便性和組合的靈活性達(dá)到一個平衡。

Flutter中并沒有單獨的布局文件,例如iOS的XIB這種,代碼都在Widget中定義。和UIView的區(qū)別在于,Widget只是負(fù)責(zé)描述視圖,并不參與視圖的渲染。UIView也是負(fù)責(zé)描述視圖,而UIViewlayer則負(fù)責(zé)渲染操作,這是二者的區(qū)別。

Widget結(jié)構(gòu)

了解Widget

在應(yīng)用程序啟動時,main方法接收一個Widget當(dāng)做主頁面,所以任何一個Widget都可以當(dāng)做根視圖。一般都是傳一個MaterialApp,也可以傳一個Container當(dāng)做根視圖,這都是被允許的。

Flutter應(yīng)用中,和界面顯示及用戶交互的對象都是由Widget構(gòu)成的,例如視圖、動畫、手勢等。Widget分為StatelessWidgetStatefulWidget兩種,分別是無狀態(tài)和有狀態(tài)的Widget。

StatefulWidget本質(zhì)上也是無狀態(tài)的,其通過State來處理Widget的狀態(tài),以達(dá)到有狀態(tài),State出現(xiàn)在整個StatefulWidget的生命周期中。

當(dāng)構(gòu)建一個Widget時,可以通過其build獲得構(gòu)建流程,在構(gòu)建流程中可以加入自己的定制操作,例如對其設(shè)置title或視圖等。

return Scaffold(
  appBar: AppBar(
    title: Text('ListView Demo'),
  ),
  body: ListView.builder(
    itemCount: dataList.length,
    itemBuilder: (BuildContext context, int index) {
      return Text(dataList[index]);
    },
  ),
);

有些Widget在構(gòu)建時,也提供一些參數(shù)來幫助構(gòu)建,例如構(gòu)建一個ListView時,會將index返回給build方法,來區(qū)別構(gòu)建的Cell,以及構(gòu)建的上下文context。

itemBuilder: (BuildContext context, int index) {
  return Text(dataList[index]);
}

StatelessWidget

StatelessWidget是一種靜態(tài)Widget,即創(chuàng)建后自身就不能再進(jìn)行改變。在創(chuàng)建一個StatelessWidget后,需要重寫build函數(shù)。每個靜態(tài)Widget都會有一個build函數(shù),在創(chuàng)建視圖對象時會調(diào)用此方法。同樣的,此函數(shù)也接收一個Widget類型的返回值。

class RectangleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center (
        // UI Code
    );
  }
}

StatefulWidget

Widget本質(zhì)上是不可被改變的,但StatefulWidget將狀態(tài)拆分到State中去管理,當(dāng)數(shù)據(jù)發(fā)生改變時由State去處理視圖的改變。

下面是創(chuàng)建一個動態(tài)Widget,當(dāng)創(chuàng)建一個動態(tài)Widget需要配合一個State,并且需要重寫createState方法。重寫此函數(shù)后,指定一個Widget對應(yīng)的State并初始化。

下面例子中,在StatefulWidget的父類中包含一個Key類型的key變量,這是無論靜態(tài)Widget還是動態(tài)Widget都具備的參數(shù)。在動態(tài)Widget中定義了自己的成員變量title,并在自定義的初始化方法中傳入,通過下面DynamicWidget類的構(gòu)造方法,并不需要在內(nèi)部手動進(jìn)行title的賦值,title即為傳入的值,是由系統(tǒng)完成的。

class DynamicWidget extends StatefulWidget {
  DynamicWidget({Key key, this.title}) : super (key : key);
  final String title;

  @override
  DynamicWidgetState createState() => new DynamicWidgetState();
}

由于上面動態(tài)Widget定義了初始化方法,在調(diào)用動態(tài)Widget時可以直接用自定義初始化方法即可。

DynamicWidget(key: 'key', title: 'title');

State

StatefulWidget的改變是由State來完成的,State中需要重寫build方法,在build中進(jìn)行視圖組織。StatefulWidget是一種響應(yīng)式視圖改變的方式,數(shù)據(jù)源和視圖產(chǎn)生綁定關(guān)系,由數(shù)據(jù)源驅(qū)動視圖的改變。

改變StatefulWidget的數(shù)據(jù)源時,需要調(diào)用setState方法,并將數(shù)據(jù)源改變的操作寫在里面。使用動態(tài)Widget后,是不需要我們手動去刷新視圖的。系統(tǒng)在setState方法調(diào)用后,會重新調(diào)用對應(yīng)Widgetbuild方法,重新繪制某個Widget

下面的代碼示例中添加了一個float按鈕,并給按鈕設(shè)置了一個回調(diào)函數(shù)_onPressAction,這樣在每次觸發(fā)按鈕事件時都會調(diào)用此函數(shù)。counter是一個整型變量并和Text相關(guān)聯(lián),當(dāng)counter的值在setState方法中改變時,Text Widget也會跟著變化。

class DynamicWidgetState extends State<DynamicWidget> {
  int counter = 0;
  void _onPressAction() {
    setState(() {
      counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Center(
        child: Text('Button tapped $_counter.')
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _onPressAction,
        tooltip: 'Increment',
        child: Icon(Icons.add)
      )
    );
  }  
}

主要Widget

在iOS中有UINavigationController的概念,其并不負(fù)責(zé)顯示,而是負(fù)責(zé)控制各個頁面的跳轉(zhuǎn)操作。在Flutter中可以將MaterialApp理解為iOS的導(dǎo)航控制器,其包含一個navigationBar以及導(dǎo)航棧,這和iOS是一樣的。

在iOS中除了用來顯示的視圖外,視圖還有對應(yīng)的UIViewController。在Flutter中并沒有專門用來管理視圖并且和View一對一的類,但從顯示的角度來說,有類似的類Scaffold,其包含控制器的appBar,也可以通過body設(shè)置一個widget當(dāng)做其視圖。

theme

themeFlutter提供的界面風(fēng)格API,MaterialApp提供有theme屬性,可以在MaterialApp中設(shè)置全局樣式,這樣可以統(tǒng)一整個應(yīng)用的風(fēng)格。

new MaterialApp(
  title: title,
  theme: new ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.lightBlue[800],
    accentColor: Colors.cyan[600],
  )
);

如果不想使用系統(tǒng)默認(rèn)主題,可以將對應(yīng)的控件或試圖用Theme包起來,并將Theme當(dāng)做Widget賦值給其他Widget。

new Theme(
  data: new ThemeData(
    accentColor: Colors.yellow,
  ),
  child: new FloatingActionButton(
    onPressed: () {},
    child: new Icon(Icons.add),
  ),
);

有時MaterialApp設(shè)定的統(tǒng)一風(fēng)格,并不能滿足某個Widget的要求,可能還需要有其他的外觀變化,可以通過Theme.of傳入當(dāng)前的BuildContext,來對theme進(jìn)行擴(kuò)展。

Flutter會根據(jù)傳入的context,順著Widget樹查找最近的Theme,并對Theme復(fù)制一份防止影響原有的Theme,并對其進(jìn)行擴(kuò)展。

new Theme(
  data: Theme.of(context).copyWith(accentColor: Colors.yellow),
  child: new FloatingActionButton(
    onPressed: null,
    child: new Icon(Icons.add),
  ),
);

網(wǎng)絡(luò)請求

Flutter中可以通過async、await組合使用,進(jìn)行網(wǎng)絡(luò)請求。Flutter中的網(wǎng)絡(luò)請求大體有三種:

  1. 系統(tǒng)自帶的HttpClient網(wǎng)絡(luò)請求,缺點是代碼量相對而言比較多,而且對post請求支持不是很好。
  2. 三方庫http.dart,請求簡單。
  3. 三方庫dio,請求簡單。

http網(wǎng)絡(luò)庫

http網(wǎng)絡(luò)庫定義在http.dart中,內(nèi)部代碼定義很全,包括HttpStatus、HttpHeaders、Cookie等很多基礎(chǔ)信息,有助于我們了解http請求協(xié)議。

因為是三方庫,所以需要在pubspec.yaml中加入下面的引用。

http: '>=0.11.3+12'

下面是http.dart的請求示例代碼,可以看到請求很簡單,真正的請求代碼其實就兩行。生成一個Client請求對象,調(diào)用client實例的get方法(如果是post則調(diào)用post方法),并用Response對象去接收請求結(jié)果即可。

通過async修飾發(fā)起請求的方法,表示這是一個異步操作,并在請求代碼的前面加入await,修飾這里的代碼需要等待數(shù)據(jù)返回,需要過一段時間后再處理。

請求回來的數(shù)據(jù)默認(rèn)是json字符串,需要對其進(jìn)行decode并解析為數(shù)據(jù)對象才可以使用,這里使用系統(tǒng)自帶的convert庫進(jìn)行解析,并解析為數(shù)組。

import 'package:http/http.dart' as http;

class RequestDemoState extends State<MyHomePage> {
  List dataList = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  // 發(fā)起網(wǎng)絡(luò)請求
  loadData() async{
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Response response = await client.get(requestURL);

    String jsonString = response.body;
    setState(() {
      // 數(shù)據(jù)解析
      dataList = json.decode(jsonString);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title)
      ),
      body: ListView.builder(
        itemCount: dataList.length,
        itemBuilder: (BuildContext context, int index) {
          return Text(dataList[index]['title']);
        },
      ),
    );
  }
}

在調(diào)用Client進(jìn)行post數(shù)據(jù)請求時,需要傳入一個字典進(jìn)去,Client會通過將字典當(dāng)做post的from表單。

 void requestData() async {
    var params = Map<String, String>();
    params["username"] = "lxz";
    params["password"] = "123456";

    var client = http.Client();
    var response = await client.post(url_post, body: params);
    _content = response.body;
}

dio網(wǎng)絡(luò)庫

dio庫的調(diào)用方式和http庫類似,這里不過多介紹。dio庫相對于http庫強(qiáng)大的在于,dio庫提供了更好的Cookie管理、文件的上傳下載、fromData表單等處理。所以,如果對網(wǎng)絡(luò)庫需求比較復(fù)雜的話,還是建議使用dio

// 引入外部依賴
dio: ^1.0.9

數(shù)據(jù)解析

convert

系統(tǒng)自帶有convert解析庫,在使用時直接import即可。convert類似于iOS自帶的JSON解析類NSJSONSerialization,可以直接將json字符串解析為字典或數(shù)組。

import 'dart:convert';
// 解析代碼
dataList = json.decode(jsonString);

但是,我們在項目中使用時,一般都不會直接使用字典取值,這是一種很不好的做法。一般都會將字典或數(shù)組轉(zhuǎn)換為模型對象,在項目中使用模型對象??梢远x類似Model.dart這樣的模型類,并在模型類中進(jìn)行數(shù)據(jù)解析,對外直接暴露公共變量來讓外界獲取值。

自動序列化

但如果定義模型類的話,一個是要在代碼內(nèi)部寫取值和賦值代碼,這些都需要手動完成。另外如果當(dāng)服務(wù)端字段發(fā)生改變后,客戶端也需要跟著進(jìn)行改變,所以這種方式并不是很靈活。

可以采用json序列化的三方庫json_serializable,此庫可以將一個類標(biāo)示為自動JSON序列化的類,并對類提供JSON和對象相互轉(zhuǎn)換的能力。也可以通過命令行開啟一個watch,當(dāng)類中的變量定義發(fā)生改變時,相關(guān)代碼自動發(fā)生改變。

首先引入下面的三個庫,其中包括依賴庫一個,以及調(diào)試庫兩個。

dependencies:
  json_annotation: ^2.0.0

dev_dependencies:
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

定義一個模型文件,例如這里叫做User.dart文件,并在內(nèi)部定義一個User的模型類。隨后引入json_annotation的依賴,通過@JsonSerializable()標(biāo)示此類需要被json_serializable進(jìn)行合成。

定義的User類包含兩部分,實例變量和兩個轉(zhuǎn)換函數(shù)。在下面定義json轉(zhuǎn)換函數(shù)時,需要注意函數(shù)命名一定要按照下面格式命名,否則不能正常生成user.g.dart文件。

import 'package:json_annotation/json_annotation.dart';

// 定義合成后的新文件為user.g.dart
part 'user.g.dart';

@JsonSerializable()

class User {
  String name;
  int age;
  String email;
  
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

下面就是user.dart指定生成的user.g.dart文件,其中包含JSON和對象相互轉(zhuǎn)換的代碼。

part of 'user.dart';

User _$UserFromJson(Map<String, dynamic> json) {
  return User(
      json['name'] as String, json['age'] as int, json['email'] as String);
}

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'name': instance.name,
      'age': instance.age,
      'email': instance.email
    };

有的時候服務(wù)端返回的參數(shù)名和本地的關(guān)鍵字沖突,或者命名不規(guī)范,導(dǎo)致本地定義和服務(wù)器字段不同的情況。這種情況可以通過@JsonKey關(guān)鍵字,來修飾json字段匹配新的本地變量。除此之外,也可以做其他修飾,例如變量不能為空等。

@JsonKey(name: 'id')
final int user_id;

現(xiàn)在項目中依然是報錯的,隨后我們在flutter工程的根目錄文件夾下,運行下面命令。

flutter packages pub run build_runner watch

此命令的好處在于,其會在后臺監(jiān)聽模型類的定義,當(dāng)模型類定義發(fā)生改變后,會自動修改本地源碼以適配新的定義。以文中User類為例,當(dāng)User.dart文件發(fā)生改變后,使用Cmd+s保存文件,隨后VSCode會將自定改變user.g.dart文件的定義,以適配新的變量定義。

系統(tǒng)文件

主要文件

  • iOS文件:iOS工程文件
  • Android:Android工程文件
  • lib:Flutter的dart代碼
  • assets:資源文件夾,例如font、image等都可以放在里面
  • .gitignore:git忽略文件

packages

這是一個系統(tǒng)文件,Flutter通過.packages文件來管理一些系統(tǒng)依賴庫,例如material、cupertinowidgets、animationgesture等系統(tǒng)庫就在里面,這些主要的系統(tǒng)庫由.packages下的flutter統(tǒng)一管理,源碼都在flutter/lib/scr目錄下。除此之外,還有一些其他的系統(tǒng)庫或系統(tǒng)資源都在.packages中。

yaml文件

Flutter中通過pubspec.yaml文件來管理外部引用,包含本地資源文件、字體文件、依賴庫等依賴,以及應(yīng)用的一些配置信息。這些配置在項目中時,需要注意代碼對其的問題,否則會導(dǎo)致加載失敗。

當(dāng)修改yaml文件的依賴信息后,需要執(zhí)行flutter get packages命令更新本地文件。但并不需要這么麻煩,可以直接Cmd+s保存文件,VSCode編譯器會自動更新依賴。

// 項目配置信息
name: WeChat
description: Tencent WeChat App.
version: 1.0.0+1

// 常規(guī)依賴
dependencies:
  flutter:125864
    sdk: flutter
    cupertino_icons: ^0.1.2
    english_words: ^3.1.0

// 開發(fā)依賴
dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true
  // 圖片依賴
  assets:
    - assets/images/ic_file_transfer.png
    - assets/images/ic_fengchao.png

  // 字體依賴
  fonts:
    - family: appIconFont
      fonts:
        - asset: assets/fonts/iconfont.ttf

Flutter開發(fā)

啟動函數(shù)

和大多數(shù)編程語言一樣,dart也包含一個main方法,是Flutter程序執(zhí)行的主入口,在main方法中寫的代碼就是在程序啟動時執(zhí)行的代碼。main方法中會執(zhí)行runApp方法,runApp方法類似于iOS的UIApplicationMain方法,runApp函數(shù)接收一個Widget用來做應(yīng)用程序的顯示。

void main() {
    runApp()
    // code
}

生命周期

在iOS中通過AppDelegate可以獲取應(yīng)用程序的生命周期回調(diào),在Flutter中也可以獲取到。可以通過向Binding添加一個Observer,并實現(xiàn)didChangeAppLifecycleState方法,來監(jiān)聽指定事件的到來。

但是由于Flutter提供的狀態(tài)有限,在iOS平臺只能監(jiān)聽三種狀態(tài),下面是示例代碼。

class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    switch (state) {
      case AppLifecycleState.inactive:
        print('Application Lifecycle inactive');
        break;
      case AppLifecycleState.paused:
        print('Application Lifecycle paused');
        break;
      case AppLifecycleState.resumed:
        print('Application Lifecycle resumed');
        break;
      default:
        print('Application Lifecycle other');
    }
  }
}

矩陣變換

Flutter中是支持矩陣變化的,例如rotatescale等方式。Flutter的矩陣變換由Widget完成,需要進(jìn)行矩陣變換的視圖,在外面包一層Transform Widget即可,內(nèi)部可以設(shè)置其變換方式。

child: Container(
    child: Transform(
      child: Container(
        child: Text(
          "Lorem ipsum",
          style: TextStyle(color: Colors.orange[300], fontSize: 12.0),
          textAlign: TextAlign.center,
        ),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        padding: EdgeInsets.all(16.0),
      ),
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..rotateZ(15 * 3.1415927 / 180),
    ),
  width: 320.0,
  height: 240.0,
  color: Colors.grey[300],
)

Transform中可以通過transform指定其矩陣變換方式,通過alignment指定變換的錨點。

頁面導(dǎo)航

在iOS中可以通過UINavigationController對頁面進(jìn)行管理,控制頁面間的push、pop跳轉(zhuǎn)。Flutter中使用NavigatorRouters來實現(xiàn)類似UINavigationController的功能,Navigator負(fù)責(zé)管理導(dǎo)航棧,包含push、pop的操作,可以把UIViewController看做一個RoutersRoutersNavigator管理著。

Navigator的跳轉(zhuǎn)方式分為兩種,一種是直接跳轉(zhuǎn)到某個Widget頁面,另一種是為MaterialApp構(gòu)建一個map,通過key來跳轉(zhuǎn)對應(yīng)的Widget頁面。map的格式是key : context的形式。

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

跳轉(zhuǎn)時通過pushNamed指定map中的key,即可跳轉(zhuǎn)到對應(yīng)的Widget。如果需要從push出來的頁面獲取參數(shù),可以通過await修飾push操作,這樣即可在新頁面pop的時候?qū)?shù)返回到當(dāng)前頁面。

Navigator.of(context).pushNamed('/b');

Map coordinates = await Navigator.of(context).pushNamed('/location');
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

編碼規(guī)范

VSCode有很好的語法檢查,如果有命名不規(guī)范等問題,都會以警告的形式表現(xiàn)出來。

  1. 駝峰命名法,方法名、變量名等,都以首字母小寫的駝峰命名法。類名也是駝峰命名法,但類名首字母大寫。
  2. 文件名,文件命名以下劃線進(jìn)行區(qū)分,不使用駝峰命名法。
  3. Flutter中創(chuàng)建Widget對象,可以用new修飾,也可以不用。
child: new Container(
    child: Text(
      'Hello World',
      style: TextStyle(color: Colors.orange, fontSize: 15.0)
    )
)
  1. 函數(shù)中可以定義可選參數(shù),以及必要參數(shù)。

下面是一個函數(shù)定義,這里定義了一個必要參數(shù)url,以及一個Map類型的可選參數(shù)headers。

Future<Response> get(url, {Map<String, String> headers});
  1. Dart中在函數(shù)定義前加下劃線,則表示是私有方法或變量。
  2. Dart通過import引入外部引用,除此之外也可以通過下面的語法單獨引入文件中的某部分。
import "dart:collection" show HashMap, IterableBase;

=>調(diào)用

Dart中經(jīng)??梢钥吹?code>=>的調(diào)用方式,這種調(diào)用方式類似于一種語法糖,下面是一些常用的調(diào)用方式。

當(dāng)進(jìn)行函數(shù)調(diào)用時,可以將普通函數(shù)調(diào)用轉(zhuǎn)換為=>的調(diào)用方式,例如下面第一個示例。在此基礎(chǔ)上,如果調(diào)用函數(shù)只有一個參數(shù),可以將其改為第二個示例的方式,也就是可以省略調(diào)用的括號,直接寫參數(shù)名。

(單一參數(shù)) => {函數(shù)聲明}
elements.map((element) => {
  return element.length;
});

單一參數(shù) => {函數(shù)聲明}
elements.map(element => {
 return element.length;
});

當(dāng)只有一個返回值,并且沒有邏輯處理時,可以直接省略return,返回數(shù)值。

(參數(shù)1, 參數(shù)2, …, 參數(shù)N) => 表達(dá)式
elements.map(element => element.length);

當(dāng)調(diào)用的函數(shù)中沒有參數(shù)時,可以直接省略參數(shù),寫一對空括號即可。

() => {函數(shù)實現(xiàn)}

小技巧

代碼重構(gòu)

VSCode支持對Dart語言進(jìn)行重構(gòu),一般作用范圍都是在函數(shù)內(nèi)小范圍的。

例如在創(chuàng)建Widget對象的地方,將鼠標(biāo)焦點放在這里,當(dāng)前行的最前面會有提示。點擊提示后會有下面兩個選項:

  • Extract Local Variable
    將當(dāng)前Widget及其子Widget創(chuàng)建的代碼,剝離到一個變量中,并在當(dāng)前位置使用這個變量。
  • Extract Method
    將當(dāng)前Widget及其子Widget創(chuàng)建的代碼,封裝到一個函數(shù)中,并在當(dāng)前位置調(diào)用此函數(shù)。

除此之外,將鼠標(biāo)焦點放在方法的一行,點擊最前面的提示,會出現(xiàn)下面兩個選項:

  • Convert to expression body
    將當(dāng)前函數(shù)轉(zhuǎn)換為一個表達(dá)式。
  • Convert to async function body
    將當(dāng)前函數(shù)轉(zhuǎn)換為一個異步線程中執(zhí)行的代碼。

附加效果

Dart中添加任何附加效果,例如動畫效果或矩陣轉(zhuǎn)換,除了直接給Widget子類的屬性賦值外,就是在被當(dāng)前Widget外面包一層,就可以使當(dāng)前Widget擁有對應(yīng)的效果。

// 動畫效果
floatingActionButton: FloatingActionButton(
    tooltip: 'Fade',
    child: Icon(Icons.brush),
    onPressed: () {
      controller.forward();
    },
),

// 矩陣轉(zhuǎn)換
Transform(
  child: Container(
    child: Text(
      "Lorem ipsum",
      style: TextStyle(color: Colors.orange[300], fontSize: 12.0),
      textAlign: TextAlign.center,
    )
  ),
  alignment: Alignment.center,
  transform: Matrix4.identity()
    ..rotateZ(15 * 3.1415927 / 180),
),

快捷鍵(VSCode)

  • Cmd + Shift + p:可以進(jìn)行快速搜索。需要注意的是,默認(rèn)是帶有一個>的,這樣搜索結(jié)果主要是dart代碼。如果想搜索其他配置文件,或者安裝插件等操作,需要把>去掉。
  • Cmd + Shift + o:可以在某個文件中搜索某個類,但前提是需要提前進(jìn)入這個文件。例如進(jìn)入framework.dart,搜索StatefulWidget類。

注意點

  • 使用Flutter要注意代碼縮進(jìn),如果縮進(jìn)有問題可能會影響最后的結(jié)果,尤其是在.yaml中寫配置文件的時候。
  • 因為Flutter是開源的,所以遇到問題后可以進(jìn)入源碼中,找解決方案。
  • 在代碼中要注意標(biāo)點符號的使用,例如第二個創(chuàng)建Stack的代碼,如果上面是以逗號結(jié)尾,則后面的創(chuàng)建會失敗,如果上面是以分號結(jié)尾則沒問題。
Widget unreadMsgText = Container(
    width: Constants.UnreadMsgNotifyDotSize,
    height: Constants.UnreadMsgNotifyDotSize,
    child: Text(
      conversation.unreadMsgCount.toString(),
      style: TextStyle(
        color: Color(AppColors.UnreadMsgNotifyTextColor),
        fontSize: 12.0
      ),
    ),
  );
  
  avatarContainer = Stack(
    overflow: Overflow.visible,
    children: <Widget>[
      avatar
    ],
  );

簡書由于排版的問題,閱讀體驗并不好,布局、圖片顯示、代碼等很多問題。所以建議到我Github上,下載Flutter編程指南 PDF合集。把所有Flutter文章總計三篇,都寫在這個PDF中,而且左側(cè)有目錄,方便閱讀。

Flutter編程指南

下載地址:Flutter編程指南 PDF
麻煩各位大佬點個贊,謝謝!??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容