【Flutter】应用开发笔记
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替換如下內容
插件版本和gradle版本匹配
查看gradle版本
android/build.gradle
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.zip3 熱重載(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: true5 視圖(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.@overrideWidget 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!';});}@overrideWidget 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樹定義布局。
5.4 在布局中添加或刪除組件
差異④:
Android中通過調用父 View 的 addChild() 或 removeChild() 方法動態地添加或者刪除子 View;
Flutter中由于 Widget 是不可變的,所以沒有 addChild() 的直接對應的方法。可以給返回一個 Widget 的父 Widget 傳入一個方法,并通過布爾標記值toggle控制子 Widget 的創建。
5.5 Widget實現動畫
差異⑤:
Android中既可以通過 XML 文件定義動畫,也可以調用 View 對象的 animate() 方法;
Flutter中使用動畫庫,通過將 Widget 嵌入一個動畫 Widget 的方式實現 Widget 的動畫效果。
Flutter 通過 Animation 的子類 AnimationController 來暫停、播放、停止以及逆向播放動畫。它需要一個 Ticker 在垂直同步 (vsync) 的時候發出信號,并且在運行的時候創建一個介于 0 和 1 之間的線性插值。然后就可以創建一個或多個 Animation,并將它們綁定到控制器上。
下面的例子展示了如何實現一個點擊 FloatingActionButton 的時候將一個 Widget 漸變為一個圖標的 FadeTransition:
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);@overrideWidget 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 過濾器:
在 MainActivity 中處理 intent,提取出其它 intent 分享的文本并保存。當 Flutter 準備好處理的時候,它會使用一個平臺通道請求數據,數據便會從原生端發送過來:
public class MainActivity extends FlutterActivity {private String sharedText;private static final String CHANNEL = "app.channel.shared.data";@Overrideprotected 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}}}@Overridepublic 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.@overrideWidget 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';@overridevoid initState() {super.initState();getSharedText();}@overrideWidget 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:
在你的位置 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 內:
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 parsereplyTo.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。通過代碼邏輯使用一個布爾標記值控制進度條的渲染。
7 工程與資源文件
7.1 放置分辨率相關圖片文件
差異14:
Android中區分對待資源文件 (resources) 和資產文件 (assets);
Flutter中只有資產文件 (assets)。
| 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 上由于沒有對應的事件,因此不會觸發此事件 |
9 布局
9.1 Flutter中的LinearLayout
差異17:
Android中LinearLayout 用于線性布局 widget 的水平或者垂直;
Flutter中使用 Row 或者 Column Widget 來實現相同的效果。
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。
9.4 Flutter中屏幕旋轉
在AndroidManifest.xml中聲明:
android:configChanges="orientation|screenSize"10 手勢監聽和觸摸事件處理
10.1 Widget添加監聽器
差異19:
Android中通過調用 setOnClickListener 方法在按鈕這樣的 View 上添加點擊監聽器;
Flutter中有兩種添加觸摸監聽器的方法。
方法一:
如果 Widget 支持事件監聽,那么向它傳入一個方法并在方法中處理事件。例如,RaisedButton 有一個 onPressed 參數:
方法二:
如果 Widget 不支持事件監聽,將 Widget 包裝進一個 GestureDetector 中并向 onTap 參數傳入一個方法:
10.2 處理其他手勢
使用GestureDetector監聽的手勢:
Tap
| onTapUp | 一個產生了點擊事件的指針停止觸摸屏幕的特定位置 |
| onTap | 一個點擊事件已經發生 |
| onTapCancel | 之前觸發了 onTapDown 事件的指針不會產生點擊事件 |
Double tap
Long press
Vertical drag
| onVerticalDragUpdate | 觸摸屏幕的指針在垂直方向移動了更多的距離 |
| onVerticalDragEnd | 之前和屏幕接觸并垂直移動的指針不再繼續和屏幕接觸,并且在和屏幕停止接觸的時候以一定的速度移動 |
Horizontal drag
| onHorizontalDragUpdate | 觸摸屏幕的指針在水平方向移動了更多的距離 |
| onHorizontalDragEnd | 之前和屏幕接觸并水平移動的指針不再繼續和屏幕接觸,并且在和屏幕停止接觸的時候以一定的速度移動 |
雙擊旋轉 Flutter 標志的 GestureDetector:
11 Listviews和adapters
11.1 Flutter中的ListView
差異20:
Android中創建一個 adapter 并將其傳給 ListView, ListView 渲染 adapter 返回的每一行內容。要確保回收了每一行視圖,否則會遇到各種奇怪的界面和內存問題;
Flutter中要向 ListView 傳入一組 widget, Flutter 會保證滑動的快速順暢。
11.2 監聽點擊列表項
差異21:
Android中ListView 有一個可以定位哪個列表項被點擊了的方法;
Flutter中使用傳入 widget 的觸摸監聽。
11.3 動態更新ListView
差異22:
Android中要更新 adapter 并調用 notifyDataSetChanged;
Flutter中在 setState() 里創建一個新的 List,并將數據從舊列表拷貝到新列表。如果在 setState() 里更新一組 widget,數據并沒有更新到界面上。這是因為當 setState() 被調用的時候, Flutter 渲染引擎會查看 Widget 樹是否有任何更改。當引擎檢查到 ListView,他會執行 == 檢查,并判斷兩個 ListView 是一樣的。沒有任何更改,所以也就不需要更新。
高效且有效的創建一個列表的方法是使用 ListView.Builder,這個方法非常適用于動態列表或者擁有大量數據的列表。這基本上就是 Android 里的 RecyclerView,會為你自動回收列表項:
class _SampleAppPageState extends State<SampleAppPage> {List<Widget> widgets = [];@overridevoid initState() {super.initState();for (int i = 0; i < 100; i++) {widgets.add(getRow(i));}}@overrideWidget 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 文件中引用它,就和導入圖片一樣。
將字體賦值給你的 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;@overrideWidget 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)的功能。
14.2 使用SQLite
差異25:
Android中使用 SQLite 來存儲可以通過 SQL 進行查詢的結構化數據;
Flutter中使用 SQFlite 插件實現此功能。
添加依賴:
dependencies:...sqflite: ^1.3.0導入sqflite.dart:
import 'package:sqflite/sqflite.dart';打開數據庫:
SQLite數據庫是一個被路徑定義在文件系統中的文件,可以通過getDatabasesPath()方法獲取該路徑。
打開
var db = await openDatabase('my_db.db');讀寫模式:讀寫模式是默認模式
配置:onConfigure是第一個可選的回調調用。它允許執行數據庫初始化,如支持級聯刪除
預加載數據:
_onCreate(Database db, int version) async {// Database is created, create the tableawait db.execute("CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");// populate dataawait 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打開同一個數據庫可能會出現:
避免同時訪問:
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表
第二版向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 tableawait 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:
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-onlyYou 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)
不要使用數據庫,而只使用事務中的事務對象訪問數據庫。
如果回調不拋出錯誤,則會進行事務。如果出現錯誤,則事務將被取消。因此,單向回滾事務的一種方式就是拋出一個異常。
總結
以上是生活随笔為你收集整理的【Flutter】应用开发笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 太平绅士CIO
- 下一篇: hive报错Could not get