一、前言
學習了佈局例項和互動後,算是對Flutter
入門了,基本可以實現一些普通頁面搭建和互動效果了。但是這遠遠還不夠,現在App
都是需要網路訪問的,而今天的目標就是學習IO
和網路這一塊。
二、Dart中的非同步任務訊息迴圈機制
Dart
是單執行緒模型,什麼是單執行緒模型呢?單執行緒就是在程式執行時,所走的程式路徑按照連續順序排列下來,前面的必須處理好,後面的才會執行(就是同一個時刻只能執行一個操作)。生活中舉個例子,在早上上班時,需要指紋打卡,正要打卡的時候,突然來電話,這時候你接電話,接完電話再打卡,從接電話到打卡這操作就是單執行緒,也就是說,在接電話的時候,打卡這個操作是阻塞的,得要接完電話才能打卡。什麼是非同步?在計算機領域中,非同步指的是執行緒不需要一直等待下去,而是繼續執行下面的操作,不管其他執行緒的狀態,當由訊息返回時系統會通知執行緒進行處理。就好像在平時生活中,我現在需要煮飯,我並不是等著飯煮熟才能去做其他事,而是把電飯鍋煮飯按鈕按下,然後就可以去看電視,看書等等,等飯好了電飯鍋的按鈕會跳到保溫狀態,這時候你就可以吃飯了。這時候我也想到了再Android
中OkHttp
的同步和非同步請求:
- 同步:向後臺傳送一個網路請求,等待後臺返回資料結果,然後再傳送下一個網路請求
- 非同步:向後臺傳送一個網路請求,不需要等待後臺返回資料,隨時可以傳送下一個網路請求
但是Flutter
中的非同步有些不一樣,下面慢慢講述。
1.事件迴圈體系
1.1.Event-Looper
Dart
是單執行緒模型,並沒有主執行緒/子執行緒之分,Dart
是Event loops
和Event Queue
模型,而EventLooper
將所有的事件依次執行,直接上圖:
Dart
中的Event
處理模式,當產生一個Event
之後,會進入Event queue
,而Event loop
從EventQueue
中獲取Event
並且處理。
1.2.單執行緒模型
當一個Dart
函式開始執行,那麼它就會執行到這個函式結束,也就是函式不會被其他程式碼所打斷。這裡首先解釋一下什麼是Dart
中的isolate
。isolate
本身是隔離的意思,有自己的記憶體和單執行緒控制的實體,因為isolate
之間的記憶體在邏輯是隔離的,isolate
的程式碼是按順序執行的。在Dart
中併發可以使用用isolate
,isolate
和Thread
很像,但是isolate
之間沒有共享記憶體。一個Dart
程式是在Main isolate
的Main函式開始,我們平時開發中,預設環境就是Main isolate
,App的啟動入口main
函式就是一個isolate
,在Main函式結束後,Main isolate
執行緒開始一個一個處理Event Queue
中的每一個Event
。
1.3.Dart的訊息迴圈和訊息佇列
一個Dart
Main isolate只有一個訊息迴圈(Event Looper)和兩個訊息佇列:Event佇列和MicroTask佇列。
- Event佇列包含所有外來事件:I/O,mouse events(滑鼠事件),drawing events(繪圖),timers(計時器),isolate之間的message等
- microTask佇列在
Dart
中是很有必要的,因為有時候事件處理想要在稍後完成一些任務但又希望是在執行下一個事件訊息之前。
Event佇列包含Dart
和系統中其他位置的事件,MicroTask只包含Dart
的程式碼,那麼Event Looper處理兩個佇列的順序是如下圖,當main
方法退出後,Event Looper就開始它的工作,首先會以FIFO的順序執行MicroTask(先執行簡短的非同步任務),當所有的microtask執行完就會從Event佇列去提取事件執行,這樣反覆,直到兩個佇列都是空。
1.4.通過連結方式指定任務順序
new Future(() => futureTask) //非同步任務的函式
.then((d) => "execute value:$d") //任務執行完後的子任務
.then((d) => d.length) //其中d為上個任務執行完後的返回的結果
.then((d) => printLength(d))
.whenComplete(() => whenTaskCompelete); //當所有任務完成後的回撥函式
}
複製程式碼
可以看到,上述程式碼明確表示前後的依賴關係,可以使用then()()
來表明要使用變數就必須要等設定完這個變數。還可以使用whenComplete()
,非同步完成時的回撥。
1.5.Event佇列
使用new Future
或者new Future.delayed()
來向Event佇列新增事件,也就是說Future操作是通過Event佇列來處理,如下面程式碼:
//向event佇列中新增一個任務
new Future(() {
//具體任務
});
複製程式碼
想要在兩秒後將任務新增到Event佇列
// 兩秒以後將任務新增至event佇列
new Future.delayed(const Duration(seconds:2), () {
//任務具體程式碼
});
複製程式碼
因為上面說過,上面這個任務想要執行必須滿足main
方法執行完,Misrotask佇列是空的,這個任務之前的任務需要執行完,所以這個任務被執行有可能大於2秒。
1.6.MicroTask佇列
scheduleMicrotask(() {
// 具體邏輯
});
複製程式碼
上面就是將一個任務加到MicroTask佇列中去。
1.7.例子1
import 'dart:async';
main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 2'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 3'));
new Future(() => print('future #3 of 3'));
scheduleMicrotask(() => print('microtask #2 of 2'));
print('main #2 of 2');
}
複製程式碼
輸出結果:
main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)
複製程式碼
上面執行順序:main方法 ->Microtask佇列->Event佇列(先 new Future 後new Future.delay),下面直接拿官方的例子實踐一下:
1.8.例子2
import 'dart:async';
main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 3'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 4'))
.then((_) => print('future #2a'))
.then((_) {
print('future #2b');
scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
})
.then((_) => print('future #2c'));
scheduleMicrotask(() => print('microtask #2 of 3'));
new Future(() => print('future #3 of 4'))
.then((_) => new Future(
() => print('future #3a (a new future)')))
.then((_) => print('future #3b'));
new Future(() => print('future #4 of 4'));
scheduleMicrotask(() => print('microtask #3 of 3'));
print('main #2 of 2');
}
複製程式碼
輸出結果:
main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)
複製程式碼
上面兩個小例子會加深對事件訊息的理解。
三、Dart中的非同步支援
因為Dart
是單執行緒語言,當遇到延遲的運算(I/O操作),執行緒中順序執行的運算就會阻塞,那就app上,使用者操作就會感到卡頓,於是通常用非同步處理來解決這個問題,當遇到需要延遲的運算時,就會放入延遲運算的佇列中,先把不需要延遲的運算先執行,最後再來處理延遲運算。Dart
類庫有非常多的返回Future
或者Stream
物件的函式,這些函式被稱為非同步函式;它們會在設定好一些需要消耗一定時間的操作之後返回,比如I/O操作,而不是等到這個操作完成。
1.Future
什麼是Future
,顧名思義,表示一件將來會發生的事情(也就是不會立即執行),將來可以從Future
中取到一個值,當一個方法返回一個Future
的事情,發生兩件事:
- 這個方法將某件事情排隊,返回一個未完成的
Future
。 - 這個方法事情完畢後,
Future
的狀態會變成已經完成,這個時候可以取到這件事情的返回值。
Future
表示一個非同步操作的最終完成(或失敗)及其結果值的表示,簡單來說,它就是用來處理非同步操作的,非同步處理成功就執行成功的操作,非同步處理失敗就捕獲錯誤或者停止後續操作,一個Future
只會對應一個結果,要麼成功,要麼失敗。
1.1.Future.then
main() {
create();
}
//模執行延時任務
void create(){
//延遲三秒執行
Future.delayed(new Duration(seconds: 3),(){
return "This is data";
}).then((data){
print(data);
});
}
複製程式碼
輸出結果如下:
This is data
複製程式碼
上面可以發現,使用Future.delayed
建立一個延時任務,當三秒後通過then
data接收了這個所返回的This is data
這個字串的值。下面讀取一個檔案,先建立一個檔案:
main() {
create();
}
void create(){
//延遲三秒執行
var file = File("/Users/luguian/Downloads/flutter第五天/flutter.rtf");
//定義了返回結果值為String型別
Future<String> data = file.readAsString();
//返回檔案內容
data.then((text){
//列印檔案內容
print(text);
});
print("I love Android");
}
複製程式碼
I love Android -->先列印I love Android
{\rtf1\ansi\ansicpg936\cocoartf1561\cocoasubrtf600
{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2083\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0
\f0\fs24 \cf0 I love flutter --->文字內容
\f1 ;}
複製程式碼
可以發現,先輸出I love Android
,然後讀取檔案這種超時的操作會後執行,也就是讀取檔案內容是將來執行的,then
接收非同步並列印出結果。注意:Future並不是並行執行的。
1.2.Future.catchError
當非同步任務發生錯誤,可以在catchError
中捕獲錯誤,例子如下:
Future.delayed(new Duration(seconds: 3),(){
throw AssertionError("This is a Error");
}).then((data){
//這是成功的邏輯
print("success");
}).catchError((e){
//失敗會走到這裡
print(e);
});
複製程式碼
輸出結果如下:
Assertion failed
複製程式碼
可以發現,在非同步任務中丟擲了一個異常,then
的回撥函式不會執行,反而catchError
函式被呼叫,當然並不是只有catchError
才能捕獲錯誤,then
方法有一個可選的引數onError
,可以用它來捕獲異常:
Future.delayed(new Duration(seconds: 3),(){
throw AssertionError("This is a Error");
}).then((data){
//這是成功的邏輯
print("success");
},onError:(e){
print(e);
});
複製程式碼
輸出結果:
Assertion failed
複製程式碼
1.3.Future.whenComplete
有很多時候,當非同步任務無論成功或者失敗都需要做一些事的場景,如在網路請求前彈出載入進度框,在請求結束後關閉進度框,下面用whenComplete
進行回撥,例子如下:
Future.delayed(new Duration(seconds: 3),(){
throw AssertionError("This is a Error");
}).then((data){
//這是成功的邏輯
print("success");
},onError:(e){
//這是失敗的邏輯
print(e);
}).whenComplete((){
print("無論失敗,或者成功都會走到這");
});
}
複製程式碼
輸出結果如下:
Assertion failed
無論失敗,或者成功都會走到這
複製程式碼
1.4.Future.wait
有時候,需要等待多個非同步任務都執行結束後才進行一些操作,如有一個介面,需要從兩個介面獲取資料,獲取成功後,將兩個資料進行處理後顯示在UI介面上,這時候,Future.wait
派出用上了,它接收一個Future
陣列引數,只有陣列中所有的Future
執行成功後,就會觸發then
回撥,當然,只要有一個Future
執行失敗就會觸發錯誤回撥,下面實現一下當兩個非同步任務都執行成功時,將結果列印出來:
Future.wait([
//3秒後返回結果
Future.delayed(new Duration(seconds: 3),(){
return "Android";
}),
//4秒後返回結果
Future.delayed(new Duration(seconds: 4),(){
return " And Future";
})
]).then((data){
//成功邏輯
print(data[0] + data[1]);
}).catchError((e){
//捕捉錯誤
print(e);
});
}
複製程式碼
輸出結果如下:
Android And Future
複製程式碼
可以看到當兩個非同步任務完成才會回撥then
函式。
2.Async/await
使用Async/await
也是可以實現非同步操作,下面直接上例子:
main() {
create();
}
void create(){
String data = getData();
print(data);
print("I love Future");
}
getData() async{
return await "I love Android";
}
複製程式碼
執行上面程式碼,報錯了:
type 'Future<dynamic>' is not a subtype of type 'String'
複製程式碼
報的是型別不匹配?為什麼呢?經過一番搜查,發現getData
是一個非同步操作函式,它的返回值是一個await
延遲執行的結果。在Dart
中,有await
標記的運算,其結果值是一個Future
物件,Future
並不是String型別,就報錯了。那麼怎麼才正確獲得非同步的結果呢?Dart規定async標記的函式,只能由await來呼叫,下面改成這樣:
main() {
create();
}
void create() async{
String data = await getData();
print(data);
print("I love Future");
}
getData() async{
return await "I love Android";
}
複製程式碼
下面直接去掉async
函式包裝,直接在getData
方法裡對data
進行賦值:
String data;
main() {
create();
}
void create(){
getData();
print("I love Future");
}
getData() async{
data = await "I love Android";
print(data);
}
複製程式碼
上面輸出結果是:
I love Future
I love Android
複製程式碼
可以發現,先輸出的是I love Future
後面再輸出I love Android
,可以發現當函式被async
修飾時,會先去執行下面的操作,當下面的操作執行完,然後再執行被async
修飾的方法。async
用來表示函式是非同步的,定義的函式會返回一個Future
物件,await
後面是一個Future
,表示等待該非同步任務完成,非同步完成後才會往下走。要注意以下幾點:
- await關鍵字必須在async函式內部使用,也就是加await不加async會報錯。
- 呼叫async函式必須使用await關鍵字,如果加async不加await會順序執行程式碼。
下面再上例子:
main() {
_startMethod();
_method_C();
}
_startMethod() async{
_method_A();
await _method_B();
print("start結束");
}
_method_A(){
print("A開始執行這個方法~");
}
_method_B() async {
print("B開始執行這個方法~");
await print("後面執行這句話~");
print("繼續執行這句哈11111~");
}
_method_C(){
print("C開始");
}
複製程式碼
結果如下:
A開始執行這個方法~
B開始執行這個方法~
後面執行這句話~
C開始
繼續執行這句哈11111~
start結束
複製程式碼
- 當使用async作為方法名字尾宣告時,說明這個方法的返回值是一個Future;
- 當執行到該方法程式碼用await關鍵字標註時,會暫停該方法其他部分執行;
- 當await關鍵字引用的Future執行完成,下一行程式碼會立即執行。
也就是首先執行_startMethod
這個方法用async宣告瞭,因為方法裡呼叫了_method_A
,所以先輸出print("A開始執行這個方法~");,後面執行_method_B()
,這個方法用await關鍵字宣告,所以會暫停print("start結束");的執行,然後繼續執行_method_B()
將 print("B開始執行這個方法~");輸出,下一行遇到await關鍵字,會暫停其他程式碼的執行。當await關鍵字引用的Future執行完成(也就是執行print("後面執行這句話~"),_method_C()
方法會立即執行,然後執行繼續執行這句哈11111~,最後執行print("start結束");
3.Stream
Stram
是接收非同步事件資料,和Future
不同的是,它可以接收多個非同步操作的結果,那麼Stram
常用於在多次讀取資料的非同步任務場景,直接上例子:
void create(){
Stream.fromFutures([
//2秒後返回結果
Future.delayed(new Duration(seconds: 2),(){
return "Android";
}),
//3秒後丟擲一個異常
Future.delayed(new Duration(seconds: 3),(){
return AssertionError("error");
}),
//4秒後返回結果
Future.delayed(new Duration(seconds: 4),(){
return "Flutter";
})
]).listen((result){
//列印接收的結果
print(result);
},onError: (e){
//錯誤回撥
print(e.message);
},onDone: (){
});
}
複製程式碼
上面可以發現Stream
可以通過觸發成功或者失敗傳遞結果或者錯誤。
四、檔案操作
有很多時候需要將檔案儲存到本地,這時候就需要用檔案讀寫介面來實現,PathProvider
外掛提供一種平臺透明的方式來訪問裝置檔案系統上的常用位置。該類當前支援兩個檔案系統位置:
- 臨時目錄:系統可隨時清除的臨時目錄(快取)。在iOS上,這對應於
NSTemporaryDirectory()
返回的值。在Android上,這是getCacheDir()
返回的值。 - 文件目錄:應用程式的目錄,用於儲存只有自己可以訪問的檔案,只有當應用程式被解除安裝時,系統才會清除目錄。在iOS上,這對應於
NSDocumentDirectory
。在Android上,這是AppData
目錄。
在Flutter
裡實現檔案讀寫,需要使用path_provider
和Dart
裡的I/O
模組,兩者的職責並不一樣,path_provider
是負責查詢iOS或者Android下的目錄檔案,而I/O
是負責檔案的讀寫操作。
1.獲取本地路徑
下面使用path_provider
來查詢本地的路徑,首先在pubspec.xml
檔案新增依賴:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
path_provider: ^0.4.1 -->新增依賴
複製程式碼
或者臨時目錄,文件目錄,sd卡目錄如下:
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
....
class _LoadFileState extends State<LoadFile>{
@override
void initState(){
super.initState();
}
@override
Widget build(BuildContext context){
return new Scaffold(
appBar: new AppBar(
title: new Text("LoadFile"),
),
body: new Center(
child: RaisedButton(
child: Text("獲取檔案路徑"),
//點選呼叫獲取檔案路徑方法
onPressed: loadPath,
),
),
);
}
}
loadPath() async{
try{
//臨時目錄
var _tempDir = await getTemporaryDirectory();
//獲取具體路徑
String tempDirPath = _tempDir.path;
//文件目錄
var _document = await getApplicationDocumentsDirectory();
String documentPath = _document.path;
//sd卡目錄
var _sdCard = await getExternalStorageDirectory();
String sdCardPath = _sdCard.path;
//列印路徑
print("臨時目錄:"+ tempDirPath);
print("文件目錄:"+ documentPath);
print("sd卡目錄:"+ sdCardPath);
}catch(err){
print(err);
}
}
複製程式碼
輸出結果(Android)如下:
I/flutter (19375): 臨時目錄:/data/user/0/com.example.loadflie/cache
I/flutter (19375): 文件目錄:/data/user/0/com.example.loadflie/app_flutter
I/flutter (19375): sd卡目錄:/storage/emulated/0
複製程式碼
2.讀取本地檔案內容
讀取檔案少不了許可權的問題,在Dart Packages可以找到simple_permissions
這個庫來簡化申請許可權的步驟,按照上面說明跟著操作就可以:
AndroidManifest
和Info.plist
檔案下新增許可權,身為Android coder對AndroidManifest
這個檔案很熟悉,這個檔案是對Android而言,而Info.plist
應該是對於iOS
而言,那下面先在Android上試試看,首先,在pubspec.yaml
上新增依賴:
simple_permissions: ^0.1.9
複製程式碼
記得點選Packages get
命令。
接著在AndroidManifest
清單檔案上新增對檔案的讀寫許可權:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
複製程式碼
下面在手機sd card內部儲存新建一個txt檔案,嘗試獲取其內容:
import 'package:simple_permissions/simple_permissions.dart';//記得加上這句話
...
//讀取檔案方法
readData() async {
try {
//申請讀檔案的許可權
var permission =
SimplePermissions.requestPermission(Permission.ReadExternalStorage);
var sdCardPath = getExternalStorageDirectory();
//當獲取到路徑的時候
sdCardPath.then((filePath) {
//獲得讀取檔案許可權
permission.then((permission_status) async {
//獲取檔案內容
var data = await File(filePath.path + "/flutter.txt").readAsString();
print(data);
});
});
} catch (e) {
print(e);
}
}
複製程式碼
按鈕點選方法改為readData
:
child: RaisedButton(
child: Text("獲取檔案路徑"),
onPressed: readData
),
複製程式碼
點選按鈕結果執行:
選擇始終執行: 輸出就是flutter.txt
檔案內容:
I/flutter (24038): flutter is very good.
複製程式碼
注意如果不加讀寫許可權,會丟擲異常:
I/flutter (25428): FileSystemException: Cannot open file, path = '/storage/emulated/0/flutter.txt' (OS Error: Permission denied, errno = 13)
複製程式碼
3.寫入檔案操作
//把內容寫入檔案操作
writeData() async{
try {
//申請讀檔案的許可權
var permission =
SimplePermissions.requestPermission(Permission.WriteExternalStorage);
var sdCardPath = getExternalStorageDirectory();
//當獲取到路徑的時候
sdCardPath.then((filePath) {
//獲得讀取檔案許可權
permission.then((permission_status) async {
//把內容寫進檔案
var data = await File(filePath.path + "/flutter.txt").writeAsString("點滴之行,看世界");
print(data);
});
});
} catch (e) {
print(e);
}
}
複製程式碼
開啟sd card的flutter.txt
檔案看看內容:
append
模式,很簡單,把預設的FileMode mode: FileMode.write
方式改為FileMode mode: FileMode.append
,程式碼如下:
//把內容寫進檔案 現在以追加的方式
var data = await File(filePath.path + "/flutter.txt").writeAsString("Flutter is very good",
mode: FileMode.append);
複製程式碼
執行結果:
好了,簡單的讀寫檔案就實現了。五、sqflite資料庫
Android
和iOS
中都會有SQLite
,那麼Flutter
有沒有呢?答案是肯定有的。Flutter
中的SQLite
資料庫是同時支援Android
和iOS
的,它的名字叫sqflite
,支援事務和批量操作,支援插入/查詢/更新/刪除操作等,是輕量級的關係型資料庫。
下面先簡單實現一個登入介面,進行簡單的資料操作:
//用無狀態控制元件顯示
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//主題色
theme: ThemeData(
//設定為藍色
primarySwatch: Colors.red),
//這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
home: DataBaseWidget(),
);
}
}
//主框架
class DataBaseWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _DataBaseState();
}
}
class _DataBaseState extends State<DataBaseWidget> {
@override
Widget build(BuildContext context) {
return new Scaffold(
//appBar
appBar: AppBar(
title: Text("Sqlite簡單操作"),
//標題居中
centerTitle: true,
),
body: new ListView(
children: <Widget>[
//使用者輸入使用者資訊widget
Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: InputMessageWidget(),
),
//資料庫表的一些基本操作,增,刪,改,查
Padding(
padding: const EdgeInsets.all(16),
child: SqliteHandleWidget(),
),
],
),
);
}
}
複製程式碼
使用者輸入資訊的Widget
如何:
//使用者名稱和密碼
class InputMessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
//這個是為了使用者輸入結束後,讓密碼輸入框獲取到焦點
FocusNode secondTextFieldNode = FocusNode();
return Column(
children: <Widget>[
TextField(
//文字內容改變觸發
onChanged: (user) {
//獲取使用者名稱
username = user;
},
//輸入法裝飾器
decoration: InputDecoration(
//標籤
labelText: '名字',
//hint 提示使用者輸入什麼
hintText: '請輸入英文或者數字'),
//最大為一行
maxLines: 1,
//文字提交觸發
onSubmitted: (result) {
FocusScope.of(context).reparentIfNeeded(secondTextFieldNode);
},
),
TextField(
onChanged: (pwd) {
//獲取使用者密碼
password = pwd;
},
//是否隱藏輸入 false 表示不隱藏,true表示隱藏
obscureText: true,
maxLines: 1,
decoration: InputDecoration(
labelText: '密碼',
hintText: '請輸入密碼',
),
//鍵盤輸入型別
keyboardType: TextInputType.text,
onSubmitted: (data) {},
),
],
);
}
}
複製程式碼
對資料庫表操作的按鈕佈局如下:
//資料庫元件操作
class SqliteHandleWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _SqliteHandleWidgetState();
}
}
class _SqliteHandleWidgetState extends State<SqliteHandleWidget> {
//資料庫名稱
String myDataBase = "usermessage.db";
//資料庫路徑
String myDataBasePath = "";
//資料庫中的表 簡單一點,就建立三個欄位,分別是主鍵,使用者名稱,密碼
String sql_createUserTable = "CREATE TABLE user("
"id INTEGER PRIMARY KEY,"
"username TEXT,"
"password TEXT)";
//查詢資料庫表的數目
String sql_queryCount = 'SELECT COUNT(*) FROM user';
//具體查詢資料庫表的所有資訊
String sql_queryMessage = 'SELECT * FROM user';
//這是從資料庫表返回資料
var _data;
@override
Widget build(BuildContext context) {
return Column(
//交叉軸設定中間
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 40.0,
child: RaisedButton(
textColor: Colors.black,
child: Text("建立資料庫表"),
onPressed: null,
),
),
Row(
//主軸方向中心對齊
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new RaisedButton(
textColor: Colors.black,
child: new Text('增'),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text('刪'),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text('改'),
onPressed: null),
],
),
Row(
//主軸方向中心對齊
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new RaisedButton(
textColor: Colors.black,
child: new Text('查條數'),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text('查資訊'),
onPressed: null),
],
),
Padding(
padding: const EdgeInsets.all(16.0),
child: new Text('具體結果是:$_data'),
),
],
);
}
}
複製程式碼
在上面_SqliteHandleWidgetState
賦值資料庫名字為usermessage.db
,建立資料庫表user
語句很簡單,就三個欄位,分別是主鍵,使用者名稱,使用者密碼,介面如下:
1.建立資料庫和資料表
首先新增依賴:可以到Dart包管理網站去查詢sqlite依賴最新版本。
sqflite: ^1.1.0
複製程式碼
並在檔案引入:
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
複製程式碼
注意:對於資料庫的操作都是耗時操作,都要通過非同步來處理。
//建立資料庫
Future<String> createDataBase(String db_name) async {
//在文件目錄建立
var document = await getApplicationDocumentsDirectory();
//獲取路徑 join是path包下的方法,就是將兩者路徑連線起來
String path = join(document.path, db_name);
//邏輯是如果資料庫存在就把它刪除然後建立
var _directory = new Directory(dirname(path));
bool exists = await _directory.exists();
if (exists) {
//必存在 這裡是為了每次建立資料庫表先表刪除則刪除資料庫表
await deleteDatabase(path);
} else {
try {
//不存在則建立目錄 如果[recursive]為false,則只有路徑中的最後一個目錄是
//建立。如果[recursive]為真,則所有不存在的路徑
//被建立。如果目錄已經存在,則不執行任何操作。
await new Directory(dirname(path)).create(recursive: true);
} catch (e) {
print(e);
}
}
return path;
}
//建立資料庫表方法
cratedb_table() async {
//得到資料庫的路徑
myDataBasePath = await createDataBase(myDataBase);
//開啟資料庫
Database my_db = await openDatabase(myDataBasePath);
//建立資料庫表
await my_db.execute(sql_createUserTable);
//關閉資料庫
await my_db.close();
setState(() {
_data = "建立usermessage.db成功,建立user表成功~";
});
}
複製程式碼
給按鈕新增點選方法:
child: RaisedButton(
textColor: Colors.black,
child: Text("建立資料庫表"),
onPressed: cratedb_table,
),
複製程式碼
執行,安裝完apk,用Device File Exploder
來看看內部儲存檔案:
synchronize
來重新整理一下:
發現在app_flutter
下多了usermessage.db
檔案,確實資料庫建立成功了,那繼續下面的操作。
2.增加資料
下面實現增加資料,可以用rawInsert
或者db.insert
方式對資料庫表資料進行增加(插入),實際上都是通過insert into
方式來插入資料表,下面就用rawInsert
方式來增加一條資料:
//增加方法
addData() async {
//首先開啟資料庫
Database my_db = await openDatabase(myDataBasePath);
//插入資料
String add_sql = "INSERT INTO user(username,password) VALUES('$username','$password')";
await my_db.transaction((tran) async{
await tran.rawInsert(add_sql);
});
//關閉資料庫
await my_db.close();
setState(() {
_data = "增加一條資料成功,名字是:$username,密碼是:$password";
});
}
複製程式碼
3.查詢具體資料
為了配合增加資料,把查詢資料庫表的功能實現:
//查詢具體數值
queryDetail() async{
//開啟資料庫
Database my_db = await openDatabase(myDataBasePath);
//將資料放到集合裡面顯示
List<Map> dataList = await my_db.rawQuery(sql_queryMessage);
await my_db.close();
setState(() {
_data = "具體資料詳情如下:$dataList";
});
}
複製程式碼
查詢資料表很簡單,實際上只用rawQuery
這個方法,把增加和查詢方法繫結到按鈕點選上:
new RaisedButton(
textColor: Colors.black, child: new Text('改'), onPressed: null),
....
new RaisedButton(
textColor: Colors.black,
child: new Text('查資訊'),
onPressed: queryDetail),
複製程式碼
驗證結果,流程是:
- 先輸入使用者名稱和密碼
- 點選增加
- 點選查資訊: 執行結果如下:
4.刪除資料
下面實現刪除資料:
//刪除一條資料
delete() async {
Database my_db = await openDatabase(myDataBasePath);
//根據id來刪除 也可以根據其他資訊來刪除 例如名字
String delete_ssql = "DELETE FROM user WHERE id = ?";
//返回所更改的數目
int delete_count = await my_db.rawDelete(delete_ssql,['1']);
//關閉資料庫
await my_db.close();
//狀態更新
setState(() {
if(delete_count == 1){
_data = "刪除成功~";
} else {
_data = "刪除失敗,請看錯誤日誌~";
}
});
}
複製程式碼
記得給刪除按鈕繫結方法,執行結果就不貼了。
5.修改資料
修改資料我相信在平時開發中是用的最頻繁的操作了,直接上實現例子:
//修改資料方法
update() async{
//資料庫
Database my_db = await openDatabase(myDataBasePath);
String update_sql = "UPDATE user SET username = ? WHERE id = ?";
await my_db.rawUpdate(update_sql,['paul','1']);
await my_db.close();
setState(() {
_data = "資料修改成功,請查閱~";
});
}
複製程式碼
上面用了rawUpdate
對資料庫表進行內容資料更新,也可以用db.update
來更新,自己可以根據需求變更去修改固定欄位或者整條資料。上面我是根據id
這個條件來修改一條資料,將id
為1的資料的名字改為paul
。
6.查詢條數
//查詢有幾條
query_num() async{
//資料庫
Database my_db = await openDatabase(myDataBasePath);
//用sqflite包的方法firstInValue
int data_count = Sqflite.firstIntValue(await my_db.rawQuery(sql_queryCount));
await my_db.close();
setState(() {
_data = "資料條數:$data_count";
});
}
複製程式碼
對本地資料庫的基本操作實現了一遍,下面學習網路請求操作。
六、網路請求操作
Flutter
的請求網路有多種方式,一種是使用dart io
中的HttpClient
發起的請求,一種是使用dio
庫,另一種是使用http
庫,先學一下get
和post
,put
、delete
就等後面用到在學。下面就實踐:
1.dart io發起的請求
1.1.get請求
import 'dart:io';//導IO包
import 'dart:convert';//解碼和編碼JSON
void main() {
_get();
}
_get() async{
var responseBody;
//1.建立HttpClient
var httpClient = new HttpClient();
//2.構造Uri
var requset = await httpClient.getUrl(Uri.parse("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1"));
//3.關閉請求,等待響應
var response = await requset.close();
//4.進行解碼,獲取資料
if(response.statusCode == 200){
//拿到請求的資料
responseBody = await response.transform(utf8.decoder).join();
//先不解析列印資料
print(responseBody);
}else{
print("error");
}
}
複製程式碼
結果如下:
1.2.post請求
_post() async{
var responseBody;
//1.建立HttpClient
var httpClient = new HttpClient();
//2.構造Uri
var requset = await httpClient.postUrl(Uri.parse("http://www.wanandroid.com/user/login?username=1&password=123456"));
//3.關閉請求,等待響應
var response = await requset.close();
//4.進行解碼,獲取資料
if(response.statusCode == 200){
//拿到請求的資料
responseBody = await response.transform(utf8.decoder).join();
//先不解析列印資料
print(responseBody);
}else{
print("error");
}
}
複製程式碼
返回結果如下:
2.dio請求
dio是一個強大的Dart Http
請求庫,支援Restful API
、FormData
、攔截器、錯誤處理、轉換器、設定Http代理、請求取消、Cookie
管理、檔案上傳和下載、超時等。在pub.flutter-io.cn/packages搜最新的依賴包,這個網址太好用,你想搜一些三方庫裡面都有:
pubspec.yaml
新增依賴:
dio: ^2.0.14
複製程式碼
匯入依賴:
import 'package:dio/dio.dart';
複製程式碼
2.1.get請求
//dio get請求
dio_get() async{
try{
Response response;
//等待返回response
response = await Dio().get("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1");
if(response.statusCode == 200){
print(response);
}else{
print("error");
}
}catch(e){
print(e);
}
}
複製程式碼
2.2.post請求
dio_post() async{
try{
Response response;
response = await Dio().post("http://www.wanandroid.com/user/login?username=1&password=123456");
if(response.statusCode == 200){
print(response);
}else{
print("error");
}
}catch(e){
print(e);
}
}
複製程式碼
效果同樣是ok的。
3.http庫
繼續去上面連結搜最新的包,是http 0.12.0+1
,在pubspec.yaml
下新增依賴,在檔案匯入包:
import 'package:http/http.dart' as my_http;
複製程式碼
上面這次匯入庫的方式有一點點區別,多了as
這個關鍵字,這是什麼意思呢?通過as
是為了解決變數名衝突的方法,因為匯入不同的庫有可能遇到不同庫之間因為匯入變數名衝突的問題。
3.1.get請求
//http庫的get請求方式
http_get() async{
try{
//因為匯入http 用了as xxx方式,所以物件請求都用xxx.get方式
var response = await my_http.get("http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1");
if(response.statusCode == 200){
//列印返回的資料
print(response.body);
}else{
print("error");
}
}catch(e){
print(e);
}
}
複製程式碼
3.2.post請求
//http庫的post請求方式
http_post() async{
try{
//因為匯入http 用了as xxx方式,所以物件請求都用xxx.get方式
var response = await my_http.post("http://www.wanandroid.com/user/login?username=1&password=123456");
if(response.statusCode == 200){
//列印返回的資料
print(response.body);
}else{
print("error");
}
}catch(e){
print(e);
}
}
複製程式碼
以上三種庫的get
和psot
方式都實踐了一遍,在平時開發中最好用dio
庫和http
庫,因為dart io
中是使用HttpClient
發起的請求,HttpClient
本身功能較弱,很多常用功能不支援。
七、JSON
現在很難想象移動應用程式不需要與後臺互動或者儲存結構化資料。現在開發,資料傳輸方式基本都是用JSON
,在Flutter
中是沒有GSON/Jackson/Moshi
這些庫,因為這些庫需要執行時反射,在Flutter
是禁用的。執行時反射會干擾Dart
的_tree shaking_。使用_tree shaking_,可以在發版是"去除"未使用的程式碼,來優化軟體的大小。由於反射會預設使用所有程式碼,因此_tree shaking_會很難工作,這些工具無法知道哪些widget
在執行時未被使用,因此冗餘程式碼很難剝離,使用反射時,應用尺寸無法輕鬆進行優化,雖然不能在Flutter
使用執行時反射,但有些庫提供了型別簡單易用的API
,但它們是基於程式碼生成的。下面學學在Flutter
中如何操作JSON
資料的使用JSON
有兩個常規策略:
- 手動序列化和反序列化
- 通過程式碼生成自動序列化和反序列化
不同的專案有不同的複雜度和場景,針對於小的專案,使用程式碼生成器可能會殺豬用牛刀了。對於具有多個
JSON model
的複雜應用程式,手動序列化可能會比較繁瑣,且容易出錯。
1.手動序列化JSON
Flutter
中基本的JSON序列化非常簡單,Flutter
有一個內建的dart:convert
庫,其中包含一個簡單的JSON解碼器和編碼器。下面簡單實現一下:
1.1.內連序列化JSON
首先記得導庫:
import 'dart:convert';
複製程式碼
然後根據字串解析:
//內連序列化JSON
decodeJson() {
var data= '{"name": "Knight","email": "Knight@163.com"}';
Map<String,dynamic> user = json.decode(data);
//輸出名字
print("Hello,my name is ${user['name']}");
//輸出郵箱
print("Hello,This is my email ${user['email']}");
}
複製程式碼
結果輸出:
I/flutter ( 5866): Hello,my name is Knight
I/flutter ( 5866): Hello,This is my email Knight@163.com
複製程式碼
這樣,可以獲得我們想要的資料了,我覺得這種方法很實用又能簡單理解,但是不幸的是,JSON.decode()
僅返回一個Map<String,dynamci>
,這意味著當直到執行才知道值的型別,這種方法會失去大部分靜態型別語言特性:型別安全、自動補全和編譯時異常。這樣的話,程式碼變得非常容易出錯,就好像上面我們訪問name
欄位,打字打錯了,達成namr
。但是這個JSON
在map結構中,編譯器不知道這個錯誤的欄位名(編譯時不會報錯)。為了解決所說的問題,模型類中序列化JSON的作用出來了。
1.2.模型類中序列化JSON
通過引入一個簡單的模型類(model class)來解決前面提到的問題,建立一個User
類,在類內部有兩個方法:
User.fromJson
建構函式,用於從一個map構造出一個User
例項map structuretoJson
方法,將User
例項化一個map 這樣呼叫的程式碼就具有型別安全、自動補全和編譯時異常,當拼寫錯誤或欄位型別視為其他型別,程式不會通過編譯,那就避免執行時崩潰。
1.2.1.user.dart
新建一個model資料夾,用來放實體,在其檔案下新建User.dart
:
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'],
email = json['email'];
Map<String, dynamic> toJson() =>
{
'name': name,
'email': email,
};
}
複製程式碼
呼叫如下:
import 'model/User.dart';//記得新增
....
//使用模型類反序列化
decodeModelJson(){
var data= '{"name": "Knight","email": "Knight@163.com"}';
Map userMap = json.decode(data);
var user = new User.fromJson(userMap);
//列印出名字
print("Hello,my name is ${user.name}");
//列印出郵箱
print("Hello,my name is ${user.email}");
}
複製程式碼
把序列化邏輯到移到模型本身內部,採用這種方法,反序列化資料就很簡單了。序列化一個user,只是將User
物件傳遞給該JSON.encode
方法:
//序列化一個user
encodeModelJson(){
var user = new User("Knight","Knight163.com");
String user_json = json.encode(user);
print(user_json);
}
複製程式碼
結果輸出:
I/flutter ( 6684): {"name":"Knight","email":"Knight163.com"}
複製程式碼
2.使用程式碼生產庫序列化JSON
下面使用json_serializable package
包,它是一個自動化的原始碼生成器,可以為開發者生成JSON序列化魔板。
2.1.新增依賴
要包含json_serializable
到專案中,需要一個常規和兩個開發依賴項,開發依賴項是不包含在應用程式原始碼中的依賴項:
dependencies:
# Your other regular dependencies here
json_annotation: ^2.0.0
dev_dependencies:-->開發依賴項
# Your other dev_dependencies here
build_runner: ^1.1.3 -->最新版本1.2.8 因為我sdk版本比較低 所以用低版本
json_serializable: ^2.0.2
複製程式碼
2.2.程式碼生成
有兩種執行程式碼生成器的方法:
- 一次性生成,在專案根目錄執行
flutter packages pub run build_runner build
,可以在需要為我們的model
生成json
序列化程式碼。這觸發一次性構建,它通過原始檔,挑選相關的併為它們生成必要的序列化程式碼。這個非常方便,但是如果我們不需要每次在model類中進行更改都要手動執行構建命令的話會更好。 - 持續生成,使用_watcher_可以使原始碼生成的過程更加方便,它會監視專案中文化的變化,並在需要時自動構建必要的檔案,通過
flutter packages pub run build_runner watch
在專案根目錄執行啟動_watcher_,只需啟動一次觀察器,然後並讓它在後臺執行,這是安全的。
將上面的User.dart
修改成下面:
import 'package:json_annotation/json_annotation.dart';
part 'User.g.dart';-->一開始爆紅
//這個標註是告訴生成器,這個類是需要生成Model類的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
factory User.fromJson(Map<String, dynamic> json){--->一開始爆紅
return _$UserFromJson(json);
}
Map<String, dynamic> toJson() { --->一開始爆紅
return _$UserToJson(this);
}
}
複製程式碼
下面就用一次性生成命令,在專案根目錄開啟命令列執行:
最後發現會在當前目錄生成User.g.dart
檔案:
裡面的內容可以自己去看看看,就是反序列化/序列化的操作。注意:沒生成User.g.dart
執行多幾次命令即可。
最後通過json_serializable
方式反序列化JSON
字串,不需要對先前程式碼修改:
2.3.反序列化
var data= '{"name": "Knight","email": "Knight@163.com"}';
Map userMap = json.decode(data);
var user = new User.fromJson(userMap);
//列印出名字
print("Hello,my name is ${user.name}");
//列印出郵箱
print("Hello,my name is ${user.email}");
複製程式碼
2.4.序列化
var user = new User("Knight","Knight163.com");
String user_json = json.encode(user);
print(user_json);
複製程式碼
結果是跟上面一樣,不過這種方式額外多了生成一個檔案...
八、例子
下面實現一個簡單例子,效果圖如下:
返回的json格式是如下:{
"error": false,
"results": [{
"_id": "5c6a4ae99d212226776d3256",
"createdAt": "2019-02-18T06:04:25.571Z",
"desc": "2019-02-18",
"publishedAt": "2019-02-18T06:05:41.975Z",
"source": "web",
"type": "\u798f\u5229",
"url": "https://ws1.sinaimg.cn/large/0065oQSqly1g0ajj4h6ndj30sg11xdmj.jpg",
"used": true,
"who": "lijinshanmx"
}, {
"_id": "5c6385b39d21225dd7a417ce",
"createdAt": "2019-02-13T02:49:23.946Z",
"desc": "2019-02-13",
"publishedAt": "2019-02-13T02:49:33.16Z",
"source": "web",
"type": "\u798f\u5229",
"url": "https://ws1.sinaimg.cn/large/0065oQSqly1g04lsmmadlj31221vowz7.jpg",
"used": true,
"who": "lijinshanmx"
}]
}
複製程式碼
上面是一個內嵌陣列,需要增加兩個實體類,如下: ViewResult類如下:
import 'ResultModel.dart';
class ViewResult{
bool error;
List<ResultModel> list;
ViewResult(joinData){
//獲得返回的error值
error = joinData['error'];
list = [];
print(joinData['results']);
//獲得"results"裡的內容
if(joinData['results'] != null){
for(var dataItem in joinData['results']){
list.add(new ResultModel(dataItem));
}
}
}
複製程式碼
ResultModel類如下:
class ResultModel{
String _id;
String createdAt;
String desc;
String publishedAt;
String source;
String type;
String url;
bool used;
String who;
ResultModel(jsonData){
_id = jsonData['_id'];
createdAt = jsonData['createdAt'];
desc = jsonData['desc'];
publishedAt = jsonData['publishedAt'];
source = jsonData['source'];
type = jsonData['type'];
url = jsonData['url'];
used = jsonData['used'];
who = jsonData['who'];
}
}
複製程式碼
ListView的Item佈局:
//需要傳list 和對應下標
Widget photoWidget(List<ResultModel> resultLists,int index){
return Card(
child: Container(
height: 300,
child: Row(
children: <Widget>[
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.network(resultLists[index].url,
fit:BoxFit.fitWidth,
//scale: 2.5,
),
),
),
],
),
),
);
}
複製程式碼
所有程式碼如下:
import 'package:flutter/material.dart';
import 'dart:convert';//解碼和編碼JSON
import 'package:http/http.dart' as my_http;
import 'model/ViewResult.dart';
import 'model/ResultModel.dart';
//app入口
void main() {
runApp(MyApp());
}
//用無狀態控制元件顯示
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//主題色
theme: ThemeData(
//設定為藍色
primarySwatch: Colors.red),
//這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
home: BigPhotoWidget(),
);
}
}
//主框架
class BigPhotoWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return new _BigPhotoState();
}
}
class _BigPhotoState extends State<BigPhotoWidget> {
ViewResult viewresult;
//具體的資料集合
List<ResultModel> resultLists = [];
@override
void initState(){
super.initState();
getData();
}
getData() async{
try{
//因為匯入http 用了as xxx方式,所以物件請求都用xxx.get方式
//方式一
// await my_http.get("http://gank.io/api/data/福利/10/1")
// .then((response){
// if(response.statusCode == 200){
// var ViewData = json.decode(response.body);
// viewresult = ViewResult(ViewData);
// if(!viewresult.error){
// //繼續解析
// for(int i = 0;i < viewresult.list.length;i++){
// resultLists.add(viewresult.list[i]);
// }
// //記得呼叫重新整理
// setState(() {
//
// });
// }
// }else{
// print("error");
// }
// });
//方式二 請求
var response = await my_http.get("http://gank.io/api/data/福利/10/1");
//判斷狀態
if(response.statusCode == 200){
//解析
var ViewData = json.decode(response.body);
viewresult = ViewResult(ViewData);
if(!viewresult.error){
//繼續解析
for(int i = 0;i < viewresult.list.length;i++){
resultLists.add(viewresult.list[i]);
}
//記得呼叫重新整理
setState(() {
});
}
}else{
print("error");
}
}catch(e){
print(e);
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
//appBar
appBar: AppBar(
title: Text("妹子圖"),
//標題居中
centerTitle: true,
),
body: ListView.builder(
itemCount: resultLists.length,
itemBuilder: (BuildContext context,int index){
return Column(
children: <Widget>[
photoWidget(resultLists,index),
],
);
},
),
);
}
}
//需要傳list 和對應下標
Widget photoWidget(List<ResultModel> resultLists,int index){
return Card(
child: Container(
height: 300,
child: Row(
children: <Widget>[
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.network(resultLists[index].url,
fit:BoxFit.fitWidth,
//scale: 2.5,
),
),
),
],
),
),
);
}
複製程式碼
上面獲取資料有兩種方式。
九、總結
- 知道Dart的簡單大致執行模型,當啟動一個Flutter應用時,會建立一個isolate,然後會初始化兩個佇列MicroTask、Event,和初始化一個訊息迴圈,程式碼執行方式和順序取決於MicroTask和Event佇列。
- Future和async都不是並行執行的。
- 簡單的本地資料庫操作。
- 對Json資料簡單處理(序列化和反序列化)方式。
- 網路簡單的請求。
作為萌新,肯定有很多技術沒學到位,如有錯誤,歡迎指出指正,謝謝~