1. 程式人生 > >一個Android開發快速入門Flutter (一)

一個Android開發快速入門Flutter (一)

目錄

一個Java開發快速入門Dart

Flutter使用簡報

一個Android開發快速入門Flutter

前言

    在閱讀本篇之前,最好已經對Flutter程式碼結構有一個初步體驗,上面目錄中的兩篇文章是個不錯的開頭。

    另外,本篇需要你對於Android開發有一定經驗,文章內容是筆者對於這篇的學習筆記,如果你是ios開發或者前端開發官網同樣有類似入門教程。

View

    在android中我們的介面都是通過view來組成的,而在Flutter中我們的介面,甚至整個app都是通過元件Widget來組成的。從這個層面上來說,android中的view可能就等於Flutter中的widget。

    但是這裡要討論的更多的是view和widget的不同點。首先在android中我們的介面採用的是命令模式,而在Flutter中介面採用的是宣告模式(Flutter使用簡報中有介紹。)

    view和widget的宣告週期就存在很大的差異,在android中view例項會一直存在,直到介面銷燬,或者物件不再被使用。而widget的宣告週期更加短暫,一旦這個widget需要改變了,那麼這個widget就會被銷燬,會重新生成新的widget(對就是這麼直接)。你可能會疑惑,每次都重新生成widget不會太耗效能嗎?關鍵就在於Flutter中的widget是輕量級了,不可變的,如果更確切的來說,widget就像是xml中的一個元件定義,本身並不是例項物件。而整個xml檔案就是

Flutter中的widget tree(就是元件的各種巢狀)

    在Flutter中預設包含了Material和Cupertino兩套風格的元件(當然可以自定義其他風格)。前套是android的風格,後一套是ios的風格。

Stateless和Stateful元件

    關於StatelessWidget和StatefulWidget元件在Flutter使用簡報中已經有過一定嘗試,這裡我們再重新拿出來說下,主要原因是這個東西確實算得上是Flutter的設計核心。

    Flutter中的widget就像是設計模式中的狀態模式,他會有一個特定的狀態類來維護元件當然的狀態——State<?>。當然對於StatelessWidget而言,就是缺失了這個State類,這就導致它不會儲存自己的狀態,也就是我們所說的不可變。

    這裡有個東西需要注意,Widget是可以巢狀的,比如在listview元件中巢狀listlite元件。如果listlite元件內部有一個StatefulWidget元件,當這個元件狀態變化的時候,實際上listlite元件並沒有狀態變化,所以它依然可以是StatelessWidget。

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

    Text一個最基礎的StatelessWidget,他沒有狀態,文字一旦設定之後就無法改變。那麼如果我們需要根據點選事件改變文字呢?我們就需要重寫一個繼承與StatefulWidget的新元件。

PS:糾結了下還是把全部程式碼貼上來了

import 'package:flutter/material.dart';

void main() {
  runApp(SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  //關鍵在這裡,修改文字內容,並且呼叫setState,該方法會重新呼叫Build
  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      //新增一個懸浮按鈕,該按鈕點選後觸發_updateText方法
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

    另外在android中我們可以通過addChild和removeChild來動態新增和刪除元件,但是在Flutter中由於元件都是不可變的,所以並不存在直接的方法來動態改變,所以的改變都是通過State。比如這個需求,我們可以通過一個bool變數來控制,在不同情況下,build方法輸出不同的元件。(這裡個人認為是Flutter比較不討人喜歡的一個方法,程式碼量會增加,並且會多出很多標誌位變數,這回增加閱讀的難度,當存在過多的標誌位時,你得思考你的家庭住址是否會被以後接手你程式碼的人知道。)

    

Flutter動畫

    基本上android擁有的動畫型別Flutter中都有,並且相比較於android中零碎的實現方式,Flutter中可能更成體系一些。本節我們僅僅是對Flutter動畫做一個初步瞭解,後續可能會有其他部落格專門分享動畫的學習。如果你已經迫不及待想要學習動畫的話

    https://flutter.io/docs/development/ui/animations

    https://flutter.io/docs/reference/widgets/animation

    以上兩個官方文件是不錯的起點。

    我們知道在android中使用xml檔案或者animate()方法對view進行相關的動畫。

    PS:下面程式碼有個with關鍵字,可以檢視這裡瞭解用法。

    在Flutter中使用AnimationController物件來進行動畫的控制,包括暫停,定位,停止,回播。他需要一個Ticker引數用於在vsync(幀繪製訊號)發生的時候生成一個0~1之間的線性插值。

    然後我們建立一個Animation(這裡有點像插值器,根據controller中的0~1線性值,然後再根據當前插值器計算最終值)繫結這個Controller。

    最後建立一個動畫型別的Widget,包含真正需要動畫的Widget,傳入上面建立的Animation。

// //使用了material的包,Flutter還提供一個cupertion包,用於表示谷歌和蘋果兩種介面風格
// //當然我們完全可以自定義自己的風格
// import 'package:flutter/material.dart';
// //引入了一個三方庫 english_words ,需要在pubspec.yaml中的dependencies: 下面新增 english_words: ^3.1.0
// import 'package:english_words/english_words.dart';

// //main是dart的主方法,主方法很簡單,呼叫系統方法runApp,執行一個App
// void main() => runApp(MyApp()); 

// //MyApp 就是我們的app了,它繼承與StatelessWidget元件
// class MyApp extends StatelessWidget {
//   @override
//   Widget build(BuildContext context) { //build是元件類的基本方法,用於構建這個元件的顯示樣式
//     return MaterialApp( //一個MaterialApp widget
//       title: 'Welcome to Flutter',
//       home: RandomWords(), //主介面是一個RandomWords型別的元件
//       theme: new ThemeData(primaryColor: Colors.white), //修改app主題
//     );
//   }
// }

// /*
//  * 為了實現StatefulWidget,必須要有一個State類,用於幫助StatefulWidget build它的顯示樣式。
//  * 實際上,大多數的業務邏輯都是在State中進行處理的。
//  */
// class RandomWordsState extends State<RandomWords>{
//   final _suggestions = <WordPair>[];
//   final _biggerFont = const TextStyle(fontSize: 18.0);
//   final Set<WordPair> _saved = new Set<WordPair>(); //用於儲存已經點讚的名字
//   /*
//    * 用於建立一個ListView元件
//    */
//   Widget _buildSuggestions(){
//     //listview的builder工廠方法
//     return ListView.builder(padding: const EdgeInsets.all(16.0),
//       itemBuilder: (context,i){  /* dart寫法,類似於匿名類和lambda */
//         //如果是基數,返回分割線
//         if(i.isOdd) return Divider();
//         final index = i ~/ 2; //i除以2 並返回整數結果
//         if(index >= _suggestions.length){
//           _suggestions.addAll(generateWordPairs().take(10));
//         }
//         return _buildRow(_suggestions[index]);
//       }, 
//     );
//   }
//   /*
//    * 建立listview中的每一個item,每個item都是一個元件
//    */
//   Widget _buildRow(WordPair pair) {
//     final bool alreadySaved = _saved.contains(pair); //記錄是否已經標記過
//     return ListTile(
//       title: Text(
//         pair.asPascalCase,
//         style: _biggerFont,
//       ),
//       trailing: Icon(alreadySaved? Icons.favorite : Icons.favorite_border,
//             color: alreadySaved ? Colors.red:null,),//建立一個星形icon
//       //icon點選事件
//       onTap: (){setState(() { //使用setState來更新list中item的狀態,setState會先執行下面程式碼,然後再通知list更改item
//               if(alreadySaved){
//                 _saved.remove(pair);
//               }else{
//                 _saved.add(pair);
//               }
//             });},
//     );
//   }
//   /*
//    * 建立RandomWords元件的介面展示。 
//    */
//   @override
//   Widget build(BuildContext context) {
//     return Scaffold (
//       appBar: AppBar(
//         title: Text('Startup Name Generatorrrrrr'),
//         //為app bar增加左上按鈕
//         actions: <Widget>[IconButton(icon: const Icon(Icons.list),onPressed: _pushSaved)],
//       ),
//       body: _buildSuggestions(),
//     );
//   }
  
//   /*
//    * appbar 左上按鈕點選事件。使用Navigator 跳轉到新頁面
//    * Navigator 是Flutter中的路由管理器,通過push進行跳轉,通過remove進行返回
//    */
//   void _pushSaved(){
//     //在路由管理器 Navigator中push一個新的路由
//     //這個路由是一個頁面路由,通過builder構建這個頁面
//     Navigator.of(context).push(MaterialPageRoute<void>(builder: (context){
//       //dart語法,每一個被標星的單詞都會生成一個ListTile元件
//       final Iterable<ListTile> tiles = _saved.map(
//         (pair){
//           return new ListTile(
//             title: new Text(pair.asPascalCase,style:_biggerFont),
//           );
//         }
//       );
//       //在tiles元件之間插入分割線
//       final List<Widget> divided = ListTile.divideTiles(context: context,tiles: tiles).toList();
//       return new Scaffold(
//         appBar: new AppBar(title: const Text('Saved Suggestions'),),
//         body: new ListView(children: divided,),//生成listview
//       );
//     }));
//   }
// }

// /*
//  * 定義一個用於顯示隨機單詞的元件,這個元件是一個StatefulWidget元件,所以我們必須要提供一個State<RandomWords>的類
//  * StatefulWidget 內部必須重寫createState方法。他的widget的build方法交給這個State去寫。
//  */
// class RandomWords extends StatefulWidget{
//   @override
//   State<StatefulWidget> createState() {
//     return RandomWordsState();
//   }
// }

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fade Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

/**
 * 使用了 with,相當於多繼承。繼承了 TickerProvider
 */
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

/**
 * 這個元件被插入到widget tree的時候會被呼叫,必須要呼叫super,否則會報錯。
 */
  @override
  void initState() {
    super.initState();
    //建立animation控制器,需要傳入一個TickerProvider,該類已經使用with繼承,所以傳入this
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    //將controller繫結到一個動畫上,打個比方的話該物件就相當於手錶中的發條。
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: Container(
            //這是一個動畫widget,用來做透明度改變的。相當於手錶殼子
              child: FadeTransition(
                  opacity: curve, //給手錶殼子裝上發條
                  child: FlutterLogo( //真正需要做動畫的元件,手錶的指標。
                    size: 100.0,
                  )))),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();//呼叫controller,開始動畫。
        },
      ),
    );
  }
}

Canvas

    在android中,我們可以直接在view中操作canvas在畫布上畫上各種想要的東西,在Flutter中我們同樣可以直接使用畫布,而且底層使用的渲染引擎是skia,這也是android使用的圖形引擎。

    在Flutter中主要的類是CustomPaint和CustomPainter,前者表示一個widget元件,這個元件需要一個CustomPainter引數,這個引數中有一個回撥方法paint,我們就可以在這個方法中通過引數canvas進行繪製。

    一下程式碼是一個簽名版,還是比較有參考意義的,對於Canvas的使用說明非常具有代表性。

// //使用了material的包,Flutter還提供一個cupertion包,用於表示谷歌和蘋果兩種介面風格
// //當然我們完全可以自定義自己的風格
// import 'package:flutter/material.dart';
// //引入了一個三方庫 english_words ,需要在pubspec.yaml中的dependencies: 下面新增 english_words: ^3.1.0
// import 'package:english_words/english_words.dart';

// //main是dart的主方法,主方法很簡單,呼叫系統方法runApp,執行一個App
// void main() => runApp(MyApp()); 

// //MyApp 就是我們的app了,它繼承與StatelessWidget元件
// class MyApp extends StatelessWidget {
//   @override
//   Widget build(BuildContext context) { //build是元件類的基本方法,用於構建這個元件的顯示樣式
//     return MaterialApp( //一個MaterialApp widget
//       title: 'Welcome to Flutter',
//       home: RandomWords(), //主介面是一個RandomWords型別的元件
//       theme: new ThemeData(primaryColor: Colors.white), //修改app主題
//     );
//   }
// }

// /*
//  * 為了實現StatefulWidget,必須要有一個State類,用於幫助StatefulWidget build它的顯示樣式。
//  * 實際上,大多數的業務邏輯都是在State中進行處理的。
//  */
// class RandomWordsState extends State<RandomWords>{
//   final _suggestions = <WordPair>[];
//   final _biggerFont = const TextStyle(fontSize: 18.0);
//   final Set<WordPair> _saved = new Set<WordPair>(); //用於儲存已經點讚的名字
//   /*
//    * 用於建立一個ListView元件
//    */
//   Widget _buildSuggestions(){
//     //listview的builder工廠方法
//     return ListView.builder(padding: const EdgeInsets.all(16.0),
//       itemBuilder: (context,i){  /* dart寫法,類似於匿名類和lambda */
//         //如果是基數,返回分割線
//         if(i.isOdd) return Divider();
//         final index = i ~/ 2; //i除以2 並返回整數結果
//         if(index >= _suggestions.length){
//           _suggestions.addAll(generateWordPairs().take(10));
//         }
//         return _buildRow(_suggestions[index]);
//       }, 
//     );
//   }
//   /*
//    * 建立listview中的每一個item,每個item都是一個元件
//    */
//   Widget _buildRow(WordPair pair) {
//     final bool alreadySaved = _saved.contains(pair); //記錄是否已經標記過
//     return ListTile(
//       title: Text(
//         pair.asPascalCase,
//         style: _biggerFont,
//       ),
//       trailing: Icon(alreadySaved? Icons.favorite : Icons.favorite_border,
//             color: alreadySaved ? Colors.red:null,),//建立一個星形icon
//       //icon點選事件
//       onTap: (){setState(() { //使用setState來更新list中item的狀態,setState會先執行下面程式碼,然後再通知list更改item
//               if(alreadySaved){
//                 _saved.remove(pair);
//               }else{
//                 _saved.add(pair);
//               }
//             });},
//     );
//   }
//   /*
//    * 建立RandomWords元件的介面展示。 
//    */
//   @override
//   Widget build(BuildContext context) {
//     return Scaffold (
//       appBar: AppBar(
//         title: Text('Startup Name Generatorrrrrr'),
//         //為app bar增加左上按鈕
//         actions: <Widget>[IconButton(icon: const Icon(Icons.list),onPressed: _pushSaved)],
//       ),
//       body: _buildSuggestions(),
//     );
//   }
  
//   /*
//    * appbar 左上按鈕點選事件。使用Navigator 跳轉到新頁面
//    * Navigator 是Flutter中的路由管理器,通過push進行跳轉,通過remove進行返回
//    */
//   void _pushSaved(){
//     //在路由管理器 Navigator中push一個新的路由
//     //這個路由是一個頁面路由,通過builder構建這個頁面
//     Navigator.of(context).push(MaterialPageRoute<void>(builder: (context){
//       //dart語法,每一個被標星的單詞都會生成一個ListTile元件
//       final Iterable<ListTile> tiles = _saved.map(
//         (pair){
//           return new ListTile(
//             title: new Text(pair.asPascalCase,style:_biggerFont),
//           );
//         }
//       );
//       //在tiles元件之間插入分割線
//       final List<Widget> divided = ListTile.divideTiles(context: context,tiles: tiles).toList();
//       return new Scaffold(
//         appBar: new AppBar(title: const Text('Saved Suggestions'),),
//         body: new ListView(children: divided,),//生成listview
//       );
//     }));
//   }
// }

// /*
//  * 定義一個用於顯示隨機單詞的元件,這個元件是一個StatefulWidget元件,所以我們必須要提供一個State<RandomWords>的類
//  * StatefulWidget 內部必須重寫createState方法。他的widget的build方法交給這個State去寫。
//  */
// class RandomWords extends StatefulWidget{
//   @override
//   State<StatefulWidget> createState() {
//     return RandomWordsState();
//   }
// }

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatelessWidget {
  Widget build(BuildContext context) => Scaffold(body: Signature());
}

class Signature extends StatefulWidget {
  SignatureState createState() => SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    //讀取手勢,記錄手勢經過的點
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      //包含一個畫板元件
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}
//畫板元件內部的畫布。
class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

自定義元件

    在android中我們可能會自定義view,但是在Flutter中我們並不會自定義view,而是將各種基礎的view通過組合生成一個新的widget,這有點像android中繼承viewgroup。

 

Intents

    在Flutter中使用Navigator和Rounte來進行頁面跳轉,但是實際上Flutter中還是可以使用android中的intents,通過這個外掛。不過這裡,我們還是集中於Flutter提供的Navigator。

    另外,我們需要知道Flutter中通過Navigator進行跳轉實際上並不是跳轉activity和fragment,所有的跳轉場景和介面都在同一個activity中

    所謂的Route實際上是一個抽象的介面或者頁面,而Navigator是一個控制和排程多個Route的元件(是的,他也是一個widget)。工作在Navigator中呼叫push或者pop進行Route頁面之間的跳轉。

    Flutter中有兩種註冊route的方式。

    直接navigate,這個在上一篇初體驗中我們已經做過了這個嘗試。

    在App中通過routes引數進行註冊,然後之後直接呼叫。這就和在AndroidManifest中註冊activity很類似了,程式碼如下    

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'),
    },
  ));
}
//這樣呼叫
Navigator.of(context).pushNamed('/b');

 

在Flutter中解析外部傳入的Intent

    實際上這並不算是Flutter中特別定製的功能,Flutter中解析外部傳入的intent是需要native端接入的,通過MethodChannel通道進行本地訪問。MethodChannel是用來在Flutter和native雙端進行相互的方法呼叫的。本篇的主要目的是瞭解在Flutter中呼叫native中提供的方法。必須要知道的是,方法呼叫都是通過非同步完成的。實際上Flutter呼叫native的步驟是這樣的,通過Flutter向native端傳送訊息,native端接收到訊息,執行相關api,然後再通過訊息回傳給flutter.

    以下是一個從flutter呼叫native的例子(在native端呼叫flutter理論上也是一樣的,因為flutter中同樣提供了setMethodCallHandler方法,但是感覺上同一個名字的MethodChannel 可能不能用來雙向通訊)。

package com.example.myapp;

import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
  private String mShareText;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }
    //建立一個MethodChannel,這個MethodChannel有一個名字,叫做 "app.channel.shared.data"
    MethodChannel methodChannel = new MethodChannel(getFlutterView(), "app.channel.shared.data");
    //使用 setMethodCallHandler 為這個MethodChannel指定一個方法處理函式。
    //同名不同例項methodChannel,或者同一個例項,多次呼叫該方法,以最後一次呼叫為準(實際上內部是一個map)
    methodChannel.setMethodCallHandler(
      new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          if (call.method.contentEquals("getSharedText")) { //當接收到getShareText訊息的時候
            result.success(sharedText);
            sharedText = null;
          }
        }
      });
  }


  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample Shared App Handler',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  //建立一個名字為 app.channel.shared.data 的 MethodChannel
  static const platform = const MethodChannel('app.channel.shared.data');
  String dataShared = "No data";

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }
  //通過 invokeMethod 在MethodChannel傳送"getSharedText" 訊息,並且等待結果
  //由於方法呼叫是非同步的,所以需要使用dart語言提供的async和await功能
  getSharedText() async {
    var sharedData = await platform.invokeMethod("getSharedText");
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

    關於native通訊這篇部落格並沒有繼續深入,我們可以在這裡找到一些資料,但是官方文件也沒有列出太多資訊,後續可能會專門寫一篇關於native通訊的博文。

 

如何代替startActivityForResult

    我們已經知道Navigator用於跳轉了,這裡還缺少一個Android中的關鍵點就是如何替代startActivityForResult這種有返回值的跳轉?

    方法很簡單,跳轉端需要使用非同步呼叫,然後等待返回值:

Map coordinates = await Navigator.of(context).pushNamed('/location');

    然後再'/location'這個Route返回是呼叫pop並且傳入想要返回的引數。

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

    需要注意,上面的跳轉使用await非同步呼叫,這意味著包含他的方法也需要新增async標記。

 

替代runOnUiThread()

    首先dart語言是一個單執行緒模型語言,也就是說dart的程式碼都是通過event loop執行在主執行緒中。Flutter中的event loop就是android中的 main looper,也就是我們的UI執行緒。

    單執行緒模型並不意味著所有的程式碼都是block形式執行,dart提供了async/await來實現類似執行緒的功能(類似於koltin中的協程,協程很好用的,c++20之後也會提供該官方支援,java竟然還沒有!)。

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

    比如上面在await等待http返回的時候,ui執行緒並沒有被掛起。

Flutter多執行緒

    對於協程,或者上面提到的 async/await而言,適合做io操作,或者網路操作。如果真的需要做一些計算密集型操作,或者對於cpu高負載的操作的時候,使用攜程並不合適(攜程的執行高度依賴cpu排程機制,如果cpu負載高,主ui的被排程的時間片就會少,這個時候就出出現卡頓現象。)

    所以Flutter也提供了執行緒的概念 Isolate。Isolate可以利用cpu的多個核心來執行長時間執行或者計算密集型任務。

    Isolate是隔離的獨立執行執行緒,不和主執行的記憶體共享任何堆疊。這意味著我們無法訪問主執行緒中定義的任何變數(比如靜態變數等),或者使用setState來更新UI.

import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _SampleAppPageState();
  }
}

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  Widget getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  Widget getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  bool showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(
        padding: EdgeInsets.all(10.0),
        child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    //建立一個 ReceivePort 用於接收從Isolate 中傳過來的資料
    ReceivePort receivePort = ReceivePort();
    //建立一個Isolate 執行dataLoader方法,輸入一個receivePort.sendPort 引數
    //receivePort.sendPort 表示這個ReceivePort的資料填入工具SendPort,所有資料都從這個SendPort填入,然後再receivePort中讀取
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'dataLoader' isolate sends its SendPort as the first message
    //從 receivePort 中讀取第一個資料,讀取後這個port就關閉了,不能再使用。
    //dataLoader 傳過來的第一個message 是一個SendPort,用於向它傳送資料
    SendPort sendPort = await receivePort.first;
    //呼叫sendReceive 向sendPort 傳送資料
    List msg = await sendReceive(
        sendPort, "https://jsonplaceholder.typicode.com/posts");

    setState(() {
      widgets = msg;
    });
  }

  // the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();
    //向通道傳送第一個資料(自己的資料通道傳送口)
    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);
    //迴圈等待收到資料
    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(json.decode(response.body));
    }
  }
  //向sendport 傳送資料,並且建立一個新的資料通道ReceivePort,準備接收返回值
  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

    使用ReceivePort和SendPort(這兩個一定成對出現,一個ReceivePort,對應一個SendPort,在SendPort中填入資料,在ReceivePort中讀取資料,一般只會用在Isolate中。)

OKHttp替代品

     https://pub.dartlang.org/packages/http    

    上面地址是Flutter的http外掛,可以用於普通的post和get請求。但是相比於okhttp,功能方面還是少了很多的。如果你覺得http外掛無法滿足你的需求的時候,可以嘗試使用native的方法,然後通過channel呼叫native。

 

Flutter 資源的儲存和使用

    首先在Android中我們有resources和assets的區別,但是在Flutter中,就沒有這兩個的區別了,所有的都統稱為資源。另外,在Flutter對於資源的位置沒有特別規定,我們可以放在自己喜歡的資料夾名字下面。

    在Android中不同螢幕密度在資源使用的時候系統會選擇不同的資料夾,而在Flutter中使用的方式和ios更加相似,使用1.0x 2.0x這樣的方式。

ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

    上表是Android端和Flutter中的對比。

    假設,我們存在資源my_icon.png,將他放入以下資料夾中

images/my_icon.png       // Base: 1.0x image 被稱為main assets
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

    然後再pubspec.yaml中註冊資源

assets:
 - images/my_icon.png

    之後我們在Flutter中使用這個資源的時候,Flutter就會根據螢幕密度自動選用適合的圖示。如果沒有適合圖示,就會選擇其他圖示,並進行一定比例的縮放,這和android的處理系統是一樣。

    我們可以把它作為一個AssetImage

return AssetImage("images/my_icon.png");

    也可以直接在Image Widget中使用

@override
Widget build(BuildContext context) {
  return Image.asset("images/my_icon.png");
}

    注:這裡只是粗略的說明,更加具體的資源使用,可以參考這篇官方文件

Flutter與Android的String資源系統

    很遺憾,目前Flutter並不支援類似android的文字資源系統,我們能做的就是將所有文字手動定義到一個類中,變成靜態變數,然後再其他地方呼叫(是的,非常原始)。

    另外,Flutter也沒有國際化的功能,不能根據語言自動選擇資源版本(連資源都沒有,選個屁啊)。

    但是有一個外掛能夠用於本地化 intl package