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()
))
);
}
}
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();
//声明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,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
- 然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
- 最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。
- 至此,我们就可以像使用 Widget 那样,使用原生视图了。整个流程,如下图所示。
- 作为调用发起方的 Flutter,如何实现原生视图的接口调用?
- 如何在原生(Android 和 iOS)系统实现接口?
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
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' => 'hangisnice@gmail.com' }
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);
* 打开原生页面 openNativePage,与关闭 Flutter 页面 closeFlutterPage,在 Android 和 iOS 平台上分别如何实现。 * 注册方法通道最合适的地方,是 Flutter 应用的入口,即在 FlutterViewController(iOS 端)和 Activity 中的 FlutterView(Android 端)这两个容器内部初始化 Flutter 页面前。为了将 Flutter 相关的行为封装到容器内部,我们需要分别继承 FlutterViewController 和 Activity,在其 viewDidLoad 和 onCreate 初始化容器时,注册 openNativePage 和 closeFlutterPage 这两个方法。
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))
)
* 这些第三方推送服务厂商提供的能力和接入流程大都一致,考虑到极光的社区和生态相对活跃,以极光推送为例,在Flutter应用中引用原生推送的能力。 ##### 原生推送接入流程 * 要想在 Flutter 中接收推送消息,我们需要把原生的推送能力暴露给 Flutter 应用,即在原生代码宿主实现推送能力(极光 SDK)的接入,并通过方法通道提供给 Dart 层感知推送消息的机制。 * 集成极光推送插件,可参考我的这篇博客
【Flutter 第三方SDK集成(友盟统计,极光推送,百度地图)】
//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", //填写默认值即可
]
}
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<Locale> locales,Iterable<Locale> 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),
),
);
}
* 完成 iOS 的工程配置后,我们回到 Flutter 工程,选择 iOS 手机运行程序。可以看到,计数器的 iOS 版本也可以正确地支持国际化了。 ##### 原生工程配置 * 上面介绍的国际化方案,其实都是在 Flutter 应用内实现的。而在 Flutter 框架运行之前,我们是无法访问这些国际化文案的。 * Flutter 需要原生环境才能运行,但有些文案,比如应用的名称,我们需要在 Flutter 框架运行之前就为它提供多个语言版本(比如英文版本为 computer,中文版本为计数器),这时就需要在对应的原生工程中完成相应的国际化配置了。 ###### Android 工程下进行应用名称的配置 * 首先,在 Android 工程中,应用名称是在 AndroidManifest.xml 文件中 application 的 android:label 属性声明的,所以我们需要将其修改为字符串资源中的一个引用,让其能够根据语言地区自动选择合适的文案。
<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>
* 由于应用名称默认是不可配置的,所以工程并没有提供英文或者中文的可配置项,这些都需要通过新建与字符串引用对应的资源文件去搞定的。 * 我们右键单击 Runner 文件夹,然后选择 New File 来添加名为 InfoPlist.strings 的字符串资源文件,并在工程面板的最右侧文件检查器中的 Localization 选项中,添加英文和中文两种语言。InfoPlist.strings 的英文版和中文版内容如下所示。
//英文版
"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 元素索引; * 而新闻详情,则用于展示新闻列表中被点击的元素索引。 * 对于手机来说,由于空间小,所以新闻列表区块和新闻详情区块都是独立的页面,可以通过点击新闻元素进行新闻详情页面的切换;而对于平板电脑(和手机横屏布局)来说,由于空间足够大,所以我们把这两个区块放置在同一个页面,可以通过点击新闻元素去刷新同一页面的新闻详情。 * 页面的实现和区块的实现是互相独立的,通过区块复用就可以减少编写两个独立布局的工作。
//列表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
* 接下来,我们就可以在 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
* AOT(Ahead Of Time),指的是提前编译或运行前编译,在 Release 模式中使用,可以为特定的平台生成稳定的二进制代码,执行性能好、运行速度快,但每次执行均需提前编译,开发调试效率低。
* 总体来说,热重载的流程可以分为扫描工程改动、增量编译、推送更新、代码合并、Widget 重建 5 个步骤。 1、工程改动。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的 Dart 代码。 2、增量编译。热重载模块会将发生变化的 Dart 代码,通过编译转化为增量的 Dart Kernel 文件。 3、推送更新。热重载模块将增量的 Dart Kernel 文件通过 HTTP 端口,发送给正在移动设备上运行的 Dart VM。 4、代码合并。Dart VM 会将收到的增量 Dart Kernel 文件,与原有的 Dart Kernel 文件进行合并,然后重新加载新的 Dart Kernel 文件。 5、Widget 重建。在确认 Dart VM 资源加载成功后,Flutter 会将其 UI 线程重置,通知 Flutter Framework 重建 Widget。 * 可以看到,Flutter 提供的热重载在收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,这就大大节省了调试复杂交互界面的时间。 ##### 不支持热重载的情景 * Flutter 提供的亚秒级热重载一直是开发者的调试利器。通过热重载,我们可以快速修改 UI、修复 Bug,无需重启应用即可看到改动效果,从而大大提升了 UI 调试效率。 * 不过,Flutter 的热重载也有一定的局限性。因为涉及到状态保存与恢复,所以并不是所有的代码改动都可以通过热重载来更新。 ###### 典型场景 * . 代码编译出现错误。 * 当代码更改导致编译错误时,热重载会提示编译错误信息。在这种情况下,只需更正上述代码中的错误,就可以继续使用热重载。 * Widget状态无法兼容。 * 当代码更改会影响 Widget 的状态时,会使得热重载前后 Widget 所使用的数据不一致,即应用程序保留的状态与新的更改不兼容。这时,热重载也是无法使用的。比如将某个类的定义从 StatelessWidget 改为 StatefulWidget 时,热重载就会直接报错,当遇到这种情况时,我们需要重启应用,才能看到更新后的程序。 * 全局变量和静态属性的修改。 * 在 Flutter 中,全局变量和静态属性都被视为状态,在第一次运行应用程序时,会将它们的值设为初始化语句的执行结果,因此在热重载期间不会重新初始化。如果我们需要更改全局变量和静态属性的初始化语句,重启应用才能查看更改效果。 * main方法里面的修改。 * 在 Flutter 中,由于热重载之后只会根据原来的根节点重新创建控件树,因此 main 函数的任何改动并不会在热重载后重新执行。所以,如果我们改动了 main 函数体内的代码,是无法通过热重载看到更新效果的。 * initState方法里面的更改。 * 在热重载时,Flutter 会保存 Widget 的状态,然后重建 Widget。而 initState 方法是 Widget 状态的初始化方法,这个方法里的更改会与状态保存发生冲突,因此热重载后不会产生效果。我们需要重启应用,才能看到更改效果。 * 枚举和泛类型更改。 * 在 Flutter 中,枚举和泛型也被视为状态,因此对它们的修改也不支持热重载。
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());
}
* B 区的按钮,主要用来控制断点的步进情况。
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = true; //打开Debug Painting调试开关
runApp(new MyApp());
}
* 通过上面的数据我们可以得出: * Column 的右边距 = 父 Widget 宽度(即 iPhone X 宽度 375)-Column 左边距(即 38.5)- Column 宽(即 298)=38.5,即左右边距相等,因此 Column 是水平方向居中的; * Column 的高度 = 父 Widget 的高度(即 iPhone X 高度 812)- AppBar 上边距(即 0)- AppBar 高度(即 100) - Column 上边距(即 0)= 712.0,即 Column 在垂直方向上完全填满了父 Widget 除去 AppBar 之后的剩余空间。 * 因此,Column 的布局行为是完全符合预期的。 * 写代码不可避免会出现 Bug,出现时就需要 Debug(调试)。调试代码本质上就是一个不断收敛问题发生范围的过程,因此排查问题的一个最基本思路,就是二分法。 * 所谓二分调试法,是指通过某种稳定复现的特征(比如 Crash、某个变量的值、是否出现某个现象等任何明显的迹象),加上一个能把问题出现的范围划分为两半的手段(比如断点、assert、日志等),两者结合反复迭代不断将问题可能出现的范围一分为二(比如能判断出引发问题的代码出现在断点之前等)。通过二分法,我们可以快速缩小问题范围,这样一来调试的效率也就上去了。
* 为了保持 60Hz 的刷新频率,GPU 线程与 UI 线程中执行每一帧耗费的时间都应该小于 16ms(1/60 秒)。在这其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。下图演示了应用出现渲染和绘制耗时的情况下,性能图层的展示样式。
* 如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。 ##### GPU问题定位 * GPU 问题主要集中在底层渲染耗时上。有时候 Widget 树虽然构造起来容易,但在 GPU 线程下的渲染却很耗时。涉及 Widget 裁剪、蒙层这类多视图叠加渲染,或是由于缺少缓存导致静态图像的反复绘制,都会明显拖慢 GPU 的渲染速度。 * 我们可以使用性能图层提供的两项参数,即检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers,和检查缓存的图像开关 checkerboardRasterCacheImages,来检查这两种情况。 ###### checkerboardOffScreenLayers * 多视图叠加通常会用到 Canvas 里的 savaLayer 方法,这个方法在实现一些特定的效果(比如半透明)时非常有用,但由于其底层实现会在 GPU 渲染上涉及多图层的反复绘制,因此会带来较大的性能问题。 * 对于 saveLayer 方法使用情况的检查,我们只要在 MaterialApp 的初始化方法中,将 checkerboardOffscreenLayers 开关设置为 true,分析工具就会自动帮我们检测多视图叠加的情况了:使用了 saveLayer 的 Widget 会自动显示为棋盘格式,并随着页面刷新而闪烁。 * 不过,saveLayer 是一个较为底层的绘制方法,因此我们一般不会直接使用它,而是会通过一些功能性 Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。所以一旦遇到这种情况,我们需要思考一下是否一定要这么做,能不能通过其他方式来实现呢。 * 下面的例子中,我们使用 CupertinoPageScaffold 与 CupertinoNavigationBar 实现了一个动态模糊的效果。
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 实现同样的产品功能,来解决这个性能问题。
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<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;
}