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 像素比例 |
|---|---|
| ldpi | 0.75x |
| mdpi | 1.0x |
| hdpi | 1.5x |
| xhdpi | 2.0x |
| xxhdpi | 3.0x |
| xxxhdpi | 4.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
標籤:其他
