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 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 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 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 get _localFile async {
final directory = await getApplicationDocumentsDirectory();
final path = directory.path;
return File('$path/content.txt');
}
//将字符串写入文件
Future writeContent(String content) async {
final file = await _localFile;
return file.writeAsString(content);
}
//从文件读出字符串
Future 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_loadCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0);
return counter;
}
//递增写入SharedPreferences中key为counter的值
Future_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 = 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 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> students() async {
final Database db = await database;
final List
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 {
//声明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(context);
//获取字体大小
final textSize = Provider.of(context);
/*
*
//使用Consumer2获取两个数据资源
Consumer2(
//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(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(
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 |