主頁 > 移動端開發 > 【Flutter】應用開發筆記

【Flutter】應用開發筆記

2021-09-05 08:28:57 移動端開發

1 獲取Flutter SDK

1.下載安裝包
2.將壓縮包解壓,然后把其中的 flutter 目錄整個放在你想放置 Flutter SDK 的路徑中

勿將 Flutter 安裝在需要高權限的檔案夾內,例如 C:\Program Files\,

2 配置環境變數

2.1 更新path環境變數在這里插入圖片描述

Environment Variables->User Variables->PATH->New加入 flutter\bin 目錄的完整路徑
在這里插入圖片描述配置國內鏡像,新增加環境變數
在這里插入圖片描述

2.2 配置Android Studio

File > Settings > Plugins下載Flutter和Dart插件
在這里插入圖片描述
配置國內依賴
android/build.gradle替換如下內容

repositories {
        //google()
        //jcenter()
        maven { url 'https://maven.aliyun.com/repository/google' }
        maven { url 'https://maven.aliyun.com/repository/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
    }

插件版本和gradle版本匹配
查看gradle版本
android/build.gradle

dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }

android/gradle\wrapper\gradle-wrapper.properties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

3 熱多載(hot reload)

在這里插入圖片描述保持app運行狀態,點擊選單欄的閃電進行熱多載,

4 創建應用

在pubspec.yaml中保持如下設定,使用更多 Material 的特性

flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

5 視圖(Views)

5.1 視圖在Flutter中的對應概念

View是Android中顯示在螢屏上的一切基礎;
Widget大致是Flutter中的對應的View,

差異①:
widget 有著不一樣的生命周期:它們是不可變的,一旦需要變化則生命周期終止,任何時候 widget 或它們的狀態變化時, Flutter 框架都會創建一個新的 widget 樹的實體,
Android View 只會繪制一次,除非呼叫 invalidate 才會重繪,

Flutter 的 widget 很輕量,部分原因在于它們的不可變性,因為它們本身既非視圖,也不會直接繪制任何內容,而是 UI 及其底層創建真正視圖物件的語意的描述,

5.2 更新widgets

差異②:
Android中可以直接更新View;
Flutter中Widget是不可變的,不能直接更新,需要操作Widget的狀態,

StatelessWidget用于用戶界面的一部分不依賴于除了物件中的配置資訊以外的任何東西的場景,如Android中的ImageView,這個圖示運行中不會改變,在Flutter中即StatelessWidget,

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

StatefulWidget用于用戶界面的一部分需要和用戶動態互動的部分場景,如根據 HTTP 請求回傳的資料或者用戶的互動來動態地更新界面,并告訴 Flutter 框架 Widget 的狀態 (State) 更新了,以便 Flutter 可以更新這個 Widget,

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

這里需要著重注意的是,無狀態和有狀態的 Widget 本質上是行為一致的,它們每一幀都會重建,不同之處在于 StatefulWidget 有一個跨幀存盤和恢復狀態資料的 State 物件,
如果一個 Widget 會變化(例如由于用戶互動),它是有狀態的,然而,如果一個 Widget 回應變化,它的父 Widget 只要本身不回應變化,就依然是無狀態的,

5.3 布局Widget

差異③:
Android中使用XML檔案定義布局;
Flutter中使用Widget樹定義布局,

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

5.4 在布局中添加或洗掉組件

差異④:
Android中通過呼叫父 View 的 addChild() 或 removeChild() 方法動態地添加或者洗掉子 View;
Flutter中由于 Widget 是不可變的,所以沒有 addChild() 的直接對應的方法,可以給回傳一個 Widget 的父 Widget 傳入一個方法,并通過布爾標記值toggle控制子 Widget 的創建,

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle.
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

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

5.5 Widget實作影片

差異⑤:
Android中既可以通過 XML 檔案定義影片,也可以呼叫 View 物件的 animate() 方法;
Flutter中使用影片庫,通過將 Widget 嵌入一個影片 Widget 的方式實作 Widget 的影片效果,

Flutter 通過 Animation 的子類 AnimationController 來暫停、播放、停止以及逆向播放影片,它需要一個 Ticker 在垂直同步 (vsync) 的時候發出信號,并且在運行的時候創建一個介于 0 和 1 之間的線性插值,然后就可以創建一個或多個 Animation,并將它們系結到控制器上,
下面的例子展示了如何實作一個點擊 FloatingActionButton 的時候將一個 Widget 漸變為一個圖示的 FadeTransition:

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: FadeTransition(
          opacity: curve,
          child: FlutterLogo(
            size: 100.0,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        onPressed: () {
          controller.forward();
        },
        child: Icon(Icons.brush),
      ),
    );
  }
}

5.6 使用Canvas繪制影片

差異⑥:
Android中使用 Canvas 和 Drawable 將圖片和形狀繪制到螢屏上;
Flutter中使用類似于 Canvas 的 API,因為它基于相同的底層渲染引擎 Skia,

Flutter 有兩個用畫布 (canvas) 進行繪制的類:CustomPaint 和 CustomPainter,后者可以實作自定義的繪制演算法,

import 'package:flutter/material.dart';

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

class DemoApp 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;
}

5.7 創建自定義Widget

差異⑦:
Android中通過繼承 View 類,或者使用已有的視圖類,再覆寫或實作可以達到特定效果的方法;
Flutter中通過 組合 更小的 Widget 來創建自定義 Widget(而不是繼承它們),

創建一個在構造器接收標簽引數的 CustomButton?你要組合 RaisedButton 和一個標簽來創建自定義按鈕,而不是繼承 RaisedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

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

使用CustomButton:

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

5 Intents

5.1 Intent在Flutter中對應的概念

差異⑧:
Android中Intent 主要有兩個使用場景:在 Activity 之前進行導航,以及組件間通信, Flutter 卻沒有 intent 這樣的概念,但是你依然可以通過原生集成 (插件) 來啟動 intent;
Flutter沒有 Activity 和 Fragment 的對應概念,在 Flutter 中需要使用 Navigator 和 Route 在同一個 Activity 內的不同界面間進行跳轉,

Route 是應用內螢屏和頁面的抽象,Navigator 是管理路徑 route 的工具,
一個 route 物件大致對應于一個 Activity,但是它的含義是不一樣的,
Navigator 可以通過對 route 進行壓堆疊和彈堆疊操作實作頁面的跳轉,Navigator 的作業原理和堆疊相似,你可以將想要跳轉到的 route 壓堆疊 (push()),想要回傳的時候將 route 彈堆疊 (pop()),

差異⑨:
Android 中,在應用的 AndroidManifest.xml 檔案中宣告 Activity,
Flutter 中,有多種不同的方式在頁面間導航:1)定義一個 route 名字的 Map,(MaterialApp) 2)直接導航到一個 route,(WidgetApp)

創建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'),
   },
 ));
}

通過將 route 名壓堆疊 (push) 到 Navigator 中來跳轉到這個 route:

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

5.2 Flutter處理從外部應用獲取接收的Intent

Flutter 可以通過直接和 Android 層通信并請求分享的資料來處理接收到的 Android intent,
示例:
首先在 Android 原生層面(在我們的 Activity 中)處理分享的文本資料,然后 Flutter 再通過使用 MethodChannel 獲取這個資料,
在 AndroidManifest.xml 中注冊 intent 過濾器:

<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 分享的文本并保存,當 Flutter 準備好處理的時候,它會使用一個平臺通道請求資料,資料便會從原生端發送過來:

public class MainActivity extends FlutterActivity {

  private String sharedText;
  private static final String CHANNEL = "app.channel.shared.data";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    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
      }
    }
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine);

      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
              .setMethodCallHandler(
                      (call, result) -> {
                          if (call.method.contentEquals("getSharedText")) {
                              result.success(sharedText);
                              sharedText = null;
                          }
                      }
              );
  }

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

當 Widget 渲染的時候,從 Flutter 這端請求資料:

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

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

5.3 startActivityForResult()的對應方法

Navigator 類負責 Flutter 的導航,并用來接收被壓堆疊的 route 的回傳值,這是通過在 push() 后回傳的 Future 上 await 來實作的,
打開一個讓用戶選擇位置的 route:

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

在你的位置 route 內,一旦用戶選擇了位置,你就可以彈堆疊 (pop) 并回傳結果:

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

6 異步UI

6.1 runOnUiThread()的對應方法

Flutter 的事件回圈對應于 Android 里的主 Looper— 也即系結到主執行緒上的 Looper,
除非創建一個 Isolate,否則你的 Dart 代碼會運行在主 UI 執行緒,并被一個事件回圈所驅動,

差異⑩:
Android中Android 中需要你時刻保持主執行緒空閑;
Flutter中使用 Dart 語言提供的異步工具,例如 async/await 來執行異步任務,

使用 async/await 來運行網路代碼而且不會導致 UI 掛起,同時讓 Dart 來處理背后的繁重細節:

Future<void> loadData() async {
  String dataURL = 'https://jsonplaceholder.typicode.com/posts';
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

用 await 修飾的網路操作完成,再呼叫 setState() 更新 UI,這會觸發 widget 子樹的重建并更新資料,
異步加載資料并展示在 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"]}"),
    );
  }

  Future<void> loadData() async {
    String dataURL = 'https://jsonplaceholder.typicode.com/posts';
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

6.2 任務轉移到后臺

差異11:
Android中訪問一個網路資源卻又不想阻塞主執行緒并避免 ANR 的時候,一般會將任務放到一個后臺執行緒中運行,使用一個 AsyncTask、一個 LiveData、一個 IntentService、一個 JobScheduler 任務或者通過 RxJava 的管道用調度器將任務切換到后臺執行緒中,
Flutter中單執行緒并且運行一個事件回圈,無須擔心執行緒的管理以及后臺執行緒的創建,在執行和 I/O 系結的任務時,例如存盤訪問或者網路請求,可以安全地使用 async/await,再例如,執行消耗 CPU 的計算密集型作業,將其轉移到一個 Isolate 上以避免阻塞事件回圈,就像 Android 中會將任何任務放到主執行緒之外一樣,

對于和 I/O 系結的任務,將方法宣告為 async 方法,并在方法內 await 一個長時間運行的任務:

Future<void> loadData() async {
  String dataURL = 'https://jsonplaceholder.typicode.com/posts';
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

Android 中繼承 AsyncTask 的時候,一般會覆寫三個方法: onPreExecute()doInBackground()onPostExecute()
Flutter 中沒有對應的 API,只需要 await 一個耗時方法呼叫, Dart 的事件回圈就會幫你處理剩下的事情,

在 Flutter 中,可以通過使用 Isolate 來利用多核處理器的優勢執行耗時或計算密集的任務,Isolate 是獨立執行的執行緒,不會和主執行記憶體堆分享記憶體,這意味著你無法訪問主執行緒的變數,或者呼叫 setState() 更新 UI,不同于 Android 中的執行緒,Isolate 如其名所示,它們無法分享記憶體(例如通過靜態變數的形式)

Isolate 將資料分享給主執行緒來更新 UI 的示例:

Future<void> 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 Future<void> 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(jsonDecode(response.body));
  }
}

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

6.3 網路請求

差異12:
Android中使用OKHttp;
Flutter中使用http包,雖然 http 包沒有 OkHttp 中的所有功能,但是它抽象了很多通常會自己實作的網路功能,這使其本身在執行網路請求時簡單易用,

先在 pubspec.yaml 檔案中添加依賴:

dependencies:
  ...
  http: ^0.11.3+16

發起一個網路請求,在異步 (async) 方法 http.get() 上呼叫 await 即可:

import 'dart:convert';

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

Future<void> loadData() async {
  String dataURL = 'https://jsonplaceholder.typicode.com/posts';
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = jsonDecode(response.body);
  });
}

6.4 耗時任務顯示進度

差異13:
Android中在后臺執行耗時任務時顯示一個ProgressBar在頁面上;
Flutter中使用 ProgressIndicator widget,通過代碼邏輯使用一個布爾標記值控制進度條的渲染,

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

  Widget getBody() {
    bool showLoadingDialog = widgets.isEmpty;
    if (showLoadingDialog) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

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

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

  ListView getListView() {
    return 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"]}"),
    );
  }

  Future<void> loadData() async {
    String dataURL = 'https://jsonplaceholder.typicode.com/posts';
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = jsonDecode(response.body);
    });
  }
}

7 工程與資源檔案

7.1 放置解析度相關圖片檔案

差異14:
Android中區分對待資源檔案 (resources) 和資產檔案 (assets);
Flutter中只有資產檔案 (assets),

Android 密度修飾符Flutter 像素比例
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

Flutter 遵循一個簡單的類似 iOS 的密度相關的格式,檔案可以是一倍 (1.0x)、兩倍 (2.0x)、三倍 (3.0x) 或其它的任意倍數, Flutter 沒有 dp 單位,但是有邏輯像素尺寸,基本和設備無關的像素尺寸是一樣的,名稱為 [devicePixelRatio][] 的尺寸表示在單一邏輯像素標準下設備物理像素的比例,

原生端訪問Flutter assets檔案:

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

Flutter不能訪問原生端的資源檔案,Flutter中添加圖片需要將基礎圖片(1.0x)放在 images 檔案夾中,并將其它倍數的圖片放入以特定倍數作為名稱的子檔案夾中:

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

在 pubspec.yaml 檔案中定義這些圖片:

assets:
 - images/my_icon.jpeg

使用 AssetImage 訪問你的圖片了:

AssetImage('images/my_icon.jpeg');

或者通過 Image widget 直接訪問:

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

7.2 字串本地化

Flutter 當下并沒有一個特定的管理字串的資源管理系統,最好的辦法是將字串作為靜態域存放在類中,并通過類訪問它們,例如:

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

訪問字串方法:

Text(Strings.welcomeMessage)

8 Activity和Fragment

8.1 Flutter中的Activity和Fragment

差異15:
Android中一個 Activity 代表用戶可以完成的一件獨立任務,一個 Fragment 代表一個行為或者用戶界面的一部分;
Flutter中這兩個概念都對應于 Widget,

8.2 監聽Activity的生命周期

差異16:
Android中覆寫 Actvity 的生命周期方法來監聽其生命周期,或在 Application 上注冊 ActivityLifecycleCallbacks;
Flutter中通過系結 WidgetsBinding 觀察者并監聽 didChangeAppLifecycleState() 的變化事件來監聽生命周期,

生命周期事件:

事件解釋
inactive應用處于非活躍狀態并且不接收用戶輸入
detached應用依然保留 flutter engine,但是它會脫離全部宿主 view
paused應用當前對用戶不可見,無法回應用戶輸入,并運行在后臺,這個事件對應于 Android 中的 onPause()
resumed應用對用戶可見并且可以回應用戶的輸入,這個事件對應于 Android 中的 onPostResume()
suspending應用暫時被掛起,這個事件對應于 Android 中的 onStop(); iOS 上由于沒有對應的事件,因此不會觸發此事件
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()));
}

9 布局

9.1 Flutter中的LinearLayout

差異17:
Android中LinearLayout 用于線性布局 widget 的水平或者垂直;
Flutter中使用 Row 或者 Column Widget 來實作相同的效果,

  @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'),
    ],
  );
}

9.2 Flutter中的RelativeLayout

RelativeLayout 通過 Widget 的相互位置對它們進行布局,

通過組合使用 Column、Row 和 Stack Widget 實作 RelativeLayout 的效果,或在 Widget構造器內宣告孩子相對父親的布局規則,

9.3 Flutter中ScrollView

差異18:
Android中使用 ScrollView 布局 widget—如果用戶的設備螢屏比應用的內容區域小,用戶可以滑動內容;
Flutter中使用 ListView widget,ListView widget 既是一個 ScrollView,也是一個 Android 中的ListView,

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

9.4 Flutter中螢屏旋轉

AndroidManifest.xml中宣告:

android:configChanges="orientation|screenSize"

10 手勢監聽和觸摸事件處理

10.1 Widget添加監聽器

差異19:
Android中通過呼叫 setOnClickListener 方法在按鈕這樣的 View 上添加點擊監聽器;
Flutter中有兩種添加觸摸監聽器的方法,

方法一:
如果 Widget 支持事件監聽,那么向它傳入一個方法并在方法中處理事件,例如,RaisedButton 有一個 onPressed 引數:

@override
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () {
      print('click');
    },
    child: Text('Button'),
  );
}

方法二:
如果 Widget 不支持事件監聽,將 Widget 包裝進一個 GestureDetector 中并向 onTap 引數傳入一個方法:

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onTap: () {
            print('tap');
          },
          child: FlutterLogo(
            size: 200.0,
          ),
        ),
      ),
    );
  }
}

10.2 處理其他手勢

使用GestureDetector監聽的手勢:
Tap

onTapDown一個可能產生點擊事件的指標觸摸到螢屏的特定位置
onTapUp一個產生了點擊事件的指標停止觸摸螢屏的特定位置
onTap一個點擊事件已經發生
onTapCancel之前觸發了 onTapDown 事件的指標不會產生點擊事件

Double tap

onDoubleTap用戶在螢屏同一位置連續快速地點擊兩次

Long press

onLongPress指標在螢屏的同一位置保持了一段較長時間的觸摸狀態

Vertical drag

onVerticalDragStart指標已經觸摸螢屏并可能開始垂直移動
onVerticalDragUpdate觸摸螢屏的指標在垂直方向移動了更多的距離
onVerticalDragEnd之前和螢屏接觸并垂直移動的指標不再繼續和螢屏接觸,并且在和螢屏停止接觸的時候以一定的速度移動

Horizontal drag

onHorizontalDragStart指標已經觸摸螢屏并可能開始水平移動
onHorizontalDragUpdate觸摸螢屏的指標在水平方向移動了更多的距離
onHorizontalDragEnd之前和螢屏接觸并水平移動的指標不再繼續和螢屏接觸,并且在和螢屏停止接觸的時候以一定的速度移動

雙擊旋轉 Flutter 標志的 GestureDetector:

11 Listviews和adapters

11.1 Flutter中的ListView

差異20:
Android中創建一個 adapter 并將其傳給 ListView, ListView 渲染 adapter 回傳的每一行內容,要確保回收了每一行視圖,否則會遇到各種奇怪的界面和記憶體問題;
Flutter中要向 ListView 傳入一組 widget, Flutter 會保證滑動的快速順暢,

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView(children: _getListData()),
    );
  }

  List<Widget> _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;
  }
}

11.2 監聽點擊串列項

差異21:
Android中ListView 有一個可以定位哪個串列項被點擊了的方法;
Flutter中使用傳入 widget 的觸摸監聽,

class _SampleAppPageState extends State<SampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Sample App'),
      ),
      body: ListView(children: _getListData()),
    );
  }

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

11.3 動態更新ListView

差異22:
Android中要更新 adapter 并呼叫 notifyDataSetChanged
Flutter中在 setState() 里創建一個新的 List,并將資料從舊串列拷貝到新串列,如果在 setState() 里更新一組 widget,資料并沒有更新到界面上,這是因為當 setState() 被呼叫的時候, Flutter 渲染引擎會查看 Widget 樹是否有任何更改,當引擎檢查到 ListView,他會執行 == 檢查,并判斷兩個 ListView 是一樣的,沒有任何更改,所以也就不需要更新,

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

  @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(
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text('Row $i'),
      ),
    );
  }
}

高效且有效的創建一個串列的方法是使用 ListView.Builder,這個方法非常適用于動態串列或者擁有大量資料的串列,這基本上就是 Android 里的 RecyclerView,會為你自動回收串列項:

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

  @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(
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length));
          print('row $i');
        });
      },
      child: Padding(
        padding: EdgeInsets.all(10.0),
        child: Text('Row $i'),
      ),
    );
  }
}

建接收兩個引數的 ListView.Builder,兩個引數分別是串列的初始長度和一個 ItemBuilder 方法,ItemBuilder 方法和 Android adapter 里的 getView 方法類似;它通過位置回傳你期望在這個位置渲染的串列項,
需要注意 onTap() 方法不再重建串列項,但是會執行 .add 操作,

12 文字處理

12.1 Text Widget 設定自定義字體

差異23:
Android 中可以創建一個字體資源檔案并將其傳給 TextView 的 FontFamily 引數;
Flutter中將字體檔案放入一個檔案夾,并在 pubspec.yaml 檔案中參考它,就和匯入圖片一樣,

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

將字體賦值給你的 Text Widget:

@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'),
      ),
    ),
  );
}

12.2 更改Text Widget樣式

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

13 表單輸入

13.1 Input中的hint

Flutter中通過向 Text Widget 構造器的 decoration 引數傳入一個 InputDecoration 物件來為輸入框展示一個“提示”或占位文本:

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

12.2 顯示驗證錯誤的資訊

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

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

14 資料庫和本地儲存

14.1 使用SharedPreference

差異24:
Android中使用 SharedPreferences API 來存盤少量的鍵值對;
Flutter中使用 Shared_Preferences 插件 實作此功能,這個插件同時包裝了 Shared Preferences 和 NSUserDefaults(iOS 平臺對應 API)的功能,

import 'package:flutter/material.dart';

import 'package:shared_preferences/shared_preferences.dart';

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

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

14.2 使用SQLite

差異25:
Android中使用 SQLite 來存盤可以通過 SQL 進行查詢的結構化資料;
Flutter中使用 SQFlite 插件實作此功能,

添加依賴:

dependencies:
  ...
  sqflite: ^1.3.0

匯入sqflite.dart:

import 'package:sqflite/sqflite.dart';

打開資料庫:
SQLite資料庫是一個被路徑定義在檔案系統中的檔案,可以通過getDatabasesPath()方法獲取該路徑,

var databasesPath = await getDatabasesPath();
var path = join(databasesPath, dbName);

// Make sure the directory exists
try {
  await Directory(databasesPath).create(recursive: true);
} catch (_) {}

打開

var db = await openDatabase('my_db.db');

讀寫模式:讀寫模式是默認模式
配置:onConfigure是第一個可選的回呼呼叫,它允許執行資料庫初始化,如支持級聯洗掉

_onConfigure(Database db) async {
  // Add support for cascade delete
  await db.execute("PRAGMA foreign_keys = ON");
}

var db = await openDatabase(path, onConfigure: _onConfigure);

預加載資料:

_onCreate(Database db, int version) async {
  // Database is created, create the table
  await db.execute(
    "CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
  // populate data
  await db.insert(...);
}

// Open the database, specifying a version and an onCreate callback
var db = await openDatabase(path,
    version: 1,
    onCreate: _onCreate);

只讀模式

// open the database in read-only mode
var db = await openReadOnlyDatabase(path);

Handle Corruption

/// Check if a file is a valid database file
///
/// An empty file is a valid empty sqlite file
Future<bool> isDatabase(String path) async {
  Database db;
  bool isDatabase = false;
  try {
    db = await openReadOnlyDatabase(path);
    int version = await db.getVersion();
    if (version != null) {
      isDatabase = true;
    }
  } catch (_) {} finally {
    await db?.close();
  }
  return isDatabase;
}

防止資料庫被鎖
如果多次使用singleInstance: false打開同一個資料庫可能會出現:

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

避免同時訪問:

class Helper {
  final String path;
  Helper(this.path);
  Future<Database> _db;

  Future<Database> getDb() {
    _db ??= _initDb();
    return _db;
  }

  // Guaranteed to be called only once.
  Future<Database> _initDb() async {
    final db = await openDatabase(this.path);
    // do "tons of stuff in async mode"
    return db;
  }
}

如果不需要資料庫資源可以關閉釋放:

await db.close();

遷移資料庫:
第一版創建一個帶有name列的Column表

/// Create tables
void _createTableCompanyV1(Batch batch) {
  batch.execute('DROP TABLE IF EXISTS Company');
  batch.execute('''CREATE TABLE Company (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
)''');
}

// First version of the database
db = await factory.openDatabase(path,
    options: OpenDatabaseOptions(
        version: 1,
        onCreate: (db, version) async {
          var batch = db.batch();
          _createTableCompanyV1(batch);
          await batch.commit();
        },
        onDowngrade: onDatabaseDowngradeDelete));

第二版向Column物體中加入一個Employee和description列

/// Let's use FOREIGN KEY constraints
Future onConfigure(Database db) async {
  await db.execute('PRAGMA foreign_keys = ON');
}

/// Create Company table V2
void _createTableCompanyV2(Batch batch) {
  batch.execute('DROP TABLE IF EXISTS Company');
  batch.execute('''CREATE TABLE Company (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    description TEXT
)''');
}

/// Update Company table V1 to V2
void _updateTableCompanyV1toV2(Batch batch) {
  batch.execute('ALTER TABLE Company ADD description TEXT');
}

/// Create Employee table V2
void _createTableEmployeeV2(Batch batch) {
  batch.execute('DROP TABLE IF EXISTS Employee');
  batch.execute('''CREATE TABLE Employee (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    companyId INTEGER,
    FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE
)''');
}

// 2nd version of the database
db = await factory.openDatabase(path,
    options: OpenDatabaseOptions(
        version: 2,
        onConfigure: onConfigure,
        onCreate: (db, version) async {
          var batch = db.batch();
          // We create all the tables
          _createTableCompanyV2(batch);
          _createTableEmployeeV2(batch);
          await batch.commit();
        },
        onUpgrade: (db, oldVersion, newVersion) async {
          var batch = db.batch();
          if (oldVersion == 1) {
            // We update existing table and create the new tables
            _updateTableCompanyV1toV2(batch);
            _createTableEmployeeV2(batch);
          }
          await batch.commit();
        },
        onDowngrade: onDatabaseDowngradeDelete));

Raw SQL查詢:

// Get a location using getDatabasesPath
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');

// Delete the database
await deleteDatabase(path);

// open the database
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  // When creating the db, create the table
  await db.execute(
      'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});

// Insert some records in a transaction
await database.transaction((txn) async {
  int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  print('inserted1: $id1');
  int id2 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
      ['another name', 12345678, 3.1416]);
  print('inserted2: $id2');
});

// Update some record
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, value = ? WHERE name = ?',
    ['updated name', '9876', 'some name']);
print('updated: $count');

// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
  {'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
  {'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));

// Count the records
count = Sqflite
    .firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);

// Delete a record
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);

// Close the database
await database.close();

SQL幫助:

final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';

class Todo {
  int id;
  String title;
  bool done;

  Map<String, Object?> toMap() {
    var map = <String, Object?>{
      columnTitle: title,
      columnDone: done == true ? 1 : 0
    };
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }

  Todo();

  Todo.fromMap(Map<String, Object?> map) {
    id = map[columnId];
    title = map[columnTitle];
    done = map[columnDone] == 1;
  }
}

class TodoProvider {
  Database db;

  Future open(String path) async {
    db = await openDatabase(path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute('''
create table $tableTodo ( 
  $columnId integer primary key autoincrement, 
  $columnTitle text not null,
  $columnDone integer not null)
''');
    });
  }

  Future<Todo> insert(Todo todo) async {
    todo.id = await db.insert(tableTodo, todo.toMap());
    return todo;
  }

  Future<Todo> getTodo(int id) async {
    List<Map> maps = await db.query(tableTodo,
        columns: [columnId, columnDone, columnTitle],
        where: '$columnId = ?',
        whereArgs: [id]);
    if (maps.length > 0) {
      return Todo.fromMap(maps.first);
    }
    return null;
  }

  Future<int> delete(int id) async {
    return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
  }

  Future<int> update(Todo todo) async {
    return await db.update(tableTodo, todo.toMap(),
        where: '$columnId = ?', whereArgs: [todo.id]);
  }

  Future close() async => db.close();
}

讀取結果
Assuming the following read results:

List<Map<String, Object?>> records = await db.query('my_table');

Resulting map items are read-only

// get the first record
Map<String, Object?> mapRead = records.first;
// Update it in memory...this will throw an exception
mapRead['my_column'] = 1;
// Crash... `mapRead` is read-only

You need to create a new map if you want to modify it in memory:

// get the first record
Map<String, Object?> map = Map<String, Object?>.from(mapRead);
// Update it in memory now
map['my_column'] = 1;

事務(Transaction)
不要使用資料庫,而只使用事務中的事務物件訪問資料庫,

await database.transaction((txn) async {
  // Ok
  await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');
  
  // DON'T  use the database object in a transaction
  // this will deadlock!
  await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});

如果回呼不拋出錯誤,則會進行事務,如果出現錯誤,則事務將被取消,因此,單向回滾事務的一種方式就是拋出一個例外,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/297611.html

標籤:其他

上一篇:移動開發技術(Android)——實驗3 Android常用基本控制元件(一)

下一篇:Flutter ColorTween實作顏色過渡影片效果

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more