1. 網(wǎng)絡(luò)編程與JSON解析
- 默認的HttpClient請求網(wǎng)絡(luò)
get() async {
//創(chuàng)建網(wǎng)絡(luò)調(diào)用示例,設(shè)置通用請求行為(超時時間)
var httpClient = HttpClient();
httpClient.idleTimeout = Duration(seconds: 5);
//構(gòu)造URI,設(shè)置user-agent為"Custom-UA"
var uri = Uri.parse("https://flutter.dev");
var request = await httpClient.getUrl(uri);
request.headers.add("user-agent", "Custom-UA");
//發(fā)起請求,等待響應(yīng)
var response = await request.close();
//收到響應(yīng),打印結(jié)果
if (response.statusCode == HttpStatus.ok) {
print(await response.transform(utf8.decoder).join());
} else {
print('Error: \nHttp status ${response.statusCode}');
}
}
- 在 Flutter 中,所有網(wǎng)絡(luò)編程框架都是以 Future 作為異步請求的包裝
- http是Dart官方的另一個網(wǎng)絡(luò)請求類,需要添加依賴
http: '>=0.11.3+12'
httpGet() async {
//創(chuàng)建網(wǎng)絡(luò)調(diào)用示例
var client = http.Client();
//構(gòu)造URI
var uri = Uri.parse("https://flutter.dev");
//設(shè)置user-agent為"Custom-UA",隨后立即發(fā)出請求
http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});
//打印請求結(jié)果
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 {
//創(chuàng)建網(wǎng)絡(luò)調(diào)用示例
Dio dio = new Dio();
//設(shè)置URI及請求user-agent后發(fā)起請求
var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
//打印請求結(jié)果
if(response.statusCode == HttpStatus.ok) {
print(response.data.toString());
} else {
print("Error: ${response.statusCode}");
}
}
//下載-------------
//使用FormData表單構(gòu)建待上傳文件
FormData formData = FormData.from({
"file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
"file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),
});
//通過post方法發(fā)送至服務(wù)端
var responseY = await dio.post("https://xxx.com/upload", data: formData);
print(responseY.toString());
//使用download方法下載文件
dio.download("https://xxx.com/file1", "xx1.zip");
//增加下載進度回調(diào)函數(shù)
dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
//do something
});
//并行請求--------------
//同時發(fā)起兩個并行請求
List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);
//打印請求1響應(yīng)結(jié)果
print("Response1: ${responseX[0].toString()}");
//打印請求2響應(yīng)結(jié)果
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:請先登錄");
}
//檢查緩存是否有數(shù)據(jù)
if(options.uri == Uri.parse('http://xxx.com/file1')) {
return dio.resolve("返回緩存數(shù)據(jù)");
}
//放行請求
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 庫中內(nèi)置的 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中去進行,不用擔(dān)心阻塞UI了. compute得有Widget才行.
3. 數(shù)據(jù)持久化
- 由于 Flutter 僅接管了渲染層,真正涉及到存儲等操作系統(tǒng)底層行為時,還需要依托于原生 Android、iOS.
- 三種數(shù)據(jù)持久化方法,即文件、SharedPreferences 與數(shù)據(jù)庫
- Flutter 提供了兩種文件存儲的目錄,即臨時(Temporary)目錄與文檔(Documents)目錄:
3.1 文件
需要引入: path_provider: ^1.6.4
//創(chuàng)建文件目錄
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 數(shù)據(jù)庫
需要引入: 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]));
}
////讀取出數(shù)據(jù)庫中插入的Student對象集合
students().then((list) => list.forEach((s) => print(s.name)));
//釋放數(shù)據(jù)庫資源
final Database db = await database;
db.close();
}
4. Flutter調(diào)原生
- 用AS單獨打開Flutter項目中的Android工程,寫代碼,每次寫完代碼rebuild一下.然后想讓Flutter代碼能調(diào)到Android這邊的代碼,得重新運行.
- 如果AS run窗口不展示任何消息,可以使用 命令
flutter run lib/native/invoke_method.dart執(zhí)行dart,然后看錯誤消息. - Flutter發(fā)起方法調(diào)用請求開始,請求經(jīng)由唯一標(biāo)識符指定的方法通道到達原生代碼宿主,而原生代碼宿主則通過注冊對應(yīng)方法實現(xiàn),響應(yīng)并處理調(diào)用請求.最后將執(zhí)行結(jié)果通過消息通道,回傳至Flutter.
- 方法通道是非線程安全的,需要在UI線程(Android或iOS的主線程)回調(diào).
- 數(shù)據(jù)持久化,推送,攝像頭,藍牙等,都需要平臺支持
- 輕量級解決方案: 方法通道機制 Method Channel
- 調(diào)用示例:
class _MyHomePageState extends State<MyHomePage> {
//聲明MethodChannel
static const platform = MethodChannel('com.xfhy.basic_ui/util');
handleButtonClick() async {
bool result;
//捕獲 萬一失敗了呢
try {
//異步等待,可能很耗時 等待結(jié)果
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的數(shù)據(jù)會被序列化成一段二進制格式的數(shù)據(jù)在通道中傳輸,當(dāng)該數(shù)據(jù)傳遞到Flutter后,又會被反序列化成Dart語言中的類型.
5. Flutter中復(fù)用原生控件
- 除去地圖、WebView、相機等涉及底層方案的特殊情況外,大部分原生代碼能夠?qū)崿F(xiàn)的 UI 效果,完全可以用 Flutter 實現(xiàn).
- 使用這種方式對性能造成非常大的影響且不方便維護.
- 方法通道: 原生邏輯復(fù)用
- 平臺視圖: 原生視圖復(fù)用
6. Android項目中嵌入Flutter
官網(wǎng)地址: 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一會兒就過時了,得去官網(wǎng)看最新的才行.
- 可以在Android App中開啟Flutter的Activity,Flutter的Activity是在另外一個進程,第一次進入特別慢.也可以加入Flutter的View和Fragment
- 在Android工程下新建一個Flutter的module比較簡單直接
7. 混合開發(fā)導(dǎo)航棧
- Android跳轉(zhuǎn)Flutter,依賴FlutterView.Flutter在FlutterView中建立了自己的導(dǎo)航棧.
- 通常會將Flutter容器封裝成一個獨立的Activity或者ViewController. 這樣打開一個普通的Activity既是打開Flutter界面了
- Flutter頁面跳轉(zhuǎn)原生界面,需要利用方法通道,然后用原生去打開響應(yīng)的界面.
- Flutter實例化成本非常高,每啟動一個Flutter實例,就會創(chuàng)建一套新的渲染機制,即Flutter Engine,以及底層的Isolate.而這些實例之間的內(nèi)存是不相互共享的,會帶來較大的系統(tǒng)資源消耗.
- 實際開發(fā)中,盡量用Flutter去開發(fā)閉環(huán)的業(yè)務(wù)模塊.原生跳轉(zhuǎn)過去就行,剩下的全部由Flutter內(nèi)部完成. 盡量避免Flutter頁面回到原生頁面,原生頁面又啟動新的Flutter實例的情況.
8. 狀態(tài)管理(跨組件傳遞數(shù)據(jù),Provider)
- Dart的一個庫,可以實現(xiàn)在StatelessWidget中刷新數(shù)據(jù).跨組件傳遞數(shù)據(jù).全局共享數(shù)據(jù).依賴注入
- 使用Provider后,我們就再也不需要StalefullWidget了.
- Provider以InheritedWidget語法糖的方法,通過數(shù)據(jù)資源封裝,數(shù)據(jù)注入,和數(shù)據(jù)讀寫這3個步驟,為我們實現(xiàn)了跨組件(跨頁面)之間的數(shù)據(jù)共享
- 我們既可以用Provider來實現(xiàn)靜態(tài)的數(shù)據(jù)讀傳遞,也可以使用ChangeNotifierProvider來實現(xiàn)動態(tài)的數(shù)據(jù)讀寫傳遞,還用通過MultiProvider來實現(xiàn)多個數(shù)據(jù)資源的共享
- Provider.of和Consumer都可以實現(xiàn)數(shù)據(jù)的讀取,并且Consumer還可以控制UI刷新的粒度,避免與數(shù)據(jù)無關(guān)的組件的無謂刷新
- 封裝數(shù)據(jù)
//定義需要共享的數(shù)據(jù)模型,通過混入ChangeNotifier管理聽眾
class CounterModel with ChangeNotifier {
int _count = 0;
//讀方法
int get counter => _count;
//寫方法
void increment() {
_count++;
notifyListeners();//通知聽眾刷新
}
}
- 放數(shù)據(jù)
盡量把數(shù)據(jù)放到更高的層級
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//通過Provider組件封裝數(shù)據(jù)資源
//因Provider是InheritedWidget的語法糖,所以它是一個Widget
//ChangeNotifierProvider只能搞一個
//MultiProvider可以搞多個
return MultiProvider(
providers: [
//注入字體大小 下個界面讀出來
Provider.value(value: 30.0),
//注入計數(shù)器實例
ChangeNotifierProvider.value(value: CounterModel())
],
child: MaterialApp(
home: FirstPage(),
),
);
}
}
- 讀數(shù)據(jù)
//示例: 讀數(shù)據(jù)
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出資源 類型是CounterModel
//獲取計時器實例
final _counter = Provider.of<CounterModel>(context);
//獲取字體大小
final textSize = Provider.of<double>(context);
/*
*
//使用Consumer2獲取兩個數(shù)據(jù)資源
Consumer2<CounterModel,double>(
//builder函數(shù)以參數(shù)的形式提供了數(shù)據(jù)資源
builder: (context, CounterModel counter, double textSize, _) => Text(
'Value: ${counter.counter}',
style: TextStyle(fontSize: textSize))
)
* 我們最多可以使用到 Consumer6,即共享 6 個數(shù)據(jù)資源。
* */
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())),
),
);
}
}
//示例: 讀和寫數(shù)據(jù)
//使用Consumer 可以精準刷新發(fā)生變化的Widget
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出數(shù)據(jù)
//final _counter = Provider.of<CounterModel>(context);
return Scaffold(
//使用Consumer來封裝counter的讀取
body: Consumer(
//builder函數(shù)可以直接獲取到counter參數(shù)
//Consumer 中的 builder 實際上就是真正刷新 UI 的函數(shù),它接收 3 個參數(shù),即 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獲得屏幕寬度來加載什么布局 - 豎屏?xí)r用什么布局,橫屏?xí)r用什么布局.可以根據(jù)屏幕寬度才判斷.
- 如需適配空間等的大小,則需要以切圖為基準,算出當(dāng)前設(shè)備的縮放系數(shù),在布局的時候乘一下.
10. 編譯模式
- 根據(jù)kReleaseMode這個編譯常數(shù)可以判斷出當(dāng)前是release環(huán)境還是debug環(huán)境.
- 還可以用個斷言判斷,release編譯的時候會將斷言全部移除.
- 通過使用InheritedWidget為應(yīng)用中可配置部分進行抽象封裝(比如接口域名,app名稱等),通過配置多入口方式為應(yīng)用的啟動注入配置環(huán)境
- 使用kReleaseMode能判斷,但是另一個環(huán)境的代碼雖然不能執(zhí)行到,但是會被打入二進制包中.會增大包體積,盡量使用斷言.或者打release包的時候把kReleaseMode的另一個邏輯注釋掉.
if (kReleaseMode) {
//正式環(huán)境
text = "release";
} else {
//測試環(huán)境 debug
text = "debug";
}
配置一些app的通用配置
///配置抽象
class AppConfig extends InheritedWidget {
//主頁標(biāo)題
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更新.由于是應(yīng)用入口,無需更新
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
}
///為不同的環(huán)境創(chuàng)建不同的應(yīng)用入口
//main_dev.dart 這個是正式環(huán)境的入口
void main() {
var configuredApp = AppConfig(
appName: 'dev', //主頁標(biāo)題
apiBaseUrl: 'http://dev.example.com/', //接口域名
child: MyApp(),
);
runApp(configuredApp);
}
//main.dart 這個是測試環(huán)境的入口
/*void main(){
var configuredApp = AppConfig(){
appName: 'example',//主頁標(biāo)題
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),
),
);
}
}
//運行開發(fā)環(huán)境應(yīng)用程序
//flutter run -t lib/main_dev.dart
//運行生產(chǎn)環(huán)境應(yīng)用程序
//flutter run -t lib/main.dart
/*
*
//打包開發(fā)環(huán)境應(yīng)用程序
flutter build apk -t lib/main_dev.dart
flutter build ios -t lib/main_dev.dart
//打包生產(chǎn)環(huán)境應(yīng)用程序
flutter build apk -t lib/main.dart
flutter build ios -t lib/main.dart
* */
11. Hot Reload
- Flutter的熱重載是基于JIT編譯模式的代碼增量同步.由于JIT屬于動態(tài)編譯,能夠?qū)art代碼編譯成生成中間代碼,讓Dart VM在運行時解釋執(zhí)行,因此可以通過動態(tài)更新中間代碼實現(xiàn)增量同步.
- 熱重載流程分為5步:
- 掃描工程改動
- 增量編譯
- 推送更新
- 代碼合并
- Widget樹重建
- Flutter接收到代碼變更,不會重新啟動App,只會觸發(fā)Widget樹的重新繪制..因此可以保持之前的狀態(tài)
- 由于涉及到狀態(tài)保存與恢復(fù),因此涉及狀態(tài)兼容和狀態(tài)初始化的場景,熱重載是無法支持的.(比如改動前后Widget狀態(tài)無法兼容,全局變量與靜態(tài)屬性的更改,main方法里面的更改,initState方法里面更改,枚舉和泛型的更改等)
- 如果遇到了熱重載無法支持的場景,可以點擊工程面板左下角的熱重啟(Hot Restart)按鈕,也很快
12. 關(guān)于調(diào)試
- debugPrint函數(shù)同樣會將消息打印至控制臺,但與print不同的是,它提供了定制打印的能力.正式環(huán)境的時候?qū)ebugPrint函數(shù)定義為一個空函數(shù)體,就可以一鍵實現(xiàn)取消打印的功能了.
// 正式環(huán)境 將debugPrint指定為空的執(zhí)行體, 所以它什么也不做
debugPrint = (String message, {int wrapWidth}) {};
debugPrint('test');
//開發(fā)環(huán)境就需要打印出日志
debugPrint = (String message, {int wrapWidth}) =>
debugPrintSynchronously(message, wrapWidth: wrapWidth);
- 開啟Debug Painting,有點像原生的繪制布局邊界.
void main() {
//Debug Painting 界面調(diào)試工具
//有點像原生的顯示布局邊界
debugPaintSizeEnabled = true;
runApp(MyApp());
}
- 還可以使用Flutter Inspector去查看更詳細的可視化信息.
13. 常用命令行
| 階段 | 子任務(wù) | 命令 |
|---|---|---|
| 工程初始化 | App工程 | flutter create --template=app hello |
| 工程初始化 | Dart包工程 | flutter create --template=package hello |
| 工程初始化 | 插件工程 | flutter create --template=plugin hello |
| 構(gòu)建 | Debug構(gòu)建 | flutter build apk --debug </p> flutter build ios --debug |
| 構(gòu)建 | Release構(gòu)建 | flutter build apk --release </p> flutter build ios --release |
| 構(gòu)建 | Profile構(gòu)建 | flutter build apk --profile </p> flutter build ios --profile |
| 集成原生工程 | 獨立App打包 | flutter build apk --release </p> flutter build ios --release |
| 集成原生工程 | Pod/AAR打包 | flutter build apk --release </p> flutter build ios --release |