1. 網絡編程與JSON解析
- 默認的HttpClient請求網絡
get() async {
//創建網絡調用示例,設置通用請求行爲(超時時間)
var httpClient = HttpClient();
httpClient.idleTimeout = Duration(seconds: 5);
//構造URI,設置user-agent爲"Custom-UA"
var uri = Uri.parse("https://flutter.dev");
var request = await httpClient.getUrl(uri);
request.headers.add("user-agent", "Custom-UA");
//發起請求,等待響應
var response = await request.close();
//收到響應,打印結果
if (response.statusCode == HttpStatus.ok) {
print(await response.transform(utf8.decoder).join());
} else {
print('Error: \nHttp status ${response.statusCode}');
}
}
- 在 Flutter 中,所有網絡編程框架都是以 Future 作爲異步請求的包裝
- http是Dart官方的另一個網絡請求類,需要添加依賴
http: '>=0.11.3+12'
httpGet() async {
//創建網絡調用示例
var client = http.Client();
//構造URI
var uri = Uri.parse("https://flutter.dev");
//設置user-agent爲"Custom-UA",隨後立即發出請求
http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});
//打印請求結果
if(response.statusCode == HttpStatus.ok) {
print(response.body);
} else {
print("Error: ${response.statusCode}");
}
}
- dio,一般使用這個,dio是一個強大的Dart Http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、自定義適配器等…添加依賴
dio: '>2.1.3'
void getRequest() async {
//創建網絡調用示例
Dio dio = new Dio();
//設置URI及請求user-agent後發起請求
var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
//打印請求結果
if(response.statusCode == HttpStatus.ok) {
print(response.data.toString());
} else {
print("Error: ${response.statusCode}");
}
}
//下載-------------
//使用FormData表單構建待上傳文件
FormData formData = FormData.from({
"file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
"file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),
});
//通過post方法發送至服務端
var responseY = await dio.post("https://xxx.com/upload", data: formData);
print(responseY.toString());
//使用download方法下載文件
dio.download("https://xxx.com/file1", "xx1.zip");
//增加下載進度回調函數
dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
//do something
});
//並行請求--------------
//同時發起兩個並行請求
List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);
//打印請求1響應結果
print("Response1: ${responseX[0].toString()}");
//打印請求2響應結果
print("Response2: ${responseX[1].toString()}");
//攔截器-----------------
//增加攔截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options){
//爲每個請求頭都增加user-agent
options.headers["user-agent"] = "Custom-UA";
//檢查是否有token,沒有則直接報錯
if(options.headers['token'] == null) {
return dio.reject("Error:請先登錄");
}
//檢查緩存是否有數據
if(options.uri == Uri.parse('http://xxx.com/file1')) {
return dio.resolve("返回緩存數據");
}
//放行請求
return options;
}
));
//增加try catch,防止請求報錯
try {
var response = await dio.get("https://xxx.com/xxx.zip");
print(response.data.toString());
}catch(e) {
print(e);
}
2.JSON解析
- 只能手動解析.
import 'dart:convert';
String jsonString = '''
{
"id":"123",
"name":"張三",
"score" : 95,
"teacher": { "name": "李四", "age" : 40 }
}
''';
//json解析
//所謂手動解析,是指使用 dart:convert 庫中內置的 JSON 解碼器,將 JSON 字符串解析成自定義對象的過程。
class Teacher {
String name;
int age;
Teacher({this.name, this.age});
factory Teacher.fromJson(Map<String, dynamic> parsedJson) {
return Teacher(name: parsedJson['name'], age: parsedJson['age']);
}
@override
String toString() {
return 'Teacher{name: $name, age: $age}';
}
}
class Student {
String id;
String name;
int score;
Teacher teacher;
Student({this.id, this.name, this.score, this.teacher});
//從Map中取
factory Student.fromJson(Map<String, dynamic> parsedJson) {
return Student(
id: parsedJson['id'],
name: parsedJson['name'],
score: parsedJson['score'],
teacher: Teacher.fromJson(parsedJson['teacher']));
}
@override
String toString() {
return 'Student{id: $id, name: $name, score: $score, teacher: $teacher}';
}
}
void main() {
final jsonResponse = json.decode(jsonString);//將字符串解碼成Map對象
Student student = Student.fromJson(jsonResponse);//手動解析
print(student.teacher.name);
}
- json解析比較耗時,放compute中去進行,不用擔心阻塞UI了. compute得有Widget才行.
3. 數據持久化
- 由於 Flutter 僅接管了渲染層,真正涉及到存儲等操作系統底層行爲時,還需要依託於原生 Android、iOS.
- 三種數據持久化方法,即文件、SharedPreferences 與數據庫
- Flutter 提供了兩種文件存儲的目錄,即臨時(Temporary)目錄與文檔(Documents)目錄:
3.1 文件
需要引入: path_provider: ^1.6.4
//創建文件目錄
Future<File> get _localFile async {
final directory = await getApplicationDocumentsDirectory();
final path = directory.path;
return File('$path/content.txt');
}
//將字符串寫入文件
Future<File> writeContent(String content) async {
final file = await _localFile;
return file.writeAsString(content);
}
//從文件讀出字符串
Future<String> readContent() async {
try {
final file = await _localFile;
String contents = await file.readAsString();
return contents;
} catch (e) {
return "";
}
}
3.2 SharedPreferences
需要引入: shared_preferences: ^0.5.6+2
//讀取SharedPreferences中key爲counter的值
Future<int>_loadCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0);
return counter;
}
//遞增寫入SharedPreferences中key爲counter的值
Future<void>_incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
prefs.setInt('counter', counter);
}
3.3 數據庫
需要引入: sqflite: ^1.2.1
dbDemo() async {
final Future<Database> database = openDatabase(
//join是拼接路徑分隔符
join(await getDatabasesPath(), 'student_database.db'),
onCreate: (db, version) => db.execute(
"CREATE TABLE students(id TEXT PRIMARY KEY,name TEXT,score INTEGER)"),
onUpgrade: (db, oldVersion, newVersion) {
//dosth for 升級
},
version: 1,
);
Future<void> insertStudent(Student std) async {
final Database db = await database;
await db.insert(
'students',
std.toJson(),
//插入衝突策略,新的替換舊的
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
//插入3個
await insertStudent(student1);
await insertStudent(student2);
await insertStudent(student3);
Future<List<Student>> students() async {
final Database db = await database;
final List<Map<String, dynamic>> maps = await db.query('students');
return List.generate(maps.length, (i) => Student.fromJson(maps[i]));
}
////讀取出數據庫中插入的Student對象集合
students().then((list) => list.forEach((s) => print(s.name)));
//釋放數據庫資源
final Database db = await database;
db.close();
}
4. Flutter調原生
- 用AS單獨打開Flutter項目中的Android工程,寫代碼,每次寫完代碼rebuild一下.然後想讓Flutter代碼能調到Android這邊的代碼,得重新運行.
- 如果AS run窗口不展示任何消息,可以使用 命令
flutter run lib/native/invoke_method.dart
執行dart,然後看錯誤消息. - Flutter發起方法調用請求開始,請求經由唯一標識符指定的方法通道到達原生代碼宿主,而原生代碼宿主則通過註冊對應方法實現,響應並處理調用請求.最後將執行結果通過消息通道,回傳至Flutter.
- 方法通道是非線程安全的,需要在UI線程(Android或iOS的主線程)回調.
- 數據持久化,推送,攝像頭,藍牙等,都需要平臺支持
- 輕量級解決方案: 方法通道機制 Method Channel
- 調用示例:
class _MyHomePageState extends State<MyHomePage> {
//聲明MethodChannel
static const platform = MethodChannel('com.xfhy.basic_ui/util');
handleButtonClick() async {
bool result;
//捕獲 萬一失敗了呢
try {
//異步等待,可能很耗時 等待結果
result = await platform.invokeMethod('isEmpty', "have data");
} catch (e) {
result = false;
}
print('result : $result');
}
}
//Android代碼
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
//參考: https://flutter.dev/docs/development/platform-integration/platform-channels
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.xfhy.basic_ui/util").setMethodCallHandler { call, result ->
//判斷方法名是否支持
if (call.method == "isEmpty") {
val arguments = call.arguments
result.success(StringUtil.isEmpty(arguments as? String))
print("success")
} else {
//方法名暫不支持
result.notImplemented()
print("fail")
}
}
}
}
- Android或者iOS的數據會被序列化成一段二進制格式的數據在通道中傳輸,當該數據傳遞到Flutter後,又會被反序列化成Dart語言中的類型.
5. Flutter中複用原生控件
- 除去地圖、WebView、相機等涉及底層方案的特殊情況外,大部分原生代碼能夠實現的 UI 效果,完全可以用 Flutter 實現.
- 使用這種方式對性能造成非常大的影響且不方便維護.
- 方法通道: 原生邏輯複用
- 平臺視圖: 原生視圖複用
6. Android項目中嵌入Flutter
官網地址: https://flutter.dev/docs/development/add-to-app
- FlutterEngine 文檔: https://github.com/flutter/flutter/wiki/Experimental:-Reuse-FlutterEngine-across-screens
- FlutterView 文檔: https://github.com/flutter/flutter/wiki/Experimental:-Add-Flutter-View
- API一會兒就過時了,得去官網看最新的才行.
- 可以在Android App中開啓Flutter的Activity,Flutter的Activity是在另外一個進程,第一次進入特別慢.也可以加入Flutter的View和Fragment
- 在Android工程下新建一個Flutter的module比較簡單直接
7. 混合開發導航棧
- Android跳轉Flutter,依賴FlutterView.Flutter在FlutterView中建立了自己的導航棧.
- 通常會將Flutter容器封裝成一個獨立的Activity或者ViewController. 這樣打開一個普通的Activity既是打開Flutter界面了
- Flutter頁面跳轉原生界面,需要利用方法通道,然後用原生去打開響應的界面.
- Flutter實例化成本非常高,每啓動一個Flutter實例,就會創建一套新的渲染機制,即Flutter Engine,以及底層的Isolate.而這些實例之間的內存是不相互共享的,會帶來較大的系統資源消耗.
- 實際開發中,儘量用Flutter去開發閉環的業務模塊.原生跳轉過去就行,剩下的全部由Flutter內部完成. 儘量避免Flutter頁面回到原生頁面,原生頁面又啓動新的Flutter實例的情況.
8. 狀態管理(跨組件傳遞數據,Provider)
- Dart的一個庫,可以實現在StatelessWidget中刷新數據.跨組件傳遞數據.全局共享數據.依賴注入
- 使用Provider後,我們就再也不需要StalefullWidget了.
- Provider以InheritedWidget語法糖的方法,通過數據資源封裝,數據注入,和數據讀寫這3個步驟,爲我們實現了跨組件(跨頁面)之間的數據共享
- 我們既可以用Provider來實現靜態的數據讀傳遞,也可以使用ChangeNotifierProvider來實現動態的數據讀寫傳遞,還用通過MultiProvider來實現多個數據資源的共享
- Provider.of和Consumer都可以實現數據的讀取,並且Consumer還可以控制UI刷新的粒度,避免與數據無關的組件的無謂刷新
- 封裝數據
//定義需要共享的數據模型,通過混入ChangeNotifier管理聽衆
class CounterModel with ChangeNotifier {
int _count = 0;
//讀方法
int get counter => _count;
//寫方法
void increment() {
_count++;
notifyListeners();//通知聽衆刷新
}
}
- 放數據
儘量把數據放到更高的層級
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//通過Provider組件封裝數據資源
//因Provider是InheritedWidget的語法糖,所以它是一個Widget
//ChangeNotifierProvider只能搞一個
//MultiProvider可以搞多個
return MultiProvider(
providers: [
//注入字體大小 下個界面讀出來
Provider.value(value: 30.0),
//注入計數器實例
ChangeNotifierProvider.value(value: CounterModel())
],
child: MaterialApp(
home: FirstPage(),
),
);
}
}
- 讀數據
//示例: 讀數據
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出資源 類型是CounterModel
//獲取計時器實例
final _counter = Provider.of<CounterModel>(context);
//獲取字體大小
final textSize = Provider.of<double>(context);
/*
*
//使用Consumer2獲取兩個數據資源
Consumer2<CounterModel,double>(
//builder函數以參數的形式提供了數據資源
builder: (context, CounterModel counter, double textSize, _) => Text(
'Value: ${counter.counter}',
style: TextStyle(fontSize: textSize))
)
* 我們最多可以使用到 Consumer6,即共享 6 個數據資源。
* */
return Scaffold(
body: Center(
child: Text(
'Counter: ${_counter.counter}',
style: TextStyle(fontSize: textSize),
),
),
floatingActionButton: FloatingActionButton(
child: Text('Go'),
onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => SecondPage())),
),
);
}
}
//示例: 讀和寫數據
//使用Consumer 可以精準刷新發生變化的Widget
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出數據
//final _counter = Provider.of<CounterModel>(context);
return Scaffold(
//使用Consumer來封裝counter的讀取
body: Consumer(
//builder函數可以直接獲取到counter參數
//Consumer 中的 builder 實際上就是真正刷新 UI 的函數,它接收 3 個參數,即 context、model 和 child
builder: (context, CounterModel counter, _) => Center(
child: Text('Value: ${counter.counter}'),
),
),
floatingActionButton: Consumer<CounterModel>(
builder: (context, CounterModel counter, child) => FloatingActionButton(
onPressed: counter.increment,
child: child,
),
child: Icon(Icons.add),
),
);
}
}
9. 適配不同分辨率的手機屏幕
- Flutter中平時寫控件的尺寸,其實有點類似於Android中的dp
- 只能是通過
MediaQuery.of(context).size.width
獲得屏幕寬度來加載什麼佈局 - 豎屏時用什麼佈局,橫屏時用什麼佈局.可以根據屏幕寬度才判斷.
- 如需適配空間等的大小,則需要以切圖爲基準,算出當前設備的縮放係數,在佈局的時候乘一下.
10. 編譯模式
- 根據kReleaseMode這個編譯常數可以判斷出當前是release環境還是debug環境.
- 還可以用個斷言判斷,release編譯的時候會將斷言全部移除.
- 通過使用InheritedWidget爲應用中可配置部分進行抽象封裝(比如接口域名,app名稱等),通過配置多入口方式爲應用的啓動注入配置環境
- 使用kReleaseMode能判斷,但是另一個環境的代碼雖然不能執行到,但是會被打入二進制包中.會增大包體積,儘量使用斷言.或者打release包的時候把kReleaseMode的另一個邏輯註釋掉.
if (kReleaseMode) {
//正式環境
text = "release";
} else {
//測試環境 debug
text = "debug";
}
配置一些app的通用配置
///配置抽象
class AppConfig extends InheritedWidget {
//主頁標題
final String appName;
//接口域名
final String apiBaseUrl;
AppConfig(
{@required this.appName,
@required this.apiBaseUrl,
@required Widget child})
: super(child: child);
//方便其子Widget在Widget樹中找到它
static AppConfig of(BuildContext context) {
return context.inheritFromWidgetOfExactType(AppConfig);
}
//判斷是否需要子Widget更新.由於是應用入口,無需更新
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
}
///爲不同的環境創建不同的應用入口
//main_dev.dart 這個是正式環境的入口
void main() {
var configuredApp = AppConfig(
appName: 'dev', //主頁標題
apiBaseUrl: 'http://dev.example.com/', //接口域名
child: MyApp(),
);
runApp(configuredApp);
}
//main.dart 這個是測試環境的入口
/*void main(){
var configuredApp = AppConfig(){
appName: 'example',//主頁標題
apiBaseUrl: 'http://api.example.com/',//接口域名
child: MyApp(),
}
runApp(configuredApp);
}*/
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
var config = AppConfig.of(context);
return MaterialApp(
title: config.appName,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var config = AppConfig.of(context);
return Scaffold(
appBar: AppBar(
title: Text(config.appName),
),
body: Center(
child: Text(config.apiBaseUrl),
),
);
}
}
//運行開發環境應用程序
//flutter run -t lib/main_dev.dart
//運行生產環境應用程序
//flutter run -t lib/main.dart
/*
*
//打包開發環境應用程序
flutter build apk -t lib/main_dev.dart
flutter build ios -t lib/main_dev.dart
//打包生產環境應用程序
flutter build apk -t lib/main.dart
flutter build ios -t lib/main.dart
* */
11. Hot Reload
- Flutter的熱重載是基於JIT編譯模式的代碼增量同步.由於JIT屬於動態編譯,能夠將Dart代碼編譯成生成中間代碼,讓Dart VM在運行時解釋執行,因此可以通過動態更新中間代碼實現增量同步.
- 熱重載流程分爲5步:
- 掃描工程改動
- 增量編譯
- 推送更新
- 代碼合併
- Widget樹重建
- Flutter接收到代碼變更,不會重新啓動App,只會觸發Widget樹的重新繪製…因此可以保持之前的狀態
- 由於涉及到狀態保存與恢復,因此涉及狀態兼容和狀態初始化的場景,熱重載是無法支持的.(比如改動前後Widget狀態無法兼容,全局變量與靜態屬性的更改,main方法裏面的更改,initState方法裏面更改,枚舉和泛型的更改等)
- 如果遇到了熱重載無法支持的場景,可以點擊工程面板左下角的熱重啓(Hot Restart)按鈕,也很快
12. 關於調試
- debugPrint函數同樣會將消息打印至控制檯,但與print不同的是,它提供了定製打印的能力.正式環境的時候將debugPrint函數定義爲一個空函數體,就可以一鍵實現取消打印的功能了.
// 正式環境 將debugPrint指定爲空的執行體, 所以它什麼也不做
debugPrint = (String message, {int wrapWidth}) {};
debugPrint('test');
//開發環境就需要打印出日誌
debugPrint = (String message, {int wrapWidth}) =>
debugPrintSynchronously(message, wrapWidth: wrapWidth);
- 開啓Debug Painting,有點像原生的繪製佈局邊界.
void main() {
//Debug Painting 界面調試工具
//有點像原生的顯示佈局邊界
debugPaintSizeEnabled = true;
runApp(MyApp());
}
- 還可以使用Flutter Inspector去查看更詳細的可視化信息.
13. 常用命令行
階段 | 子任務 | 命令 |
---|---|---|
工程初始化 | App工程 | flutter create --template=app hello |
工程初始化 | Dart包工程 | flutter create --template=package hello |
工程初始化 | 插件工程 | flutter create --template=plugin hello |
構建 | Debug構建 | flutter build apk --debug flutter build ios --debug |
構建 | Release構建 | flutter build apk --release flutter build ios --release |
構建 | Profile構建 | flutter build apk --profile flutter build ios --profile |
集成原生工程 | 獨立App打包 | flutter build apk --release flutter build ios --release |
集成原生工程 | Pod/AAR打包 | flutter build apk --release flutter build ios --release |