class _AnimateAppState extends State<AnimateApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
//创建动画周期为1秒的AnimationController对象
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
// 创建从50到200线性变化的Animation对象
animation = Tween(begin: 50.0, end: 200.0).animate(controller)
..addListener(() {
setState(() {}); //刷新界面
});
controller.forward(); //启动动画
}
...
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Center(
child: Container(
width: animation.value, // 将动画的值赋给widget的宽高
height: animation.value,
child: FlutterLogo()
)));
}
@override
void dispose() {
controller.dispose(); // 释放资源
super.dispose();
}
//创建动画周期为1秒的AnimationController对象
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
//创建一条震荡曲线
final CurvedAnimation curve = CurvedAnimation(
parent: controller, curve: Curves.elasticOut);
// 创建从50到200跟随振荡曲线变化的Animation对象
animation = Tween(begin: 50.0, end: 200.0).animate(curve)
//以下两段语句等价
//第一段
controller.repeat(reverse: true);//让动画重复执行
//第二段
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();//动画结束时反向执行
} else if (status == AnimationStatus.dismissed) {
controller.forward();//动画反向执行完毕时,重新执行
}
});
controller.forward();//启动动画
class AnimatedLogo extends AnimatedWidget {
//AnimatedWidget需要在初始化时传入animation对象
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
//取出动画对象
final Animation<double> animation = listenable;
return Center(
child: Container(
height: animation.value,//根据动画对象的当前状态更新宽高
width: animation.value,
child: FlutterLogo(),
));
}
}
MaterialApp(
home: Scaffold(
body: AnimatedLogo(animation: animation)//初始化AnimatedWidget时传入animation对象
));
MaterialApp(
home: Scaffold(
body: Center(
child: AnimatedBuilder(
animation: animation,//传入动画对象
child:FlutterLogo(),
//动画构建回调
builder: (context, child) => Container(
width: animation.value,//使用动画的当前状态更新UI
height: animation.value,
child: child, //child参数即FlutterLogo()
)
)
)
));
class Page1 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(//手势监听点击
child: Hero(
tag: 'hero',//设置共享tag
child: Container(
width: 100, height: 100,
child: FlutterLogo())),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_)=>Page2()));//点击后打开第二个页面
},
)
);
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Hero(
tag: 'hero',//设置共享tag
child: Container(
width: 300, height: 300,
child: FlutterLogo()
))
);
}
}
事实上,上图的Event Loop 示意图只是一个简化版。在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。
分别看一下这两个队列的特点和使用场景。
微任务队列。微任务顾名思义,表示一个短时间内就会完成的异步任务。从上面的流程图可以看到,微任务队列在事件循环中的优先级是最高的,只要队列中还有任务,就可以一直霸占着事件循环。
微任务是由 scheduleMicroTask 建立的。如下所示,这段代码会在下一个事件循环中输出一段字符串。
scheduleMicrotask(() => print('This is a microtask'));
Future(() => print('Running in Future 1'));//下一个事件循环输出字符串
Future(() => print(‘Running in Future 2'))
.then((_) => print('and then 1'))
.then((_) => print('and then 2’));//上一个事件循环结束后,连续输出三段字符串
//f1比f2先执行
Future(() => print('f1'));
Future(() => print('f2'));
//f3执行后会立刻同步执行then 3
Future(() => print('f3')).then((_) => print('then 3'));
//then 4会加入微任务队列,尽快执行
Future(() => null).then((_) => print('then 4'));
Future(() => print('f1'));//声明一个匿名Future
Future fx = Future(() => null);//声明Future fx,其执行体为null
//声明一个匿名Future,并注册了两个then。在第一个then回调里启动了一个微任务
Future(() => print('f2')).then((_) {
print('f3');
scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));
//声明了一个匿名Future,并注册了两个then。第一个then是一个Future
Future(() => print('f6'))
.then((_) => Future(() => print('f7')))
.then((_) => print('f8'));
//声明了一个匿名Future
Future(() => print('f9'));
//往执行体为null的fx注册了了一个then
fx.then((_) => print('f10'));
//启动一个微任务
scheduleMicrotask(() => print('f11'));
print('f12');
------------------------------------------------------------
打印结果:
f12 f11 f1 f10 f2 f3 f5 f4 f6 f9 f7 f8
//声明了一个延迟3秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:3), () => "Hello")
.then((x) => "$x 2019");
main() async{
print(await fetchContent());//等待Hello 2019的返回
}
Future(() => print('f1'))
.then((_) async => await Future(() => print('f2')))
.then((_) => print('f3'));
Future(() => print('f4'));
------------------------------------------------------
打印结果:f1 f4 f2 f3
//声明了一个延迟2秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:2), () => "Hello")
.then((x) => "$x 2019");
//异步函数会同步等待Hello 2019的返回,并打印
func() async => print(await fetchContent());
main() {
print("func before");
func();
//await fun();
print("func after");
}
-------------------------------------------------------------------------
打印结果:func before func after Hello 2019
doSth(msg) => print(msg);
main() {
Isolate.spawn(doSth, "Hi");
...
}
//在主 Isolate 里,我们创建了一个并发 Isolate,在函数入口传入了主 Isolate 的发送管道,然后等待并发 Isolate 的回传消息。在并发 Isolate 中,我们用这个管道给主 Isolate 发了一个 Hello 字符串。
Isolate isolate;
start() async {
ReceivePort receivePort= ReceivePort();//创建管道
//创建并发Isolate,并传入发送管道
isolate = await Isolate.spawn(getMsg, receivePort.sendPort);
//监听管道消息
receivePort.listen((data) {
print('Data:$data');
receivePort.close();//关闭管道
isolate?.kill(priority: Isolate.immediate);//杀死并发Isolate
isolate = null;
});
}
//并发Isolate往管道发送一个字符串
getMsg(sendPort) => sendPort.send("Hello");
//并发计算阶乘
Future<dynamic> asyncFactoriali(n) async{
final response = ReceivePort();//创建管道
//创建并发Isolate,并传入管道
await Isolate.spawn(_isolate,response.sendPort);
//等待Isolate回传管道
final sendPort = await response.first as SendPort;
//创建了另一个管道answer
final answer = ReceivePort();
//往Isolate回传的管道中发送参数,同时传入answer管道
sendPort.send([n,answer.sendPort]);
return answer.first;//等待Isolate通过answer管道回传执行结果
}
//Isolate函数体,参数是主Isolate传入的管道
_isolate(initialReplyTo) async {
final port = ReceivePort();//创建管道
initialReplyTo.send(port.sendPort);//往主Isolate回传管道
final message = await port.first as List;//等待主Isolate发送消息(参数和回传结果的管道)
final data = message[0] as int;//参数
final send = message[1] as SendPort;//回传结果的管道
send.send(syncFactorial(data));//调用同步计算阶乘的函数回传结果
}
//同步计算阶乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
main() async => print(await asyncFactoriali(4));//等待并发计算阶乘结果
//同步计算阶乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
//使用compute函数封装Isolate的创建和结果的返回
main() async => print(await compute(syncFactorial, 4));
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}');
}
}
dependencies:
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}");
}
}
dependencies:
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}");
}
}
需要注意的是,创建 URI、设置 Header 及发出请求的行为,都是通过 dio.get 方法实现的。这个方法的 options 参数提供了精细化控制网络请求的能力,可以支持设置 Header、超时时间、Cookie、请求方法等。
//使用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
});
//增加拦截器
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);
}
String jsonString = '''
{
"id":"123",
"name":"张三",
"score" : 95
}
''';
class Student{
//属性id,名字与成绩
String id;
String name;
int score;
//构造方法
Student({
this.id,
this.name,
this.score
});
//JSON解析工厂类,使用字典数据为对象初始化赋值
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
id: parsedJson['id'],
name : parsedJson['name'],
score : parsedJson ['score']
);
}
}
loadStudent() {
//jsonString为JSON文本
final jsonResponse = json.decode(jsonString);
Student student = Student.fromJson(jsonResponse);
print(student.name);
}
String jsonString = '''
{
"id":"123",
"name":"张三",
"score" : 95,
"teacher": {
"name": "李四",
"age" : 40
}
}
''';
class Teacher {
//Teacher的名字与年龄
String name;
int age;
//构造方法
Teacher({this.name,this.age});
//JSON解析工厂类,使用字典数据为对象初始化赋值
factory Teacher.fromJson(Map<String, dynamic> parsedJson){
return Teacher(
name : parsedJson['name'],
age : parsedJson ['age']
);
}
}
class Student{
...
//增加teacher属性
Teacher teacher;
//构造函数增加teacher
Student({
...
this.teacher
});
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
...
//增加映射规则
teacher: Teacher.fromJson(parsedJson ['teacher'])
);
}
}
final jsonResponse = json.decode(jsonString);//将字符串解码成Map对象
Student student = Student.fromJson(jsonResponse);//手动解析
print(student.teacher.name);
static Student parseStudent(String content) {
final jsonResponse = json.decode(content);
Student student = Student.fromJson(jsonResponse);
return student;
}
doSth() {
...
//用compute函数将json解析放到新Isolate
compute(parseStudent,jsonString).then((student)=>print(student.teacher.name));
}
//创建文件目录
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 "";
}
}
writeContent("Hello World!");
...
readContent().then((value)=>print(value));
//读取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);
}
//读出counter数据并打印
_loadCounter().then((value)=>print("before:$value"));
//递增counter数据后,再次读出并打印
_incrementCounter().then((_) {
_loadCounter().then((value)=>print("after:$value"));
});
class Student{
String id;
String name;
int score;
//构造方法
Student({this.id, this.name, this.score,});
//用于将JSON字典转换成类对象的工厂类方法
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
id: parsedJson['id'],
name : parsedJson['name'],
score : parsedJson ['score'],
);
}
}
class Student{
...
//将类对象转换成JSON字典,方便插入数据库
Map<String, dynamic> toJson() {
return {'id': id, 'name': name, 'score': score,};
}
}
var student1 = Student(id: '123', name: '张三', score: 90);
var student2 = Student(id: '456', name: '李四', score: 80);
var student3 = Student(id: '789', name: '王五', score: 85);
final Future<Database> database = openDatabase(
join(await getDatabasesPath(), 'students_database.db'),
onCreate: (db, version)=>db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER)"),
onUpgrade: (db, oldVersion, newVersion){
//dosth for migration
},
version: 1,
);
Future<void> insertStudent(Student std) async {
final Database db = await database;
await db.insert(
'students',
std.toJson(),
//插入冲突策略,新的替换旧的
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
//插入3个Student对象
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();
Flutter 作为一个跨平台框架,提供了一套标准化的解决方案,为开发者屏蔽了操作系统的差异。但,Flutter 毕竟不是操作系统,因此在某些特定场景下(比如推送、蓝牙、摄像头硬件调用时),也需要具备直接访问系统底层原生代码的能力。为此,Flutter 提供了一套灵活而轻量级的机制来实现 Dart 和原生代码之间的通信,即方法调用的消息传递机制,而方法通道则是用来传递通信消息的信道。
一次典型的方法调用过程类似网络调用,由作为客户端的 Flutter,通过方法通道向作为服务端的原生代码宿主发送方法调用请求,原生代码宿主在监听到方法调用的消息后,调用平台相关的 API 来处理 Flutter 发起的请求,最后将处理完毕的结果通过方法通道回发至 Flutter。调用过程如下图所示。
从上图中可以看到,方法调用请求的处理和响应,在 Android 中是通过 FlutterView,而在 iOS 中则是通过 FlutterViewController 进行注册的。FlutterView 与 FlutterViewController 为 Flutter 应用提供了一个画板,使得构建于 Skia 之上的 Flutter 通过绘制即可实现整个应用所需的视觉效果。因此,它们不仅是 Flutter 应用的容器,同时也是 Flutter 应用的入口,自然也是注册方法调用请求最合适的地方。
//声明MethodChannel
//注意:通道名称没要求,Android/IOS通道入口做判断即可
const platform = MethodChannel('samples.chenhang/utils');
//处理按钮点击
handleButtonClick() async{
int result;
//异常捕获
try {
//异步等待方法通道的调用结果
result = await platform.invokeMethod('openAppMarket');
}
catch (e) {
result = -1;
}
print("Result:$result");
}
protected void onCreate(Bundle savedInstanceState) {
...
//创建与调用方标识符一样的方法通道
new MethodChannel(getFlutterView(), "samples.chenhang/utils").setMethodCallHandler(
//设置方法处理回调
new MethodCallHandler() {
//响应方法请求
@Override
public void onMethodCall(MethodCall call, Result result) {
//判断方法名是否支持
if(call.method.equals("openAppMarket")) {
try {
//应用市场URI
Uri uri = Uri.parse("market://details?id=com.hangchen.example.flutter_module_page.host");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//打开应用市场
activity.startActivity(intent);
//返回处理结果
result.success(0);
} catch (Exception e) {
//打开应用市场出现异常
result.error("UNAVAILABLE", "没有安装应用市场", null);
}
}else {
//方法名暂不支持
result.notImplemented();
}
}
});
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//创建命名方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/utils" binaryMessenger:(FlutterViewController *)self.window.rootViewController];
//往方法通道注册方法调用处理回调
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
//方法名称一致
if ([@"openAppMarket" isEqualToString:call.method]) {
//打开App Store(本例打开微信的URL)
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/xy/app/foo/id414478124"]];
//返回方法处理结果
result(@0);
} else {
//找不到被调用的方法
result(FlutterMethodNotImplemented);
}
}];
...
}
//Flutter端
// 处理按钮点击
handleButtonClick() async {
int result;
// 异常捕获
try {
// 异步等待方法通道的调用结果
result = await platform.invokeMethod('openAppMarket', <String, dynamic>{
'appId': "com.xxx.xxx",
'packageName': "xxx.com.xxx",
});
} catch (e) {
result = -1;
}
print("Result:$result");
}
//Android端
if (call.method == "openAppMarket") {
if (call.hasArgument("appId")) {
//获取 appId
call.argument<String>("appId")
}
if (call.hasArgument("packageName")) {
//获取包名
call.argument<String>("packageName")
}
}
如果说方法通道解决的是原生能力逻辑复用问题,那么平台视图解决的就是原生视图复用问题。Flutter 提供了一种轻量级的方法,让我们可以创建原生(Android 和 iOS)的视图,通过一些简单的 Dart 层接口封装之后,就可以将它插入 Widget 树中,实现原生视图与 Flutter 视图的混用。
一次典型的平台视图使用过程与方法通道类似。
以一个具体的案例,将一个红色的原生视图内嵌到 Flutter 中,演示如何使用平台视图。这部分内容主要包括两部分。
class SampleView extends StatelessWidget {
@override
Widget build(BuildContext context) {
//使用Android平台的AndroidView,传入唯一标识符sampleView
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(viewType: 'sampleView');
} else {
//使用iOS平台的UIKitView,传入唯一标识符sampleView
return UiKitView(viewType: 'sampleView');
}
}
}
Android端的实现。
//视图工厂类
class SampleViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
//初始化方法
public SampleViewFactory(BinaryMessenger msger) {
super(StandardMessageCodec.INSTANCE);
messenger = msger;
}
//创建原生视图封装类,完成关联
@Override
public PlatformView create(Context context, int id, Object obj) {
return new SimpleViewControl(context, id, messenger);
}
}
//原生视图封装类
class SimpleViewControl implements PlatformView {
private final View view;//缓存原生视图
//初始化方法,提前创建好视图
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
view = new View(context);
view.setBackgroundColor(Color.rgb(255, 0, 0));
}
//返回原生视图
@Override
public View getView() {
return view;
}
//原生视图销毁回调
@Override
public void dispose() {
}
}
protected void onCreate(Bundle savedInstanceState) {
Registrar registrar = registrarFor("samples.chenhang/native_views");//生成注册类
SampleViewFactory playerViewFactory = new SampleViewFactory(registrar.messenger());//生成视图工厂
registrar.platformViewRegistry().registerViewFactory("sampleView", playerViewFactory);//注册视图工厂
}
iOS 端的实现。
//平台视图工厂
@interface SampleViewFactory : NSObject<FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messager;
@end
@implementation SampleViewFactory{
NSObject<FlutterBinaryMessenger>*_messenger;
}
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager{
self = [super init];
if (self) {
_messenger = messager;
}
return self;
}
-(NSObject<FlutterMessageCodec> *)createArgsCodec{
return [FlutterStandardMessageCodec sharedInstance];
}
//创建原生视图封装实例
-(NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args{
SampleViewControl *activity = [[SampleViewControl alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
return activity;
}
@end
//平台视图封装类
@interface SampleViewControl : NSObject<FlutterPlatformView>
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end
@implementation SampleViewControl{
UIView * _templcateView;
}
//创建原生视图
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
_templcateView = [[UIView alloc] init];
_templcateView.backgroundColor = [UIColor redColor];
}
return self;
}
-(UIView *)view{
return _templcateView;
}
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject<FlutterPluginRegistrar>* registrar = [self registrarForPlugin:@"samples.chenhang/native_views"];//生成注册类
SampleViewFactory* viewFactory = [[SampleViewFactory alloc] initWithMessenger:registrar.messenger];//生成视图工厂
[registrar registerViewFactory:viewFactory withId:@"sampleView"];//注册视图工厂
...
}
<dict>
...
<key>io.flutter.embedded_views_preview</key>
<true/>
....
</dict>
Scaffold(
backgroundColor: Colors.yellowAccent,
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
));
//原生视图控制器
class NativeViewController {
MethodChannel _channel;
//原生视图完成创建后,通过id生成唯一方法通道
onCreate(int id) {
_channel = MethodChannel('samples.chenhang/native_views_$id');
}
//调用原生视图方法,改变背景颜色
Future<void> changeBackgroundColor() async {
return _channel.invokeMethod('changeBackgroundColor');
}
}
//原生视图Flutter侧封装,继承自StatefulWidget
class SampleView extends StatefulWidget {
const SampleView({
Key key,
this.controller,
}) : super(key: key);
//持有视图控制器
final NativeViewController controller;
@override
State<StatefulWidget> createState() => _SampleViewState();
}
class _SampleViewState extends State<SampleView> {
//根据平台确定返回何种平台视图
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'sampleView',
//原生视图创建完成后,通过onPlatformViewCreated产生回调
onPlatformViewCreated: _onPlatformViewCreated,
);
} else {
return UiKitView(viewType: 'sampleView',
//原生视图创建完成后,通过onPlatformViewCreated产生回调
onPlatformViewCreated: _onPlatformViewCreated
);
}
}
//原生视图创建完成后,调用control的onCreate方法,传入view id
_onPlatformViewCreated(int id) {
if (widget.controller == null) {
return;
}
widget.controller.onCreate(id);
}
}
Android端接口实现代码
class SimpleViewControl implements PlatformView, MethodCallHandler {
private final MethodChannel methodChannel;
...
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
...
//用view id注册方法通道
methodChannel = new MethodChannel(messenger, "samples.chenhang/native_views_" + id);
//设置方法通道回调
methodChannel.setMethodCallHandler(this);
}
//处理方法调用消息
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
//如果方法名完全匹配
if (methodCall.method.equals("changeBackgroundColor")) {
//修改视图背景,返回成功
view.setBackgroundColor(Color.rgb(0, 0, 255));
result.success(0);
}else {
//调用方发起了一个不支持的API调用
result.notImplemented();
}
}
...
}
iOS 端接口实现代码
@implementation SampleViewControl{
...
FlutterMethodChannel* _channel;
}
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
...
//使用view id完成方法通道的创建
_channel = [FlutterMethodChannel methodChannelWithName:[NSString stringWithFormat:@"samples.chenhang/native_views_%lld", viewId] binaryMessenger:messenger];
//设置方法通道的处理回调
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
}
return self;
}
//响应方法调用消息
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
//如果方法名完全匹配
if ([[call method] isEqualToString:@"changeBackgroundColor"]) {
//修改视图背景色,返回成功
_templcateView.backgroundColor = [UIColor blueColor];
result(@0);
} else {
//调用方发起了一个不支持的API调用
result(FlutterMethodNotImplemented);
}
}
...
@end
class DefaultState extends State<DefaultPage> {
NativeViewController controller;
@override
void initState() {
controller = NativeViewController();//初始化原生View控制器
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
//内嵌原生View
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
),
//设置点击行为:改变视图颜色
floatingActionButton: FloatingActionButton(onPressed: ()=>controller.changeBackgroundColor())
);
}
}
use_frameworks!
platform :ios, '8.0'
target 'iOSDemo' do
#todo
end
Flutter create -t module flutter_library
这里的 Flutter 模块,也是 Flutter 工程,我们用 Android Studio 打开它,其目录如下图所示。
可以看到,和传统的 Flutter 工程相比,Flutter 模块工程也有内嵌的 Android 工程与 iOS 工程,因此我们可以像普通工程一样使用 Android Studio 进行开发调试。
仔细查看可以发现,Flutter 模块有一个细微的变化:Android 工程下多了一个 Flutter 目录,这个目录下的 build.gradle 配置就是我们构建 aar 的打包配置。这就是模块工程既能像 Flutter 传统工程一样使用 Android Studio 开发调试,又能打包构建 aar 与 pod 的秘密。
然后,打开 main.dart 文件,将其逻辑更新为以下代码逻辑,即一个写着“Hello from Flutter”的全屏红色的 Flutter Widget。
import 'package:flutter/material.dart';
import 'dart:ui';
void main() => runApp(_widgetForRoute(window.defaultRouteName));//独立运行传入默认路由
Widget _widgetForRoute(String route) {
switch (route) {
default:
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xFFD63031),//ARGB红色
body: Center(
child: Text(
'Hello from Flutter', //显示的文字
textDirection: TextDirection.ltr,
style: TextStyle(
fontSize: 20.0,
color: Colors.blue,
),
),
),
),
);
}
}
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/AppTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(MainActivity.this)
);
}
});
myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/my_route")
.build(MainActivity.this)
);
}
});
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);
// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}
myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(MainActivity.this)
);
}
});
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);
// Configure an initial route.
flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);
// Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}
Flutter build apk --debug
...
repositories {
flatDir {
dirs 'libs' // aar目录
}
}
android {
...
compileOptions {
sourceCompatibility 1.8 //Java 1.8
targetCompatibility 1.8 //Java 1.8
}
...
}
dependencies {
...
implementation(name: 'flutter-debug', ext: 'aar')//Flutter模块aar
...
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//import io.flutter.facade.Flutter;找不到???//TODO
View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); //传入路由标识符
setContentView(FlutterView);//用FlutterView替代Activity的ContentView
}
iOS模块集成
Flutter build ios --debug
Pod::Spec.new do |s|
s.name = 'FlutterEngine'
s.version = '0.1.0'
s.summary = 'XXXXXXX'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/xx/FlutterEngine'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'chenhang' => '[email protected]' }
s.source = { :git => "", :tag => "#{s.version}" }
s.ios.deployment_target = '8.0'
s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
end
...
target 'iOSDemo' do
pod 'FlutterEngine', :path => './'
end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
FlutterViewController *vc = [[FlutterViewController alloc]init];
[vc setInitialRoute:@"defaultRoute"]; //路由标识符
self.window.rootViewController = vc;
[self.window makeKeyAndVisible];
return YES;
}
//iOS 跳转至Flutter页面
FlutterViewController *vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:@"defaultPage"];//设置Flutter初始化路由页面
[self.navigationController pushViewController:vc animated:YES];//完成页面跳转
//Android 跳转至Flutter页面
//创建一个作为Flutter页面容器的Activity
public class FlutterHomeActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置Flutter初始化路由页面
//flutter类包找不到???
View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); //传入路由标识符
setContentView(FlutterView);//用FlutterView替代Activity的ContentView
}
}
//用FlutterPageActivity完成页面跳转
Intent intent = new Intent(MainActivity.this, FlutterHomeActivity.class);
startActivity(intent);
iOS端代码实现
@interface FlutterHomeViewController : FlutterViewController
@end
@implementation FlutterHomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
//声明方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/navigation" binaryMessenger:self];
//注册方法回调
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
//如果方法名为打开新页面
if([call.method isEqualToString:@"openNativePage"]) {
//初始化原生页面并打开
SomeOtherNativeViewController *vc = [[SomeOtherNativeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
result(@0);
}
//如果方法名为关闭Flutter页面
else if([call.method isEqualToString:@"closeFlutterPage"]) {
//关闭自身(FlutterHomeViewController)
[self.navigationController popViewControllerAnimated:YES];
result(@0);
}
else {
result(FlutterMethodNotImplemented);//其他方法未实现
}
}];
}
@end
Android端代码实现
//继承AppCompatActivity来作为Flutter的容器
public class FlutterHomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化Flutter容器
FlutterView flutterView = Flutter.createView(this, getLifecycle(), "defaultPage"); //传入路由标识符
//注册方法通道
new MethodChannel(flutterView, "samples.chenhang/navigation").setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
//如果方法名为打开新页面
if(call.method.equals("openNativePage")) {
//新建Intent,打开原生页面
Intent intent = new Intent(FlutterHomeActivity.this, SomeNativePageActivity.class);
startActivity(intent);
result.success(0);
}
//如果方法名为关闭Flutter页面
else if(call.method.equals("closeFlutterPage")) {
//销毁自身(Flutter容器)
finish();
result.success(0);
}
else {
//方法未实现
result.notImplemented();
}
}
});
//将flutterView替换成Activity的contentView
setContentView(flutterView);
}
}
void main() => runApp(_widgetForRoute(window.defaultRouteName));
//获取方法通道
const platform = MethodChannel('samples.chenhang/navigation');
//根据路由标识符返回应用入口视图
Widget _widgetForRoute(String route) {
switch (route) {
default://返回默认视图
return MaterialApp(home:DefaultPage());
}
}
class PageA extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
child: Text("Go PageB"),
onPressed: ()=>platform.invokeMethod('openNativePage')//打开原生页面
));
}
}
class DefaultPage extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("DefaultPage Page"),
leading: IconButton(icon:Icon(Icons.arrow_back), onPressed:() => platform.invokeMethod('closeFlutterPage')//关闭Flutter页面
)),
body: RaisedButton(
child: Text("Go PageA"),
onPressed: ()=>Navigator.push(context, MaterialPageRoute(builder: (context) => PageA())),//打开Flutter页面 PageA
));
}
}
dependencies:
flutter:
sdk: flutter
provider: 3.0.0+1 #provider依赖
//定义需要共享的数据模型,通过混入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组件封装数据资源
return ChangeNotifierProvider.value(
value: CounterModel(),//需要共享的数据资源
child: MaterialApp(
home: FirstPage(),
)
);
}
}
//第一个页面,负责读数据
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出资源
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
//展示资源中的数据
body: Text('Counter: ${_counter.counter}'),
//跳转到SecondPage
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()))
));
}
}
//第二个页面,负责读写数据
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出资源
final _counter = Provider.of<CounterModel>(context);
return Scaffold(
//展示资源中的数据
body: Text('Counter: ${_counter.counter}'),
//用资源更新方法来设置按钮点击回调
floatingActionButton:FloatingActionButton(
onPressed: _counter.increment,
child: Icon(Icons.add),
));
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
//使用Consumer来封装counter的读取
body: Consumer<CounterModel>(
//builder函数可以直接获取到counter参数
builder: (context, CounterModel counter, _) => Text('Value: ${counter.counter}')),
//使用Consumer来封装increment的读取
floatingActionButton: Consumer<CounterModel>(
//builder函数可以直接获取到increment参数
builder: (context, CounterModel counter, child) => FloatingActionButton(
onPressed: counter.increment,
child: child,
),
child: TestIcon(),
),
);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(providers: [
Provider.value(value: 30.0),//注入字体大小
ChangeNotifierProvider.value(value: CounterModel())//注入计数器实例
],
child: MaterialApp(
home: FirstPage(),
));
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(providers: [
Provider.value(value: 30.0),//注入字体大小
ChangeNotifierProvider.value(value: CounterModel())//注入计数器实例
],
child: MaterialApp(
home: FirstPage(),
));
}
}
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))
)
使用 Provider 可以实现 2 个同样类型的对象共享,应该如何实现吗?
答:可以封装一个大对象,将两个同样类型的对象封装为其内部属性。
在之前学习了如何在原生工程中的 Flutter 应用入口注册原生代码宿主回调,从而实现 Dart 层调用原生接口的方案。这种方案简单直接,适用于 Dart 层与原生接口之间交互代码量少、数据流动清晰的场景。
但对于推送这种涉及 Dart 与原生多方数据流转、代码量大的模块,这种与工程耦合的方案就不利于独立开发维护了。这时,我们需要使用 Flutter 提供的插件工程对其进行单独封装。
Flutter 的插件工程与普通的应用工程类似,都有 android 和 ios 目录,这也是我们完成平台相关逻辑代码的地方,而 Flutter 工程插件的注册,则仍会在应用的入口完成。除此之外,插件工程还内嵌了一个 example 工程,这是一个引用了插件代码的普通 Flutter 应用工程。我们通过 example 工程,可以直接调试插件功能。
在了解了整体工程的目录结构之后,接下来我们需要去 Dart 插件代码所在的 flutter_push_plugin.dart 文件,实现 Dart 层的推送接口封装。
//Flutter Push插件
class FlutterPushPlugin {
//单例
static final FlutterPushPlugin _instance = new FlutterPushPlugin.private(const MethodChannel('flutter_push_plugin'));
//方法通道
final MethodChannel _channel;
//消息回调
EventHandler _onOpenNotification;
//构造方法
FlutterPushPlugin.private(MethodChannel channel) : _channel = channel {
//注册原生反向回调方法,让原生代码宿主可以执行onOpenNotification方法
_channel.setMethodCallHandler(_handleMethod);
}
//初始化极光SDK
setupWithAppID(String appID) {
_channel.invokeMethod("setup", appID);
}
//注册消息通知
setOpenNotificationHandler(EventHandler onOpenNotification) {
_onOpenNotification = onOpenNotification;
}
//注册原生反向回调方法,让原生代码宿主可以执行onOpenNotification方法
Future<Null> _handleMethod(MethodCall call) {
switch (call.method) {
case "onOpenNotification":
return _onOpenNotification(call.arguments);
default:
throw new UnsupportedError("Unrecognized Event");
}
}
//获取地址id
Future<String> get registrationID async {
final String regID = await _channel.invokeMethod('getRegistrationID');
return regID;
}
}
dependencies {
implementation 'cn.jiguang.sdk:jpush:3.3.4'
implementation 'cn.jiguang.sdk:jcore:2.1.2'
}
public class FlutterPushPlugin implements MethodCallHandler {
//注册器,通常为MainActivity
public final Registrar registrar;
//方法通道
private final MethodChannel channel;
//插件实例
public static FlutterPushPlugin instance;
//注册插件
public static void registerWith(Registrar registrar) {
//注册方法通道
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_push_plugin");
instance = new FlutterPushPlugin(registrar, channel);
channel.setMethodCallHandler(instance);
//把初始化极光SDK提前至插件注册时
JPushInterface.setDebugMode(true);
JPushInterface.init(registrar.activity().getApplicationContext());
}
//私有构造方法
private FlutterPushPlugin(Registrar registrar, MethodChannel channel) {
this.registrar = registrar;
this.channel = channel;
}
//方法回调
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("setup")) {
//极光Android SDK的初始化工作需要在App工程中配置,因此不需要代码实现
result.success(0);
}
else if (call.method.equals("getRegistrationID")) {
//获取极光推送地址标识符
result.success(JPushInterface.getRegistrationID(registrar.context()));
} else {
result.notImplemented();
}
}
public void callbackNotificationOpened(NotificationMessage message) {
//将推送消息回调给Dart层
channel.invokeMethod("onOpenNotification",message.notificationContent);
}
}
Pod::Spec.new do |s|
...
s.dependency 'JPush', '3.2.2-noidfa'
end
@implementation FlutterPushPlugin
//注册插件
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
//注册方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"flutter_push_plugin" binaryMessenger:[registrar messenger]];
//初始化插件实例,绑定方法通道
FlutterPushPlugin* instance = [[FlutterPushPlugin alloc] init];
instance.channel = channel;
//为插件提供ApplicationDelegate回调方法
[registrar addApplicationDelegate:instance];
//注册方法通道回调函数
[registrar addMethodCallDelegate:instance channel:channel];
}
//处理方法调用
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if([@"setup" isEqualToString:call.method]) {
//极光SDK初始化方法
[JPUSHService setupWithOption:self.launchOptions appKey:call.arguments channel:@"App Store" apsForProduction:YES advertisingIdentifier:nil];
} else if ([@"getRegistrationID" isEqualToString:call.method]) {
//获取极光推送地址标识符
[JPUSHService registrationIDCompletionHandler:^(int resCode, NSString *registrationID) {
result(registrationID);
}];
} else {
//方法未实现
result(FlutterMethodNotImplemented);
}
}
//应用程序启动回调
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//初始化极光推送服务
JPUSHRegisterEntity * entity = [[JPUSHRegisterEntity alloc] init];
//设置推送权限
entity.types = JPAuthorizationOptionAlert|JPAuthorizationOptionBadge|JPAuthorizationOptionSound;
//请求推送服务
[JPUSHService registerForRemoteNotificationConfig:entity delegate:self];
//存储App启动状态,用于后续初始化调用
self.launchOptions = launchOptions;
return YES;
}
//推送token回调
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
///注册DeviceToken,换取极光推送地址标识符
[JPUSHService registerDeviceToken:deviceToken];
}
//推送被点击回调
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
//获取推送消息
NSDictionary * userInfo = response.notification.request.content.userInfo;
NSString *content = userInfo[@"aps"][@"alert"];
if ([content isKindOfClass:[NSDictionary class]]) {
content = userInfo[@"aps"][@"alert"][@"body"];
}
//延迟1秒通知Flutter,确保Flutter应用已完成初始化
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.channel invokeMethod:@"onOpenNotification" arguments:content];
});
//清除应用的小红点
UIApplication.sharedApplication.applicationIconBadgeNumber = 0;
//通知系统,推送回调处理完毕
completionHandler();
}
//前台应用收到了推送消息
- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger options))completionHandler {
//通知系统展示推送消息提示
completionHandler(UNNotificationPresentationOptionAlert);
}
@end
defaultConfig {
...
//ndk支持架构
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
}
manifestPlaceholders = [
JPUSH_PKGNAME : applicationId, //包名
JPUSH_APPKEY : "f861910af12a509b34e266c2", //JPush 上注册的包名对应的Appkey
JPUSH_CHANNEL : "developer-default", //填写默认值即可
]
}
iOS 的应用配置相对 Android 会繁琐一些,因为整个配置过程涉及应用、苹果 APNs 服务、极光三方之间的信息关联。
除了需要在应用内绑定极光信息之外(即 handleMethodCall 中的 setup 方法),还需要在苹果的开发者官网提前申请苹果的推送证书。关于申请证书,苹果提供了.p12 证书和 APNs Auth Key 两种鉴权方式。
这里,我推荐使用更为简单的 Auth Key 方式。申请推送证书的过程,极光官网提供了详细的注册步骤,这里我就不再赘述了。需要注意的是,申请 iOS 的推送证书时,你只能使用付费的苹果开发者账号。
在拿到了 APNs Auth Key 之后,我们同样需要去极光官网,根据 Bundle ID 进行推送设置,并把 Auth Key 上传至极光进行托管,由它完成与苹果的鉴权工作。
接下来,我们回到 Xcode 打开的 example 工程,进行最后的配置工作。
//获取推送插件单例
FlutterPushPlugin fpush = FlutterPushPlugin();
void main() {
//使用AppID注册极光推送服务(仅针对iOS平台)
fpush.setupWithAppID("f861910af12a509b34e266c2");
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
//极光推送地址regID
String _regID = 'Unknown';
//接收到的推送消息
String _notification = "";
@override
initState() {
super.initState();
//注册推送消息回调
fpush.setOpenNotificationHandler((String message) async {
//刷新界面状态,展示推送消息
setState(() {
_notification = message;
});
});
//获取推送地址regID
initPlatformState();
}
initPlatformState() async {
//调用插件封装的regID
String regID = await fpush.registrationID;
//刷新界面状态,展示regID
setState(() {
_regID = regID;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: <Widget>[
//展示regID,以及收到的消息
Text('Running on: $_regID\n'),
Text('Notification Received: $_notification')
],
),
),
),
);
}
}
需要注意的是,我们今天的实际工程演示是通过内嵌的 example 工程示例所完成的,如果你有一个独立的 Flutter 工程(比如Flutter_Push_Demo)需要接入 Flutter_Push_Plugin,其配置方式与 example 工程并无不同,唯一的区别是,需要在 pubspec.yaml 文件中将对插件的依赖显示地声明出来而已:
flutter_localizations:
sdk: flutter
MaterialApp(
//生成本地化值的集合;
localizationsDelegates: [
//Material组件库提供本地化的字符和其他值
GlobalMaterialLocalizations.delegate,
//定义Widget默认文本方向,即从左到右或从右到左
GlobalWidgetsLocalizations.delegate,
//为Cupertino库(iOS风格)提高本地化的字符串和其他值
GlobalCupertinoLocalizations.delegate,
],
//支持本地化区域的集合
supportedLocales: [
//Locale标识用户的语言环境
Locale('zh'),
Locale('en'),
],
)
Locale myLocale=Localizations.localeOf(context);
MaterialApp(
supportedLocales: [
Locale('zh'),
Locale('en'),
],
localeListResolutionCallback: (List locales,Iterable supportLocales){
print('$locales');
print('$supportLocales');
},
)
class SimpleLocalizations {
SimpleLocalizations(this._locale);
final Locale _locale;
static SimpleLocalizations of(BuildContext context) {
return Localizations.of<SimpleLocalizations>(context, SimpleLocalizations);
}
Map<String, Map<String, String>> _localizedValues = {
"zh": valuesZHCN,
"en": valuesEN
};
Map<String, String> get values {
if (_locale == null) {
return _localizedValues['zh'];
}
return _localizedValues[_locale.languageCode];
}
static const LocalizationsDelegate<SimpleLocalizations> delegate =
_SimpleLocalizationsDelegate();
static Future<SimpleLocalizations> load(Locale locale) {
return SynchronousFuture<SimpleLocalizations>(SimpleLocalizations(locale));
}
}
var valuesZHCN = {
LocalizationsKey.appName: "应用名称",
LocalizationsKey.title: "标题"
};
var valuesEN = {
LocalizationsKey.appName: "App Name",
LocalizationsKey.title: "Title"
};
class LocalizationsKey {
static const appName = "app_name";
static const title = "title";
}
class _SimpleLocalizationsDelegate
extends LocalizationsDelegate<SimpleLocalizations> {
const _SimpleLocalizationsDelegate();
//是否支持某个Locale,正常情况返回true。
@override
bool isSupported(Locale locale) => true;
//加载相应的Locale资源类
@override
Future<SimpleLocalizations> load(Locale locale) =>
SimpleLocalizations.load(locale);
//返回值决定当Localizations组件重新构建是,是否调用load方法重新加载Locale资源,
//通常情况不需要,返回false即可。
@override
bool shouldReload(LocalizationsDelegate<SimpleLocalizations> old) => false;
}
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
SimpleLocalizations.delegate,
],
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text(
"当前App_Name的国际化属性值:${ SimpleLocalizations.of(context).values[LocalizationsKey.appName]}"
)
)
);
}
dependencies:
intl: ^0.16.1
dev_dependencies:
flutter_test:
sdk: flutter
intl_translation: ^0.17.3
class IntlLocalizations {
IntlLocalizations();
static IntlLocalizations of(BuildContext context) {
return Localizations.of<IntlLocalizations>(context, IntlLocalizations);
}
String get appName {
return Intl.message('app_name');
}
static const LocalizationsDelegate<IntlLocalizations> delegate =_IntlLocalizationsDelegate();
static Future<IntlLocalizations> load(Locale locale) async{
final String localeName=Intl.canonicalizedLocale(locale.toString());
//代码会报错,在使用intl_trnaslation工具生成arb文件再转换成dart文件就不会了。
await initializeMessages(localeName);
Intl.defaultLocale=localeName;
return IntlLocalizations();
}
}
class _IntlLocalizationsDelegate
extends LocalizationsDelegate<IntlLocalizations> {
const _IntlLocalizationsDelegate();
@override
bool isSupported(Locale locale)=>true;
@override
Future<IntlLocalizations> load(Locale locale)=>IntlLocalizations.load(locale);
@override
bool shouldReload(LocalizationsDelegate<IntlLocalizations> old)=>false;
}
flutter pub run intl_translation:extract_to_arb --output-dir=lib/locations/intl_messages lib/locations/intl_messages/intl_localizations.dart
{
"@@last_modified": "2020-09-11T16:20:39.991382",
"app_name": "app_name",
"@app_name": {
"type": "text",
"placeholders": {}
}
}
{
"@@last_modified": "2020-09-11T16:20:39.991382",
"app_name": "app_name",
"@app_name": {
"type": "en_US",
"placeholders": {}
}
}
flutter pub run intl_translation:generate_from_arb --output-dir=lib/locations/intl_messages --no-use-deferred-loading lib/locations/intl_messages/intl_localizations.dart lib/locations/intl_messages/intl_*.arb
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
SimpleLocalizations.delegate,
IntlLocalizations.delegate
],
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text(
"当前App_Name的国际化属性值:${IntlLocalizations.of(context).appName}"
)
)
);
}
注:Flutter i8n已经停止维护。
dependencies:
flutter_localizations:
sdk: flutter
//应用程序入口
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: const [
S.delegate,//应用程序的翻译回调
GlobalMaterialLocalizations.delegate,//Material组件的翻译回调
GlobalWidgetsLocalizations.delegate,//普通Widget的翻译回调
],
supportedLocales: S.delegate.supportedLocales,//支持语系
//title的国际化回调
onGenerateTitle: (context){
return S.of(context).app_title;
},
home: MyHomePage(),
);
}
}
Widget build(BuildContext context) {
return Scaffold(
//获取appBar title的翻译文案
appBar: AppBar(
title: Text(S.of(context).main_title),
),
body: Center(
//传入_counter参数,获取计数器动态文案
child: Text(
S.of(context).message_tip(_counter.toString())
)
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,//点击回调
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
<manifest ... >
...
<!-- 设置应用名称 -->
<application
...
android:label="@string/title"
...
>
</application>
</manifest>
<!--英文(默认)字符串资源-->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title">Computer</string>
</resources>
<!--中文字符串资源-->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title">计数器</string>
</resources>
//英文版
"CFBundleName" = "Computer";
//中文版
"CFBundleName" = "计数器";
@override
Widget build(BuildContext context) {
return Scaffold(
//使用OrientationBuilder的builder模式感知屏幕旋转
body: OrientationBuilder(
builder: (context, orientation) {
//根据屏幕旋转方向返回不同布局行为
return orientation == Orientation.portrait
? _buildVerticalLayout()
: _buildHorizontalLayout();
},
),
);
}
if(MediaQuery.of(context).orientation == Orientation.portrait) {
//dosth
}
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
//列表Widget
class ListWidget extends StatefulWidget {
final ItemSelectedCallback onItemSelected;
ListWidget(
this.onItemSelected,//列表被点击的回调函数
);
@override
_ListWidgetState createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
//创建一个20项元素的列表
return ListView.builder(
itemCount: 20,
itemBuilder: (context, position) {
return ListTile(
title: Text(position.toString()),//标题为index
onTap:()=>widget.onItemSelected(position),//点击后回调函数
);
},
);
}
}
//详情Widget
class DetailWidget extends StatefulWidget {
final int data; //新闻列表被点击元素索引
DetailWidget(this.data);
@override
_DetailWidgetState createState() => _DetailWidgetState();
}
class _DetailWidgetState extends State<DetailWidget> {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,//容器背景色
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.data.toString()),//居中展示列表被点击元素索引
],
),
),
);
}
}
if(MediaQuery.of(context).size.width > 480) {
//tablet
} else {
//phone
}
class _MasterDetailPageState extends State<MasterDetailPage> {
var selectedValue = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: OrientationBuilder(builder: (context, orientation) {
//平板或横屏手机,页面内嵌列表ListWidget与详情DetailWidget
if (MediaQuery.of(context).size.width > 480) {
return Row(children: <Widget>[
Expanded(
child: ListWidget((value) {//在列表点击回调方法中刷新右侧详情页
setState(() {selectedValue = value;});
}),
),
Expanded(child: DetailWidget(selectedValue)),
]);
} else {//普通手机,页面内嵌列表ListWidget
return ListWidget((value) {//在列表点击回调方法中打开详情页DetailWidget
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return Scaffold(
body: DetailWidget(value),
);
},
));
});
}
}),
);
}
}
assert(() {
//Do sth for debug
return true;
}());
if(kReleaseMode){
//Do sth for release
} else {
//Do sth for debug
}
class AppConfig extends InheritedWidget {
AppConfig({
@required this.appName,
@required this.apiBaseUrl,
@required Widget child,
}) : super(child: child);
final String appName;//主页标题
final String apiBaseUrl;//接口域名
//方便其子Widget在Widget树中找到它
static AppConfig of(BuildContext context) {
return context.inheritFromWidgetOfExactType(AppConfig);
}
//判断是否需要子Widget更新。由于是应用入口,无需更新
@override
bool updateShouldNotify(InheritedWidget oldWidget) => 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('API host: ${config.apiBaseUrl}'),//接口域名
),
);
}
}
//运行开发环境应用程序
flutter run -t lib/main_dev.dart
//运行生产环境应用程序
flutter run -t lib/main.dart
在 Android Studio 上为应用程序创建不同的启动配置,则可以通过 Flutter 插件为 main_dev.dart 增加启动入口。
首先,点击工具栏上的 Config Selector,选择 Edit Configurations 进入编辑应用程序启动选项。
接下来,我们就可以在 Config Selector 中切换不同的启动入口,从而直接在 Android Studio 中注入不同的配置环境了。
而如果我们想要打包构建出适用于 Android 的 APK,或是 iOS 的 IPA 安装包,则可以在 flutter build 命令后面,同样追加–target 或 -t 参数,指定应用程序初始化入口。
//打包开发环境应用程序
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
debugPrint = (String message, {int wrapWidth}) {};//空实现
//main.dart
void main() {
// 将debugPrint指定为空的执行体, 所以它什么也不做
debugPrint = (String message, {int wrapWidth}) {};
runApp(MyApp());
}
//main-dev.dart
void main() async {
// 将debugPrint指定为同步打印数据
debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronously(message, wrapWidth: wrapWidth);
runApp(MyApp());
}
Debug 视图模式划分为 4 个区域,即 A 区控制调试工具、B 区步进调试工具、C 区帧调试窗口、D 区变量查看窗口。
C 区用来指示当前断点所包含的函数执行堆栈,D 区则是其堆栈中的函数帧所对应的变量。
我们的断点是在 _MyHomePageState 类中的 build 方法设置的,因此 D 区显示的也是 build 方法上下文所包含的变量信息(比如 _counter、_widget、this、_element 等)。如果我们想切换到 _MyHomePageState 的 build 方法执行堆栈中的其他函数(比如 StatefulElement.build),查看相关上下文的变量信息时,只需要在 C 区中点击对应的方法名即可。
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = true; //打开Debug Painting调试开关
runApp(new MyApp());
}
辅助线提供了基本的 Widget 可视化能力。通过辅助线,我们能够感知界面中是否存在对齐或边距的问题,但却没有办法获取到布局信息,比如 Widget 距离父视图的边距信息、Widget 宽高尺寸信息等。
如果我们想要获取到 Widget 的可视化信息(比如布局信息、渲染信息等)去解决渲染问题,就需要使用更强大的 Flutter Inspector 了。Flutter Inspector 对控件布局详细数据提供了一种强大的可视化手段,来帮助我们诊断布局问题。
为了使用 Flutter Inspector,我们需要回到 Android Studio,通过工具栏上的“Open DevTools”按钮启动 Flutter Inspector。
随后,Android Studio 会打开浏览器,将计数器示例中的 Widget 树结构展示在面板中。可以看到,Flutter Inspector 所展示的 Widget 树结构,与代码中实现的 Widget 层次是一一对应的。
我们的 App 运行在 iPhone X 之上,其分辨率为 375*812。接下来,我们以 Column 组件的布局信息为例,通过确认其水平方向为居中布局、垂直方向为充满父 Widget 剩余空间的过程,来说明 Flutter Inspector 的具体用法。
为了确认 Column 在垂直方向是充满其父 Widget 剩余空间的,我们首先需要确定其父 Widget 在垂直方向上的另一个子 Widget,即 AppBar 的信息。我们点击 Flutter Inspector 面板左侧中的 AppBar 控件,右侧对应显示了它的具体视觉信息。
可以看到 AppBar 控件距离左边距为 0,上边距也为 0;宽为 375,高为 100。
CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),//动态模糊导航栏
child: ListView.builder(
itemCount: 100,
//为列表创建100个不同颜色的RowItem
itemBuilder: (context, index)=>TabRowItem(
index: index,
lastItem: index == 100 - 1,
color: colorItems[index],//设置不同的颜色
colorName: colorNameItems[index],
)
)
);
Scaffold(
//使用普通的白色AppBar
appBar: AppBar(title: Text('Home', style: TextStyle(color:Colors.black),),backgroundColor: Colors.white),
body: ListView.builder(
itemCount: 100,
//为列表创建100个不同颜色的RowItem
itemBuilder: (context, index)=>TabRowItem(
index: index,
lastItem: index == 100 - 1,
color: colorItems[index],//设置不同的颜色
colorName: colorNameItems[index],
)
),
);
RepaintBoundary(//设置静态缓存图像
child: Center(
child: Container(
color: Colors.black,
height: 10.0,
width: 10.0,
),
));
class MyHomePage extends StatelessWidget {
MyHomePage({Key key}) : super(key: key);
String generateMd5(String data) {
//MD5固定算法
var content = new Utf8Encoder().convert(data);
var digest = md5.convert(content);
return hex.encode(digest.bytes);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('demo')),
body: ListView.builder(
itemCount: 30,// 列表元素个数
itemBuilder: (context, index) {
//反复迭代计算MD5
String str = '1234567890abcdefghijklmnopqrstuvwxyz';
for(int i = 0;i<10000;i++) {
str = generateMd5(str);
}
return ListTile(title: Text("Index : $index"), subtitle: Text(str));
}// 列表项创建方法
),
);
}
}
dev_dependencies:
test:
备注:test 包的声明需要在 dev_dependencies 下完成,在这个标签下面定义的包只会在开发模式生效。
import 'package:test/test.dart';
import 'package:flutter_app/main.dart';
void main() {
//第一个用例,判断Counter对象调用increase方法后是否等于1
test('Increase a counter value should be 1', () {
final counter = Counter();
counter.increase();
expect(counter.value, 1);
});
//第二个用例,判断1+1是否等于2
test('1+1 should be 2', () {
expect(1+1, 2);
});
}
import 'package:test/test.dart';
import 'package:counter_app/counter.dart';
void main() {
//组合测试用例,判断Counter对象调用increase方法后是否等于1,并且判断Counter对象调用decrease方法后是否等于-1
group('Counter', () {
test('Increase a counter value should be 1', () {
final counter = Counter();
counter.increase();
expect(counter.value, 1);
});
test('Decrease a counter value should be -1', () {
final counter = Counter();
counter.decrease();
expect(counter.value, -1);
});
});
}
import 'package:http/http.dart' as http;
class Todo {
final String title;
Todo({this.title});
//工厂类构造方法,将JSON转换为对象
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
title: json['title'],
);
}
}
Future<Todo> fetchTodo(http.Client client) async {
final response =
await client.get('https://xxx.com/todos/1');
if (response.statusCode == 200) {
//请求成功,解析JSON
return Todo.fromJson(json.decode(response.body));
} else {
//请求失败,抛出异常
throw Exception('Failed to load post');
}
}
dev_dependencies:
test:
mockito:
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
class MockClient extends Mock implements http.Client {}
void main() {
group('fetchTodo', () {
test('returns a Todo if successful', () async {
final client = MockClient();
//使用Mockito注入请求成功的JSON字段
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
//验证请求结果是否为Todo实例
expect(await fetchTodo(client), isInstanceOf<Todo>());
});
test('throws an exception if error', () {
final client = MockClient();
//使用Mockito注入请求失败的Error
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('Forbidden', 403));
//验证请求结果是否抛出异常
expect(fetchTodo(client), throwsException);
});
});
}
dev_dependencies:
flutter_test:
sdk: flutter
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_app_demox/main.dart';
void main() {
testWidgets('Counter increments UI test', (WidgetTester tester) async {
//声明所需要验证的Widget对象(即MyApp),并触发其渲染
await tester.pumpWidget(MyApp());
//查找字符串文本为'0'的Widget,验证查找成功
expect(find.text('0'), findsOneWidget);
//查找字符串文本为'1'的Widget,验证查找失败
expect(find.text('1'), findsNothing);
//查找'+'按钮,施加点击行为
await tester.tap(find.byIcon(Icons.add));
//触发其渲染
await tester.pump();
//查找字符串文本为'0'的Widget,验证查找失败
expect(find.text('0'), findsNothing);
//查找字符串文本为'1'的Widget,验证查找成功
expect(find.text('1'), findsOneWidget);
});
}
Future<bool>updateSP(SharedPreferences prefs, int counter) async {
bool result = await prefs.setInt('counter', counter);
return result;
}
Future<int>increaseSPCounter(SharedPreferences prefs) async {
int counter = (prefs.getInt('counter') ?? 0) + 1;
await updateSP(prefs, counter);
return counter;
}
Future fetchTodo(http.Client client) async {
final response =
await client.get(‘https://xxx.com/todos/1’);
if (response.statusCode == 200) {
//请求成功,解析JSON
return Todo.fromJson(json.decode(response.body));
} else {
//请求失败,抛出异常
throw Exception(‘Failed to load post’);
}
}
* 考虑到这些外部依赖并不是我们的程序所能控制的,因此很难覆盖所有可能的成功或失败方案。比如,对于一个正常运行的 Web 服务来说,我们基本不可能测试出 fetchTodo 这个接口是如何应对 403 或 502 状态码的。因此,更好的一个办法是,在测试用例中“模拟”这些外部依赖(对应本例即为 http.client),让这些外部依赖可以返回特定结果。
* 在单元测试用例中模拟外部依赖,我们需要在 pubspec.yaml 文件中使用 mockito 包,以接口实现的方式定义外部依赖的接口。
```java
dev_dependencies:
test:
mockito:
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
class MockClient extends Mock implements http.Client {}
void main() {
group('fetchTodo', () {
test('returns a Todo if successful', () async {
final client = MockClient();
//使用Mockito注入请求成功的JSON字段
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('{"title": "Test"}', 200));
//验证请求结果是否为Todo实例
expect(await fetchTodo(client), isInstanceOf<Todo>());
});
test('throws an exception if error', () {
final client = MockClient();
//使用Mockito注入请求失败的Error
when(client.get('https://xxx.com/todos/1'))
.thenAnswer((_) async => http.Response('Forbidden', 403));
//验证请求结果是否抛出异常
expect(fetchTodo(client), throwsException);
});
});
}
dev_dependencies:
flutter_test:
sdk: flutter
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_app_demox/main.dart';
void main() {
testWidgets('Counter increments UI test', (WidgetTester tester) async {
//声明所需要验证的Widget对象(即MyApp),并触发其渲染
await tester.pumpWidget(MyApp());
//查找字符串文本为'0'的Widget,验证查找成功
expect(find.text('0'), findsOneWidget);
//查找字符串文本为'1'的Widget,验证查找失败
expect(find.text('1'), findsNothing);
//查找'+'按钮,施加点击行为
await tester.tap(find.byIcon(Icons.add));
//触发其渲染
await tester.pump();
//查找字符串文本为'0'的Widget,验证查找失败
expect(find.text('0'), findsNothing);
//查找字符串文本为'1'的Widget,验证查找成功
expect(find.text('1'), findsOneWidget);
});
}
Future<bool>updateSP(SharedPreferences prefs, int counter) async {
bool result = await prefs.setInt('counter', counter);
return result;
}
Future<int>increaseSPCounter(SharedPreferences prefs) async {
int counter = (prefs.getInt('counter') ?? 0) + 1;
await updateSP(prefs, counter);
return counter;
}