Flutter筆記:給Android開(kāi)發(fā)者的Flutter指南

導(dǎo)引

本文原文在這里:Flutter for Android developers
本文屬于半翻譯半筆記注解,按照自己的描述整合了原文,如若讀者讀過(guò)原文會(huì)發(fā)現(xiàn)有些許不一樣不要驚訝,但仍然完整表達(dá)了原文的意思,文中斜體加粗部分是我自己的注解。


前言

Android的知識(shí)和技能對(duì)Flutter開(kāi)發(fā)很有價(jià)值,因?yàn)镕lutter依賴于移動(dòng)系統(tǒng)的眾多功能和配置。Flutter構(gòu)建App的UI的一種新方式,它有一個(gè)插件系統(tǒng)去連接Android/IOS原生系統(tǒng)來(lái)完成非UI的任務(wù),如果你精通Android,你可以不用學(xué)習(xí)任何東西直接使用Flutter(當(dāng)然需要看完這篇指南)

Views

在Flutter中什么東西相當(dāng)于View

在Android里,View是所有布局的基礎(chǔ),所有的控件都繼承于View。在Flutter中,大致相當(dāng)于View的是Widget。Widget不能和View劃上等號(hào),當(dāng)你熟悉了Flutter的開(kāi)發(fā)后你可以將它們視為你聲明和構(gòu)建UI的方式。

Widget與View主要有這幾點(diǎn)不同:

  1. Widgets一經(jīng)創(chuàng)建,是不可改變的一直存活到他們必須改變?yōu)橹?。?dāng)Widgets或者他們的狀態(tài)發(fā)生改變,F(xiàn)lutter會(huì)創(chuàng)建新的Widget樹(shù)的實(shí)例,而View則是繪制完成后不會(huì)再重繪直到invalidate被調(diào)用。
  2. Widgets是輕量級(jí)的,部分原因在于他們的不變性。他們不能直接繪制任何東西,而是對(duì)UI及其語(yǔ)義的描述,會(huì)通過(guò)"inflated"生成真實(shí)的視圖對(duì)象中去。
    Flutter包括 Material Components library.這些Widgets實(shí)現(xiàn)了Material Design guidelines.Material Design 是一個(gè)包括iOS在內(nèi)的所有平臺(tái)進(jìn)行優(yōu)化的靈活的設(shè)計(jì)。
    但是Flutter的靈活性和表現(xiàn)力足以實(shí)現(xiàn)任何設(shè)計(jì)語(yǔ)言。例如,在iOS上,您可以使用 Cupertino widgets生成一個(gè)類似于 Apple’s iOS design language的界面。

怎樣更新Widgets

在Android中可以直接改變View,但是在Flutter里,Widgets 是不可以直接更新的,取而代之的你需要去改變Widgets的狀態(tài)。
這就是有狀態(tài)和無(wú)狀態(tài)Widgets的概念來(lái)源。 StatelessWidget聽(tīng)起來(lái)就像是一個(gè)沒(méi)有狀態(tài)信息的Widget.
當(dāng)你描述的UI里有一些部分不依賴于對(duì)象中的配置信息時(shí), StatelessWidget非常有用。
例如,在Android中,這類似于放置Logo的ImageView。Logo在運(yùn)行時(shí)不會(huì)更改,所以在Flutter中可以使用 StatelessWidget.
如果你想根據(jù)HTTP請(qǐng)求或用戶交互后接收到的數(shù)據(jù)動(dòng)態(tài)更改UI,那么你必須使用StatefulWidget,并告訴Flutter widget的狀態(tài)已經(jīng)更新,以便它可以更新該widget。
這里需要注意的重要一點(diǎn)是,無(wú)狀態(tài)和有狀態(tài)widget的行為都是相同的,每一幀都會(huì)重建,區(qū)別在于StatefulWidget有一個(gè)State對(duì)象,它跨越幀時(shí)序,并且恢復(fù)StatefulWidget的狀態(tài)。
如果你有疑問(wèn),那么請(qǐng)始終記住這條規(guī)則:如果widget發(fā)生更改(例如,由于用戶交互),那么它是有狀態(tài)的。但是,如果widget對(duì)更改作出反應(yīng),但它的父widget仍然可以是無(wú)狀態(tài)的,如果它不需要更改作出反應(yīng)。
下面的例子使用了一個(gè)StatelessWidget.一個(gè)常見(jiàn)的StatelessWidgetText.如果你去看Text的實(shí)現(xiàn)你會(huì)發(fā)現(xiàn)它是Statelesswidget的子類。

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

如你所見(jiàn),Text沒(méi)有與之關(guān)聯(lián)的狀態(tài),它只呈現(xiàn)在其構(gòu)造函數(shù)中傳遞的內(nèi)容。
但是,如果您想要?jiǎng)討B(tài)地更改“I Like Flutter”,例如在單擊FloatingActionButton時(shí)?
要實(shí)現(xiàn)這一點(diǎn),請(qǐng)將Text包裝在一個(gè)有StatefulWidget中,并在用戶單擊按鈕時(shí)更新它。
例如:

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";

  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)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

怎么布局以及XML在哪里

在Android中,使用XML編寫布局,但在Flutter中,使用控件樹(shù)編寫布局。
以下示例顯示如何使用一個(gè)padding部件:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: MaterialButton(
        onPressed: () {},
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

你可以查看Flutter用作布局的控件類型:widget catalog

怎么添加或者刪除一個(gè)布局中的控件

在Android中,可以對(duì)父布局調(diào)用addChild()removeChild()來(lái)動(dòng)態(tài)添加或刪除子布局。在Flutter中,因?yàn)榭丶遣豢勺兊模詻](méi)有與addChild()直接等價(jià)的方式。相反,可以將一個(gè)函數(shù)傳遞給返回小部件的父控件,并用一個(gè)boolean控制子控件的創(chuàng)建。
例如這里,你可以點(diǎn)擊FloatingActionButton來(lái)切換兩個(gè)控件:

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 value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return MaterialButton(onPressed: () {}, child: Text('Toggle Two'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

怎么給控件添加動(dòng)畫呢

在Android中,可以使用XML創(chuàng)建動(dòng)畫,也可以在View上調(diào)用animate()方法。在Flutter中,實(shí)現(xiàn)動(dòng)畫的方式是使用animated library中的animated widget來(lái)包裝控件。
在Flutter中,使用AnimationController它是一個(gè)Animation<double>,可以實(shí)現(xiàn)暫停、搜索、停止、反轉(zhuǎn)的動(dòng)畫。它需要一個(gè)Ticker作為vsync的信號(hào)發(fā)生器,并且在每一幀發(fā)出一個(gè)0-1之間的線性插值。你可以創(chuàng)建多個(gè)Animation使用同一個(gè)AnimationController
你可以使用CurvedAnimation去實(shí)現(xiàn)插值曲線的動(dòng)畫,從這個(gè)意義上來(lái)說(shuō),控制器是實(shí)現(xiàn)動(dòng)畫變換的主要部分,而使用CurvedAnimation可以實(shí)現(xiàn)曲線插值替換默認(rèn)的線性運(yùn)動(dòng),就像Widget一樣,F(xiàn)lutter的動(dòng)畫也可以互相組合。
當(dāng)創(chuàng)建控件樹(shù)的時(shí)候可以將Animation賦給控件的動(dòng)畫屬性,例如實(shí)現(xiàn)透明的FadeTransition,同時(shí)告訴控制器開(kāi)始動(dòng)畫。
下面這個(gè)例子使用FadeTransition是實(shí)現(xiàn)了漸進(jìn)顯示Logo的一個(gè)效果:

import 'package:flutter/material.dart';

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

class FadeAppTest 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();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: Container(
              child: FadeTransition(
                  opacity: curve,
                  child: FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}

更多信息, 參考 Animation & Motion widgets, Animations tutorial, 和 Animations overview.

怎么使用Canvas來(lái)繪制?

在Android中,您可以使用CanvasDrawable在屏幕上繪制圖像和形狀。Flutter也有類似的Canvas API,因?yàn)樗谙嗤牡讓愉秩疽?,Skia。因此,對(duì)于Android開(kāi)發(fā)人員來(lái)說(shuō),在Flutter中繪制到Canvas是一項(xiàng)非常熟悉的工作。
Flutter有兩個(gè)類來(lái)幫助你繪制Canvas:CustomPaintCustomPainter,后者可以實(shí)現(xiàn)你的算法去繪制Canvas.
想了解如何使用Flutter實(shí)現(xiàn)一個(gè)簽名繪畫區(qū),可以參考Collin的回答: StackOverflow.
這里需要注意的是Collin原回答不能實(shí)現(xiàn)繪畫,需要使用下面的代碼需要替換掉源碼中這一行,修改CustomPaint的構(gòu)造參數(shù)

child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),

import 'package:flutter/material.dart';

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset> points;

  void paint(Canvas canvas, Size size) {
    Paint paint = new 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;
}

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

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];

  Widget build(BuildContext context) {
    return new Stack(
      children: [
        GestureDetector(
          onPanUpdate: (DragUpdateDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);

            setState(() {
              _points = new List.from(_points)..add(localPosition);
            });
          },
          onPanEnd: (DragEndDetails details) => _points.add(null),
        ),
        CustomPaint(painter: new SignaturePainter(_points)),
      ],
    );
  }
}

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

void main() => runApp(new MaterialApp(home: new DemoApp()));

怎么創(chuàng)建自定義控件

在Android中,通過(guò)繼承于View或者繼承于一個(gè)已經(jīng)存在的View,通過(guò)覆蓋或者實(shí)現(xiàn)接口來(lái)完成期望的效果。
在Flutter中,通過(guò)組合更小控件(而不是繼承)來(lái)構(gòu)建一個(gè)自定義的控件。這有點(diǎn)類似于在Android中實(shí)現(xiàn)自定義ViewGroup,其中所有構(gòu)建塊都已經(jīng)存在,但是你提供了不同的行為—例如,自定義布局邏輯。
例如,如何構(gòu)建一個(gè)帶有標(biāo)簽的CustomButton呢,通過(guò)組合RaisedButton包含一個(gè)lablel更好,而不是通過(guò)繼承RaisedButton。

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}

然后可以使用CustomButton就像你使用其他的Flutter的控件一樣:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Intents

Flutter中與Intent相對(duì)應(yīng)的是什么?

Android中,Intent有兩個(gè)主要用途:用于activity間的跳轉(zhuǎn)、用于組件間的通信。而在Flutter中,沒(méi)有Intent這個(gè)概念,雖然你依然可以通過(guò)本地集成(native integrations(使用插件))來(lái)啟動(dòng)Intent
Route可以看做是應(yīng)用屏幕或者頁(yè)面的抽象,而Navigator是一個(gè)管理Route的控件??梢源致缘膶?code>Route看成Activity,但是它們含義不同。Navigator通過(guò)pushpop操作Route在頁(yè)面之間切換,Navigator就像是一個(gè)棧,push表示切換到新的頁(yè)面,pop表示返回。
Android中,需要在AndroidManifest.xml中聲明activity,而在Flutter中,你有以下頁(yè)面切換選擇:

  • 指定一個(gè)包含所有Route名字的Map(MaterialApp)
  • 直接切換到Route(WidgetApp)
    下面的實(shí)例里建立了一個(gè)Map
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'),
    },
  ));
}

通過(guò)壓入對(duì)應(yīng)的名字到Navigator來(lái)切換到對(duì)應(yīng)的Route:

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

另一種Intent常見(jiàn)的使用場(chǎng)景是調(diào)用外部組件比如相機(jī)、文件選擇器,對(duì)于這種情況你需要?jiǎng)?chuàng)建一個(gè)原生平臺(tái)集成(或者使用現(xiàn)成插件 existing plugin
關(guān)于構(gòu)建原生平臺(tái)集成,請(qǐng)查看 Developing Packages and Plugins.

Flutter 如何處理來(lái)自外部的Intent?

Flutter可以通過(guò)直接訪問(wèn)Android layer來(lái)處理來(lái)自Android的Intent,或者請(qǐng)求共享數(shù)據(jù)。
下面的實(shí)例注冊(cè)了一個(gè)文本共享的Intent過(guò)濾器在運(yùn)行我們Flutter代碼的原生activity上,所以其他應(yīng)用就能共享文本給我們的Flutter 應(yīng)用。
基本流程就是先在Android 原生層(即Activity)先處理這些共享數(shù)據(jù),然后等待Flutter請(qǐng)求,通過(guò)MethodChannel可以將這些數(shù)據(jù)提供給Flutter.
首先,在AndroidManifest.xml中注冊(cè)Intent過(guò)濾器:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

接著在MainActivity中處理Intent,從Intent中獲取共享的數(shù)據(jù),然后先暫時(shí)持有這些數(shù)據(jù),當(dāng)Flutter準(zhǔn)備好處理時(shí),它會(huì)通過(guò)平臺(tái)通道(platform channel)進(jìn)行請(qǐng)求,然后從原生Android層發(fā)送這些數(shù)據(jù)。

package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import java.nio.ByteBuffer;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {

  private String sharedText;

  @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
      }
    }

    new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
      new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          if (call.method.contentEquals("getSharedText")) {
            result.success(sharedText);
            sharedText = null;
          }
        }
      });
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

最后,當(dāng) Flutter的控件渲染完成時(shí)請(qǐng)求數(shù)據(jù):

import 'package:flutter/material.dart';
import 'package:flutter/services.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 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> {
  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)));
  }

  getSharedText() async {
    var sharedData = await platform.invokeMethod("getSharedText");
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

startActivityForResult()等價(jià)的是什么?

Flutter中Navigator除了用于處理Route,也可以獲得已入棧Route的返回結(jié)果。只需要等push()返回的Future執(zhí)行完即可。

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

然后,在定位Route里,當(dāng)用戶選擇完位置后,你就可以通過(guò)pop獲取結(jié)果了:

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

異步UI

runOnUiThread 在Flutter中等價(jià)于什么

Dart是單線程執(zhí)行模型,支持Isolates(在另一個(gè)線程上運(yùn)行Dart代碼的方式)、事件循環(huán)和異步編程。 除非您啟動(dòng)一個(gè)Isolate,否則您的Dart代碼將在主UI線程中運(yùn)行,并由事件循環(huán)驅(qū)動(dòng)(和JavaScript一樣)。Flutter的事件驅(qū)動(dòng)循環(huán)等價(jià)于Android的主線程/UI線程的Looper。
Dart雖然是單線程模型但是并不意味著你需要用一種阻塞式的方式來(lái)執(zhí)行其他所有的代碼造成UI線程被凍結(jié)。不像Android,你需要保持主線程一直空閑不被阻塞,在Flutter中,你可以使用Dart語(yǔ)言提供的異步工具,如async/await來(lái)執(zhí)行異步操作。如果你用過(guò)C#,Javascript,或者你使用過(guò)Kotlin的協(xié)程的話,你可能會(huì)熟悉async/await這個(gè)范例。
例如,你可以使用async/await來(lái)執(zhí)行網(wǎng)絡(luò)請(qǐng)求代碼同時(shí)不會(huì)引起UI線程掛起,讓Dart完成繁重的操作:

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

一旦等到網(wǎng)絡(luò)請(qǐng)求結(jié)束,就會(huì)調(diào)用setState()方法以更新UI,接著觸發(fā)控件子樹(shù)的重建并更新數(shù)據(jù)。
下面的示例描述了如何異步加載數(shù)據(jù),然后填充到ListView:

import 'dart:convert';

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

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

class SampleApp 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
  _SampleAppPageState createState() => _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: 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 {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

上述代碼需要引入http.dart(下文中也有提到,這里是為了方便順序查看本文時(shí)驗(yàn)證demo所以先提到),可以訪問(wèn)http 0.12.0+2(當(dāng)前版本)查看對(duì)應(yīng)版本號(hào)在pubspec.yaml中引入:

dependencies:
  http: ^0.12.0+2

如何將任務(wù)放入后臺(tái)線程?

在Android中,訪問(wèn)網(wǎng)絡(luò)資源的一種典型方式就是在后臺(tái)線程中去執(zhí)行任務(wù),從而避免在主線程中執(zhí)行造成ANR。例如,你可以使用AsyncTask, LiveData, IntentService, JobScheduler或者RxJava Scheduler進(jìn)行后臺(tái)處理。
由于Flutter是一個(gè)單線程模型運(yùn)行著一個(gè)事件循環(huán)(像Node.js一樣),你不需要替線程管理和創(chuàng)建后臺(tái)線程而操心。如果你進(jìn)行I/O操作,例如硬盤訪問(wèn)或者網(wǎng)絡(luò)請(qǐng)求,你可以簡(jiǎn)單的使用async/await來(lái)完成所有的操作,反過(guò)來(lái)說(shuō),如果你需要執(zhí)行計(jì)算密集型的任務(wù),捏可以把它放入一個(gè)Isolate中從而避免阻塞事件循環(huán),就像Android中,你不會(huì)在主線程執(zhí)行任何除UI更新相關(guān)的其他任何類型的任務(wù)。
I/O類的任務(wù),只需要聲明函數(shù)是一個(gè)async函數(shù),然后在函數(shù)中,用await修飾在耗時(shí)任務(wù)之前:

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

這就是網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)操作等的典型做法,它們都是I/O操作。
在Android中,當(dāng)你繼承AsyncTask,那么通常你需要重寫三個(gè)方法,onPreExecute()、doInBackground()onPostExecute(),而在Flutter中則沒(méi)有與之等價(jià)的方式,因?yàn)?code>await修飾的耗時(shí)任務(wù)函數(shù),剩余的工作都交給Dart的事件循環(huán)去處理。
然而當(dāng)你處理大量數(shù)據(jù)時(shí),你的UI會(huì)掛起,所以在Flutter中使用Isolate來(lái)充分利用多核CPU去執(zhí)行耗時(shí)的計(jì)算密集型任務(wù)。
Isolate是獨(dú)立的執(zhí)行線程,它不會(huì)與主線程共享內(nèi)存堆,這就意味著你不能在Isolate中直接訪問(wèn)主線程的變量,或者調(diào)用setState更新UI。不像Android中的線程,Isolate是名副其實(shí)的隔離區(qū),不能共享內(nèi)存(比如不能以靜態(tài)字段的方式共享等)。
下面示例展示了一個(gè)簡(jiǎn)單的isolate怎么去共享數(shù)據(jù)返回給主線程去更新UI。

loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  SendPort sendPort = await receivePort.first;

  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));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

這里,dataLoader()Isolate執(zhí)行在它擁有的獨(dú)立執(zhí)行線程中。在Isolate中,可以執(zhí)行CPU密集型任務(wù)(比如解析超大的Json數(shù)據(jù)),或者執(zhí)行計(jì)算密集型的數(shù)學(xué)運(yùn)算,比如加密或者信號(hào)處理等。
你可以跑一下下面這個(gè)完整的示例:

import 'dart:convert';

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

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

class SampleApp 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
  _SampleAppPageState createState() => _SampleAppPageState();
}

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

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

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

    return false;
  }

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

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

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

  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 receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    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));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

在Flutter中與OkHttp等價(jià)的是什么?

在Flutter中使用流行的 httppackage調(diào)用一個(gè)網(wǎng)絡(luò)請(qǐng)求是很簡(jiǎn)單的。
雖然在http包中沒(méi)有OkHttp所有的功能,它抽象了大多你通常需要自己實(shí)現(xiàn)的網(wǎng)絡(luò)功能,使得網(wǎng)絡(luò)請(qǐng)求更加簡(jiǎn)單。
要使用http包,需要在pubspec.yaml中添加如下依賴:

dependencies:
  ...
  http: ^0.11.3+16

發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求可以使用await調(diào)用一個(gè)async函數(shù)例如http.get():

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

如何顯示耗時(shí)任務(wù)的進(jìn)度?

在Android中,當(dāng)在后臺(tái)線程中執(zhí)行一個(gè)耗時(shí)任務(wù)時(shí),通常顯示一個(gè)ProgressBar在UI上。
在Flutter中,則是使用ProgressIndicator控件。通過(guò)boolean標(biāo)記位來(lái)控制何時(shí)開(kāi)始渲染,然后在耗時(shí)任務(wù)開(kāi)始之前顯示它,并在任務(wù)結(jié)束時(shí)隱藏掉。
下面示例中,build函數(shù)分割成了三個(gè)不同的子函數(shù),如果showLoadingDialog()返回true(當(dāng)widgets.length == 0),則渲染ProgressIndicator,否則就將網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)渲染到ListView中并顯示它。

import 'dart:convert';

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

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

class SampleApp 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
  _SampleAppPageState createState() => _SampleAppPageState();
}

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

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

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

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

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

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

  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 {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

項(xiàng)目結(jié)構(gòu)&資源文件

在哪里存放不同分辨率的圖片文件?

Android中,resourcesassets是兩個(gè)獨(dú)立的文件夾,而在Flutter中,只有assets,所有放在Androidres/drawable-*文件夾中的文件全都放在Flutter中的assets文件夾中。

Flutterios一樣遵循簡(jiǎn)單的基于密度(density-base)的格式,assets包含1.0x、2.0x3.0x或者更高乘數(shù),Flutter中并沒(méi)有dp這一說(shuō),而是使用與設(shè)備無(wú)關(guān)的邏輯像素,在devicePixelRatio 中描述了單個(gè)邏輯像素與物理像素的比例。

對(duì)應(yīng)于Android密度清單如下:

Android density qualifier Flutter pixel ratio
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

Assets可以存在任意的文件夾,F(xiàn)lutter沒(méi)有預(yù)定義的文件夾結(jié)構(gòu)。需要在pubspec.yaml聲明assets對(duì)應(yīng)的物理位置,F(xiàn)lutter就能正確的讀取到。
注意,在Flutter 1.0 beta 2之前,F(xiàn)lutter定義的assets無(wú)法被原生端訪問(wèn),反之亦然,原生定義的assetsresources也不能被Flutter訪問(wèn),因?yàn)樗麄兾挥诓煌奈募A中。
而從Flutter 1.0 beta2開(kāi)始,assets存儲(chǔ)于本地層的assets文件夾中,且可以被本地層通過(guò)AssetManager訪問(wèn),但是beta2版本中,F(xiàn)lutter依然不能訪問(wèn)本地層的resourcesassets

val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

例如,要將名為my_icon.png的新圖像資源添加到我們的Flutter項(xiàng)目中,比方說(shuō),把它放到一個(gè)我們?nèi)我饷麨?code>images的文件夾中,你需要把1.0x圖片資源放在該文件的根目錄,然后把其他放在對(duì)應(yīng)乘數(shù)比例命名的子文件夾中:

images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image

接下來(lái)你需要去聲明這些圖片在pubspec.yaml文件中:

assets:
 - images/my_icon.jpeg

你可以用AssetImage去訪問(wèn)這些圖片:

return AssetImage("images/a_dot_burr.jpeg");

或者直接可以在Image控件中寫入路徑:

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

在哪里存放strings字符串資源,怎么實(shí)現(xiàn)本地化?

目前Flutter沒(méi)有像strings這樣的專用資源系統(tǒng),所以目前來(lái)說(shuō),最佳的方法就是把你的字符串以static字段的方式存放在一個(gè)類中,然后從這個(gè)類去訪問(wèn)它們。如下示例:

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

然后在代碼中你可以這樣調(diào)用你的字符串:

Text(Strings.welcomeMessage)

Flutter對(duì)Android上的輔助功能提供了基本支持,目前工作正在進(jìn)行中。官方鼓勵(lì)Flutter開(kāi)發(fā)者使用intl package來(lái)實(shí)現(xiàn)國(guó)際化和本地化。

與Gradle文件等價(jià)的是什么?該怎么添加依賴?

在Android中,使用Gradle構(gòu)建腳本添加依賴。在Flutter中,使用Dart自己的構(gòu)建系統(tǒng)和Pub包管理器。該工具會(huì)把原生的Android和IOS的包裝應(yīng)用的構(gòu)建過(guò)程委托給各自的構(gòu)建系統(tǒng)。
gradle文件在Flutter工程目錄的android文件夾下,只有需要針對(duì)單個(gè)平臺(tái)添加本地依賴時(shí)才使用gradle,通常來(lái)說(shuō)在Flutter中直接在pubspec.yaml聲明外部依賴即可。找Flutter包的好地方就是 Pub.

Activities和fragments

Flutter中與activitiesfragments相對(duì)應(yīng)的是啥?

在Android中,activity是用戶可以操作的一個(gè)焦點(diǎn)物,而Fragment則代表著一個(gè)行為表現(xiàn)或者是UI的一部分。Fragment可以模塊化你的代碼,可以給大屏設(shè)備組合出復(fù)雜的UI部分,幫助你拓展你的應(yīng)用UI.在Flutter中,這兩個(gè)概念都落在了Widget的范疇里。
要了解有關(guān)使用ActivityFragment構(gòu)建UI的更多信息,可以看社區(qū)里的文章,Flutter For Android Developers : How to design an Activity UI in Flutter.
正如在Intent部分所提到的,在Flutter中,Widget就代表著屏幕,因?yàn)樵贔lutter中萬(wàn)物皆Widget。我們使用Navigator來(lái)切換Route,這代表著不同屏幕或頁(yè)面,亦或是不同狀態(tài)或是渲染相同的數(shù)據(jù)。

如何監(jiān)聽(tīng)Android中activity的生命周期事件

在Android中, 通過(guò)重寫Activity中的方法來(lái)捕獲生命周期函數(shù)的回調(diào),或者實(shí)現(xiàn)ActivityLifecycleCallbacksApplication上。而在Flutter中,沒(méi)有這個(gè)概念,但你可以通過(guò)hookWidgetsBinding并且監(jiān)聽(tīng)didChangeAppLifecycleState()的變化事件來(lái)代替監(jiān)聽(tīng)生命周期。
可觀察的生命周期事件有:

  • inactive — 應(yīng)用處于非活動(dòng)狀態(tài),不接受用戶輸入。這個(gè)事件只在IOS中有效,因?yàn)樵贏ndroid中沒(méi)有與此等價(jià)的事件。
  • paused — 當(dāng)前應(yīng)用對(duì)用戶不可見(jiàn),不響應(yīng)用戶輸入,且運(yùn)行在后臺(tái)。等同于Android中的onPause。(如果等同于Android中onPause的話這里應(yīng)該是可見(jiàn)但是不在頂層可見(jiàn))
  • resumed — 此時(shí)應(yīng)用可見(jiàn)且響應(yīng)用戶輸入。等同于Android中的onPostResume()
  • suspending — 此時(shí)應(yīng)用暫時(shí)掛起。等同于Android中的onStop;不會(huì)觸發(fā)IOS上的事件,IOS中沒(méi)有等價(jià)的事件。
    想要了解更多關(guān)于這些狀態(tài)的信息,可以參見(jiàn) AppLifecycleStatus documentation.
    正如你注意到的,只有少數(shù)的Activity生命周期可用,而FlutterActivity確實(shí)在內(nèi)部捕獲了幾乎所有的Activity生命周期事件然后把它們發(fā)送到Flutter的引擎中去,然而很多事件都替你屏蔽了。Flutter會(huì)替你管理引擎的啟動(dòng)和關(guān)閉,所以大多數(shù)情況下我們幾乎不需要去觀察activity的生命周期在Flutter中。如果你一定要在生命周期里獲取或者釋放本地資源,你至少可以在原生端做這些事。
    以下示例描述了如何監(jiān)聽(tīng)Activity中的生命周期事件:
import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null)
      return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);

    return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
        textDirection: TextDirection.ltr);
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

布局

LinearLayout等價(jià)的是什么

在Android中,LinearLayout用于橫向和縱向布局控件,而在Flutter中則是使用RowColumn控件來(lái)實(shí)現(xiàn)相同的行為。
你可能注意到兩段示例代碼實(shí)現(xiàn)相同,僅僅RowColumn控件不同。子控件是完全相同的,這樣的特性可以被用來(lái)開(kāi)發(fā)具有相同子控件但是隨著時(shí)間推移會(huì)變化的富布局。

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );
}

如果想了解更多關(guān)于構(gòu)建線性布局的內(nèi)容,可以參見(jiàn)社區(qū)文章:Flutter For Android Developers : How to design LinearLayout in Flutter?.

RelativeLayout等價(jià)的是什么

使用相對(duì)布局可以將你的子控件按照彼此的位置相互放置。在Flutter中,有很多方法可以達(dá)到同樣的效果。
你可以組合Column,RowStack控件來(lái)實(shí)現(xiàn)與相對(duì)布局一樣的效果。你可以在widgets的構(gòu)造函數(shù)中指定子控件相對(duì)于父布局的規(guī)則。
有關(guān)在FLutter中構(gòu)建RelativeLayout的好例子,可以參考Collin's的回答: StackOverflow.

ScrollView等價(jià)的是什么

在Android中,當(dāng)你的內(nèi)容超出設(shè)備屏幕時(shí),使用ScrollView來(lái)布局。
在Flutter中,最簡(jiǎn)單的實(shí)現(xiàn)方式就是使用ListView控件。這與Android看起來(lái)仿佛有點(diǎn)矯枉過(guò)正,但是在Flutter中一個(gè)ListView即是Android中的ScrollView也是ListView。

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

在Flutter中如何處理橫屏反轉(zhuǎn)

只要在AndroidManifest.xml中包含下列內(nèi)容FlutterView就會(huì)處理這些配置變化:

android:configChanges="orientation|screenSize"

手勢(shì)檢測(cè)和觸摸事件處理

Flutter中怎么給一個(gè)空間添加點(diǎn)擊事件

在Android中你可以通過(guò)setOnClickListener來(lái)給一個(gè)View,例如Button添加點(diǎn)擊事件。
在Flutter中,有下面兩種方式添加觸摸事件監(jiān)聽(tīng):

  1. 如果控件支持事件檢測(cè),那可以給它傳入一個(gè)函數(shù)去處理這個(gè)事件。比如,RaisedButton包含一個(gè)onPressd參數(shù):
@override
Widget build(BuildContext context) {
  return RaisedButton(
      onPressed: () {
        print("click");
      },
      child: Text("Button"));
}
  1. 如果空間不支持事件檢測(cè),可以把它包裝在GestureDetector中通過(guò)onTap參數(shù)傳遞一個(gè)監(jiān)聽(tīng)函數(shù)。
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}

如何處理控件的其他手勢(shì)?

使用GestureDetector可以監(jiān)聽(tīng)廣泛的手勢(shì),例如:

  • 列表第一項(xiàng)
    • 列表第二項(xiàng)
      • 列表第二項(xiàng)
  • 單擊(Tab)
    • onTabDown: 觸發(fā)單擊事件的指針已經(jīng)與屏幕上特定點(diǎn)聯(lián)系在一起
    • onTapUp: 觸發(fā)單擊事件的指針停止與屏幕在特定點(diǎn)上的聯(lián)系
    • onTap: 單擊事件發(fā)生
    • onTapCancel: 觸發(fā)時(shí)會(huì)導(dǎo)致之前觸發(fā)onTabDown的指針無(wú)法形成單擊事件
  • 雙擊(Double Tab)
    • onDoubleTab: 用戶在屏幕的同一個(gè)點(diǎn)上連續(xù)快速點(diǎn)擊了兩次
  • 長(zhǎng)按Long Press
    • onLongPress: 指針和屏幕上同一個(gè)位置發(fā)生一段較長(zhǎng)事件的聯(lián)系
  • 垂直拖動(dòng)(Vertical drag)
    • onVerticalDragStart: 指針已經(jīng)和屏幕聯(lián)系,并且可能開(kāi)始垂直移動(dòng)。
    • onVerticalDragUpdate: 正在和屏幕聯(lián)系的指針已經(jīng)開(kāi)始在垂直方向上進(jìn)行移動(dòng)。
    • onVerticalDragEnd: 之前與屏幕進(jìn)行聯(lián)系且在垂直方向移動(dòng)的指針,現(xiàn)在已經(jīng)不需要再與屏幕聯(lián)系了,并且在停止聯(lián)系的瞬間,指針依然以一定的速度移動(dòng)。
  • 水平拖動(dòng)(Horizontal drag)
    • onHorizontalDragStart: 指針已經(jīng)和屏幕聯(lián)系,并且可能開(kāi)始水平移動(dòng)。
    • onHorizontalDragUpdate: 正在和屏幕聯(lián)系的指針已經(jīng)開(kāi)始在水平方向上進(jìn)行移動(dòng)。
    • onHorizontalDragEnd: 之前與屏幕進(jìn)行聯(lián)系且在水平方向移動(dòng)的指針,現(xiàn)在已經(jīng)不需要再與屏幕聯(lián)系了,并且在停止聯(lián)系的瞬間,指針依然以一定的速度移動(dòng)。

下面示例展示了使用GestureDetector監(jiān)聽(tīng)雙擊Flutter logo時(shí),logo旋轉(zhuǎn)的效果:

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: GestureDetector(
            child: RotationTransition(
                turns: curve,
                child: FlutterLogo(
                  size: 200.0,
                )),
            onDoubleTap: () {
              if (controller.isCompleted) {
                controller.reverse();
              } else {
                controller.forward();
              }
            },
        ),
    ));
  }
}

ListView 與 Adapter

在Flutter中替代ListView的是什么

在Flutter中與之對(duì)應(yīng)的就是ListView。
在Android中,ListView使用一個(gè)適配器模式,每條數(shù)據(jù)都由你的適配器來(lái)渲染返回。然而你必須確保每條數(shù)據(jù)都被你回收,否則你可能會(huì)遇到各種顯示錯(cuò)亂和內(nèi)存問(wèn)題。
而在Flutter中控件不可變,你提供一個(gè)控件的列表給你的ListView,然后Flutter去處理確?;瑒?dòng)快速和流暢。

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> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

如何知道哪一條列表項(xiàng)被點(diǎn)擊

在Android中,onItemClickListener會(huì)找出哪一條列表項(xiàng)被點(diǎn)擊。在Flutter里,觸摸事件的處理由傳入的控件提供。

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> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("Row $i")),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

如何動(dòng)態(tài)更新ListView

在Android中是通過(guò)適配器調(diào)用notifyDataSetChanged來(lái)更新。
而在Flutter中,如果你是在setState()方法中更新控件列表,那么你很快會(huì)發(fā)現(xiàn)你的數(shù)據(jù)沒(méi)有更新視圖,這是因?yàn)楫?dāng)setState()被調(diào)用時(shí),F(xiàn)lutter渲染引擎會(huì)在控件樹(shù)中搜索是否存在發(fā)生改變的東西,而當(dāng)它找到了你的ListView,它會(huì)進(jìn)行==判斷,然后判定這兩個(gè)ListView是相同的,因而不會(huì)發(fā)生任何改變,也就不會(huì)請(qǐng)求更新。
一個(gè)簡(jiǎn)單更新ListView的方法就是,在setState()方法中創(chuàng)建一個(gè)新的List,然后將舊集合的數(shù)據(jù)拷貝過(guò)來(lái)。這個(gè)途徑比較簡(jiǎn)單,但是不推薦在數(shù)據(jù)量大的時(shí)候使用,如下示例:

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> {
  List widgets = <Widget>[];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

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

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

一個(gè)推薦的高效且有效的方式是通過(guò)ListView.Builder來(lái)建立ListView,這種方式在你的數(shù)據(jù)量巨大或者需要?jiǎng)討B(tài)改變數(shù)據(jù)的情況下非常有用,這實(shí)質(zhì)上等價(jià)于Android中的RecyclerView,因?yàn)樗鼤?huì)自動(dòng)循環(huán)復(fù)用列表元素:

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> {
  List widgets = <Widget>[];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: ListView.builder(
            itemCount: widgets.length,
            itemBuilder: (BuildContext context, int position) {
              return getRow(position);
            }));
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

與創(chuàng)建ListView不同的是,創(chuàng)建ListView.Builder需要傳入兩個(gè)命名參數(shù):初始列表的長(zhǎng)度和ItemBuilder函數(shù)。
ItemBuilder與Android適配器中的getView()方法類似,它給你一個(gè)位置,然后返回你想要在對(duì)應(yīng)位置上渲染的行。
最后,且最重要的一點(diǎn)是, onTab函數(shù)已經(jīng)不需要在重新創(chuàng)建數(shù)據(jù)列表了,取而代之的是通過(guò).add()來(lái)添加到控件列表中。

文字處理

如何給文字控件設(shè)置自定義字體

在Android SDK(Android O),可以創(chuàng)建一個(gè)Font資源,然后作為FontFamily參數(shù)傳入TextView。而在Flutter中,則是將字體文件放到一個(gè)文件夾中,然后在pubspec.yaml中引用即可,跟引入圖片是類似的。

fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

然后給你的Text指定字體:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

如何給Text控件自定義風(fēng)格

除了字體,我們還可以定義Text控件的其他風(fēng)格屬性,Text控件的風(fēng)格參數(shù)中包含一個(gè)TextStyle對(duì)象,我們可以定義它的很多參數(shù),例如:

  • color
  • decoration
  • decorationColor
  • decorationStyle
  • fontFamily
  • fontSize
  • fontStyle
  • fontWeight
  • hashCode
  • height
  • inherit
  • letterSpacing
  • textBaseline
  • wordSpacing

表單輸入

有關(guān)表單的更多信息請(qǐng)查閱Flutter cookbook 中的Retrieve the value of a text field.

與輸入框中"hint"等價(jià)的是什么

在Flutter中,可以通過(guò)給Text控件傳入一個(gè)InputDecoration對(duì)象來(lái)顯示“hint”或者占位字符,如下示例:

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  )
)

如何展示驗(yàn)證錯(cuò)誤?

和顯示hint一樣,傳一個(gè)InputDecoration對(duì)象給Text控件的構(gòu)造函數(shù)即可。

然而,你肯定不想一開(kāi)始就顯示錯(cuò)誤,而是當(dāng)用戶鍵入一個(gè)非法值才顯示,并傳入一個(gè)新的InputDecoration對(duì)象。

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> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

Flutter Plugins

如何訪問(wèn)GPS傳感器

使用 geolocator社區(qū)插件。

如何訪問(wèn)相機(jī)

流行的方式是使用 image_picker插件。

如何登錄Facebook

flutter_facebook_login插件可以用來(lái)登錄到FaceBook.

如何使用Firebase

大部分Firebase函數(shù)轉(zhuǎn)換自 first party plugins,以下是第一批集成的插件,由Flutter團(tuán)隊(duì)維護(hù):

如何構(gòu)建自定義的 native integrations(本地集成)

如果沒(méi)有Flutter或者社區(qū)插件沒(méi)有提供那些平臺(tái)特定的功能,你可以構(gòu)建自己的,參考developing packages and plugins頁(yè)面。
Flutter的插件架構(gòu),簡(jiǎn)而言之,類似于在Android中使用EventBus:發(fā)出消息給接收器處理,接收器處理完成后將結(jié)果發(fā)射回給你。在這里,接收器代碼運(yùn)行在本地層,也就是AndroidIOS.

如何在Flutter應(yīng)用中使用NDK

如果你已經(jīng)在Android應(yīng)用中使用了NDK,并且想要讓Flutter也能利用到這些類庫(kù),那么就需要構(gòu)建自定義插件了。
你的自定義插件先和Android應(yīng)用交互,即在Android應(yīng)用中通過(guò)JNI調(diào)用native函數(shù),當(dāng)響應(yīng)準(zhǔn)備好時(shí),就發(fā)送給Flutter,然后渲染結(jié)果。
目前不支持直接從Flutter中調(diào)用native代碼。

主題

如何給應(yīng)用定制主題

直接使用的情況下(Out of the box),F(xiàn)lutter自帶漂亮的Material Design實(shí)現(xiàn),它可以滿足你通常所需的大量樣式和主題需求。不像Android在XML文件中聲明主題,然后在AndroidManifest.xml中使用。在Flutter中是在頂層控件中聲明主題的。
為了能充分利用Material組件,你可以聲明一個(gè)頂層控件MaterialApp作為應(yīng)用的入口。MaterialApp是一個(gè)很便攜的控件,他包裝了實(shí)現(xiàn)了Material Design且應(yīng)用需要的控件。他建立在WidgetsApp之上,添加了Material特定的功能。
同樣也可以使用WidgetsApp作為應(yīng)用控件,它提供了一些相同的功能,但不如MaterialApp豐富。
想要在任意子組件上自定義顏色和風(fēng)格的話,那么給MaterialApp傳入一個(gè)ThemeData對(duì)象,例如,在下面代碼中,初始樣本設(shè)置為藍(lán)色,而文字選擇后顯示為紅色。

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

數(shù)據(jù)庫(kù)與本地存儲(chǔ)

怎樣訪問(wèn)Share Preferences

在Android中,你可以使用SharePreferences來(lái)存儲(chǔ)一些小的鍵值對(duì)。
在Flutter中,使用Shared_Preferences plugin來(lái)訪問(wèn)這一功能。這個(gè)插件包裝了Shared PreferencesNSUserDefaults(IOS中等價(jià)的功能)兩個(gè)功能。

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: RaisedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  prefs.setInt('counter', counter);
}

如何訪問(wèn)SQLite數(shù)據(jù)庫(kù)

在Flutter中使用SQFlite插件實(shí)現(xiàn)此功能。

通知

如何推送通知

(國(guó)內(nèi)環(huán)境中firebase基本也沒(méi)有用)
Flutter中,通過(guò)Firebase_Messaging插件可以使用該功能,更多關(guān)于使用Firebase Cloud Messaging API的信息,請(qǐng)查看firebase_messaging插件文檔。

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

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

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