|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: const Text('倒计时'), ), body: const MyHomePage(), )); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { late Duration time; var seconds = 0; Timer? countdownTimer; @override Widget build(BuildContext context) { return Center( child: Column( children: <Widget>[ ElevatedButton( child: const Text('定时'), onPressed: () { showCupertinoModalPopup<void>( context: context, builder: (BuildContext context) { return Container( height: 200, color: CupertinoColors.white, child: DefaultTextStyle( style: const TextStyle( color: CupertinoColors.black, fontSize: 22.0, ), child: CupertinoTimerPicker( //initialTimerDuration: time, //minuteInterval: 5, mode: CupertinoTimerPickerMode.ms, onTimerDurationChanged: (Duration newTimer) { setState(() { time = newTimer; seconds = time.inSeconds; // flag = true; }); }, ), )); }, ); }, ), ElevatedButton( child: const Text('开始倒计时'), onPressed: () { if (countdownTimer != null) { return; } countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { if (seconds > 0) { seconds--; } else { countdownTimer?.cancel(); countdownTimer = null; } }); }); }, ), Text( '倒计时: $seconds 秒', style: const TextStyle(fontSize: 30), ), ], ), ); } @override void dispose() { countdownTimer?.cancel(); countdownTimer = null; super.dispose(); } } |
Flutter : LateInitializationError: Field ‘data’ has not been initialized
You don’t want a late variable, you want a nullable one. If you need to check if something is initialized, you should be using a nullable variable instead and your code is already set up to check for null
just change
|
1 |
late MyData data; |
to
|
1 |
MyData? data; |
E.g.
change to
参考链接
Flutter : LateInitializationError: Field ‘data’ has not been initialized, got error
Flutter画圆/虚线圆/渐变圆/进度圆
同一颜色的圆
例子样式:
废话不多说 直接上代码,注释清楚!
1.调用该方法 如果只要一个圆,赋值completeColor, completeWidth, completePercent就可以,剩下的不用赋值 就是一个圆
2.如果需要俩圆的话直接调用赋值就行
3.如果只需要一个虚线圆的话,赋值isDividerRound = true; ,lineColor, width, 剩下的不用写,completeWidth 不得大于0
这是一个全圆就是比例是百分之百的圆,可以自行修改
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
/* * 如果只有一个圆的情况下,请设置已完成的圆 默认的圆的颜色不要设置 */ import 'dart:math'; import 'package:flutter/cupertino.dart'; class MyPainter extends CustomPainter { //默认的线的背景颜色 Color? lineColor; //默认的线的宽度 double width; //已完成线的颜色 Color completeColor; //已完成的百分比 double completePercent; //已完成的线的宽度 double completeWidth; // 从哪开始 1从下开始, 2 从上开始 3 从左开始 4 从右开始 默认从下开始 double startType; //是不是虚线的圈 bool isDividerRound; //中间的实圆 统计线条是不是渐变的圆 bool isGradient; //结束的位置 double endAngle; //默认的线的背景颜色 List<Color> lineColors; //实心圆阴影颜色 // Color shadowColor; //渐变圆 深色在下面 还是在左面 默认在下面 bool isTransform; MyPainter({ required this.lineColor, required this.completeColor, required this.completePercent, required this.width, required this.completeWidth, this.startType = 1, this.isDividerRound = false, this.isGradient = false, this.endAngle = pi * 2, required this.lineColors, this.isTransform = false, // this.shadowColor, }); @override void paint(Canvas canvas, Size size) { Offset center = Offset(size.width / 2, size.height / 2); // 坐标中心 double radius = min(size.width / 2, size.height / 2); // 半径 //是否有第二层圆 if (lineColor != null) { //是不是 虚线圆 if (isDividerRound) { //背景的线 Paint line = Paint() ..color = lineColor! // ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..isAntiAlias = true //抗锯齿 ..strokeWidth = width; double i = 0.00; while (i < pi * 2) { canvas.drawArc(Rect.fromCircle(center: center, radius: radius), i, 0.04, false, line); i = i + 0.08; } } else { //背景的线 实线 Paint line = Paint() ..color = lineColor! ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..strokeWidth = width; canvas.drawCircle( // 画圆方法 center, radius, line); } } //画上面的圆 if (completeWidth > 0) { double arcAngle = 2 * pi * (completePercent / 100); // 从哪开始 1从下开始, 2 从上开始 3 从左开始 4 从右开始 默认从下开始 double start = pi / 2; if (startType == 2) { start = -pi / 2; } else if (startType == 3) { start = pi; } else if (startType == 4) { start = pi * 2; } //创建画笔 Paint paint = Paint() ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..strokeWidth = completeWidth; ///是渐变圆 if (isGradient == true) { //渐变圆 深色位置偏移量 默认深色在下面 double transform; if (isTransform == false) { //深色在下面 transform = -pi / 2; } else { //深色在左面 transform = pi * 2; } paint.shader = SweepGradient( startAngle: 0.0, endAngle: pi * 2, colors: lineColors, tileMode: TileMode.clamp, transform: GradientRotation(transform), ).createShader( Rect.fromCircle(center: center, radius: radius), ); canvas.drawArc(Rect.fromCircle(center: center, radius: radius), start, arcAngle, false, paint); } else { ///是实体圆 paint.color = completeColor; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), start, // -pi / 2,从正上方开始 pi / 2,从下方开始 arcAngle, false, paint, ); } } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; } |
实践示例
我们用 Flutter 新建项目的例子代码来演示,如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke "debug painting" (press "p" in the console, choose the // "Toggle Debug Paint" action from the Flutter Inspector in Android // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), CustomPaint( size: const Size(200, 200), foregroundPainter: MyPainter( lineColor: const Color(0xFF000000), lineColors: [const Color(0xFF000000), const Color(0xFFFFFFFF)], completeWidth: 10, width: 6, completePercent: 90, completeColor: const Color(0xFFFF000F), ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } |
渐变色的圆
代码看看就会了
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import 'dart:math'; import 'package:flutter/cupertino.dart'; ///渐变圆 class MyGradientPainter extends CustomPainter { //默认的线的背景颜色 List<Color> lineColor; //默认的线的宽度 double width; double endAngle; MyGradientPainter({ required this.lineColor, required this.width, this.endAngle = pi * 2, }); @override void paint(Canvas canvas, Size size) { Offset center = Offset(size.width / 2, size.height / 2); // 坐标中心 double radius = min(size.width / 2, size.height / 2); // 半径 var paint = Paint() ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke ..isAntiAlias = true //抗锯齿 ..strokeWidth = width; paint.shader = SweepGradient( startAngle: 0.0, endAngle: pi * 2, colors: lineColor, transform: const GradientRotation(pi * 1.2), ).createShader( Rect.fromCircle(center: center, radius: radius), ); canvas.drawArc(Rect.fromCircle(center: center, radius: radius), 0, 2 * pi, false, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; } |
实践示例
我们用 Flutter 新建项目的例子代码来演示,如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke "debug painting" (press "p" in the console, choose the // "Toggle Debug Paint" action from the Flutter Inspector in Android // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), CustomPaint( size: const Size(200, 200), foregroundPainter: MyGradientPainter( width: 8, lineColor: [const Color(0xFF000000),const Color(0xFFFFFFFF)]), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } |
参考链接
Flutter全屏效果实现
在切换横竖屏/全屏模式时用到 SystemChrome,它作为一个全局属性,很像 Android 的 Application,功能很强大。注意,这个SystemChrome 里面的 Chrome 实质上指的是 ChromeOS 不是 Chrome 浏览器,这些 API 最早在 ChromeOS 上实现,因此才这样命名。
setPreferredOrientations
|
1 |
在我们日常应用中可能会需要设置横竖屏,或锁定单方向屏幕等不同要求,通过 **setPreferredOrientations** 配合实现;简单可以按 **portraitUp 上 / portraitDown 下 / landscapeLeft 右 / landscapeRight 左** 来区分; **Tips:landscapeLeft 是以 portraitUp 顺时针旋转 90 度;landscapeRight 是以逆时针旋转 90 度,故是视觉相反。** |
1. 单方向
|
1 |
若需要固定应用为单一方向,仅需设置所需要的方向属性即可; |
|
1 2 3 4 5 6 7 8 |
// 竖直上 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // 竖直下 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitDown]); // 水平左 SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft]); // 水平右 SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeRight]); |
2. 多方向
|
1 |
若需要应用随重力感应变化方向,需设置多个方向属性; |
|
1 2 3 4 5 6 |
// 竖直方向 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); // 水平方向 SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeLeft]); // 多方向 SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeLeft, DeviceOrientation.portraitUp]); |
Tips:
- portraitDown 属性请注意,多方向时一般不会有效果,系统默认不会颠倒;
- 多方向设置时初始方向分两种情况,第一种:当前重力感应方向不在设置多方向列表中,初始方向为列表第一个设置方法;第二种:当前重力感应方向在设置多方向列表中,无论顺序第几位,默认均展示当前重力感应方向(非 portraitDown)。
setEnabledSystemUIOverlays
|
1 2 3 |
**setEnabledSystemUIOverlays** 是指定在应用程序运行时可见的系统叠加,主要对状态栏的操作,读起来比较拗口,但是看测试用例就很明了;参数分 **top 顶部 / bottom 底部** 两种; 这个 API 从 v2.3.0-17.0.pre 开始被标注为过期,被 setEnabledSystemUIMode 替代 |
1. SystemUiOverlay.top
|
1 |
默认隐藏底部虚拟状态栏(需手机支持虚拟状态栏设备),即三大金刚键;获取焦点后展示状态栏,展示大小为去掉状态栏时整体大小; |
|
1 |
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top]); |
2. SystemUiOverlay.bottom
|
1 |
默认隐藏顶部虚拟状态栏,获取焦点后展示状态栏,展示大小为去掉状态栏时整体大小; |
|
1 |
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]); |
3. 两者皆有
|
1 |
即默认情况,顶部底部状态栏均展示; |
|
1 |
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]); |
setSystemUIOverlayStyle
|
1 |
**setSystemUIOverlayStyle** 用来设置状态栏顶部和底部样式,默认有 **light** 和 **dark** 模式,也可以按照需求自定义样式; |
1. systemNavigationBarColor
|
1 |
该属性仅用于 **Android** 设备且 **SDK >= O** 时,底部状态栏颜色; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(systemNavigationBarColor: Colors.pink)); |
2. systemNavigationBarDividerColor
|
1 |
该属性仅用于 **Android** 设备且 **SDK >= P** 时,底部状态栏与主内容分割线颜色,效果不是很明显; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(systemNavigationBarDividerColor: Colors.yellow)); |
3. systemNavigationBarIconBrightness
|
1 |
该属性仅用于 **Android** 设备且 **SDK >= O** 时,底部状态栏图标样式,主要是三大按键颜色; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(systemNavigationBarColor: Colors.pink)); |
4. statusBarColor
|
1 |
该属性仅用于 **Android** 设备且 **SDK >= M** 时,顶部状态栏颜色; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.red)); |
5. statusBarIconBrightness
|
1 |
该属性仅用于 **Android** 设备且 **SDK >= M** 时,顶部状态栏图标的亮度;但感觉并不明显; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarIconBrightness: Brightness.dark)); |
6. statusBarBrightness
|
1 |
该属性仅用于 **iOS** 设备顶部状态栏亮度; |
|
1 |
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarBrightness: Brightness.light)); |
setApplicationSwitcherDescription
|
1 |
个人理解该属性显示效果是在应用程序切换器相关的应用程序的当前状态时,但是反复测试并没有实际效果,希望有理解的大神多多指点; |
|
1 2 3 4 5 6 |
SystemChrome.setApplicationSwitcherDescription( const ApplicationSwitcherDescription( label: "Demo Flutter", primaryColor: 0xFFE53935)) .then((_) { runApp(new MyApp()); }); |
|
1 |
整体来说 **Flutter** 对顶部底部状态栏的设置很方便,只是有些理解不够深入的地方,有见解对朋友希望多多指导! |
实践示例
上面都是对于 API 的介绍,但是在实际项目开发中,上述的代码究竟应该添加到何处呢?我们用 Flutter 新建项目的例子代码来演示,如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override void initState() { /// 初始化时隐藏 SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); super.initState(); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke "debug painting" (press "p" in the console, choose the // "Toggle Debug Paint" action from the Flutter Inspector in Android // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } } |
全屏模式简介
Flutter 定义了五种全屏模式,其实就是 Android/iOS 系统的全屏模式的映射,定义如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
/// Describes different display configurations for both Android and iOS. /// /// These modes mimic Android-specific display setups. /// /// Used by [SystemChrome.setEnabledSystemUIMode]. enum SystemUiMode { /// Fullscreen display with status and navigation bars presentable by tapping /// anywhere on the display. /// /// Available starting at SDK 16 or Android J. Earlier versions of Android /// will not be affected by this setting. /// /// For applications running on iOS, the status bar and home indicator will be /// hidden for a similar fullscreen experience. /// /// Tapping on the screen displays overlays, this gesture is not received by /// the application. /// /// See also: /// /// * [SystemUiChangeCallback], used to listen and respond to the change in /// system overlays. leanBack, /// Fullscreen display with status and navigation bars presentable through a /// swipe gesture at the edges of the display. /// /// Available starting at SDK 19 or Android K. Earlier versions of Android /// will not be affected by this setting. /// /// For applications running on iOS, the status bar and home indicator will be /// hidden for a similar fullscreen experience. /// /// A swipe gesture from the edge of the screen displays overlays. In contrast /// to [SystemUiMode.immersiveSticky], this gesture is not received by the /// application. /// /// See also: /// /// * [SystemUiChangeCallback], used to listen and respond to the change in /// system overlays. immersive, /// Fullscreen display with status and navigation bars presentable through a /// swipe gesture at the edges of the display. /// /// Available starting at SDK 19 or Android K. Earlier versions of Android /// will not be affected by this setting. /// /// For applications running on iOS, the status bar and home indicator will be /// hidden for a similar fullscreen experience. /// /// A swipe gesture from the edge of the screen displays overlays. In contrast /// to [SystemUiMode.immersive], this gesture is received by the application. /// /// See also: /// /// * [SystemUiChangeCallback], used to listen and respond to the change in /// system overlays. immersiveSticky, /// Fullscreen display with status and navigation elements rendered over the /// application. /// /// Available starting at SDK 29 or Android 10. Earlier versions of Android /// will not be affected by this setting. /// /// For applications running on iOS, the status bar and home indicator will be /// visible. /// /// The system overlays will not disappear or reappear in this mode as they /// are permanently displayed on top of the application. /// /// See also: /// /// * [SystemUiOverlayStyle], can be used to configure transparent status and /// navigation bars with or without a contrast scrim. edgeToEdge, /// Declares manually configured [SystemUiOverlay]s. /// /// When using this mode with [SystemChrome.setEnabledSystemUIMode], the /// preferred overlays must be set by the developer. /// /// When [SystemUiOverlay.top] is enabled, the status bar will remain visible /// on all platforms. Omitting this overlay will hide the status bar on iOS & /// Android. /// /// When [SystemUiOverlay.bottom] is enabled, the navigation bar and home /// indicator of Android and iOS applications will remain visible. Omitting this /// overlay will hide them. /// /// Omitting both overlays will result in the same configuration as /// [SystemUiMode.leanBack]. manual, } |
下面我们简单介绍一下这几种模式的区别:
1. leanBack : 向后倾斜模式适用于用户不会与屏幕进行大量互动的全屏体验,例如在观看视频时。
当用户希望调出系统栏时,只需点按屏幕上的任意位置即可。2. immersive : 沉浸模式适用于用户将与屏幕进行大量互动的应用。 示例包括游戏、查看图库中的图片或者阅读分页内容,如图书或演示文稿中的幻灯片。
当用户需要调出系统栏时,他们可从隐藏系统栏的任一边滑动。要求使用这种这种意图更强的手势是为了确保用户与您应用的互动不会因意外轻触和滑动而中断。3. immersiveSticky : 在普通的沉浸模式中,只要用户从边缘滑动,系统就会负责显示系统栏,您的应用甚至不会知道发生了该手势。因此,如果用户实际上可能是出于主要的应用体验而需要从屏幕边缘滑动,例如在玩需要大量滑动的游戏或使用绘图应用时,您应改为启用“粘性”沉浸模式。
在粘性沉浸模式下,如果用户从隐藏了系统栏的边缘滑动,系统栏会显示出来,但它们是半透明的,并且轻触手势会传递给应用,因此应用也会响应该手势。
例如,在使用这种方法的绘图应用中,如果用户想绘制从屏幕最边缘开始的线条,则从这个边缘滑动会显示系统栏,同时还会开始绘制从最边缘开始的线条。无互动几秒钟后,或者用户在系统栏之外的任何位置轻触或做手势时,系统栏会自动消失。4. edgeToEdge : 实现从边到边的全面屏体验后,系统栏会覆盖在应用内容前方。应用也得以通过更大幅面的内容为用户带来更具有冲击力的体验。
5. manual : 手动配置,可以独立配置状态栏,导航栏的显示与隐藏。如果选择状态栏,导航栏都隐藏,那么行为与向后倾斜模式相同。
适配安卓刘海、水滴屏显示全屏
上面的配置在刘海、水滴屏幕上会在顶部出现一道黑色的背景,比如 Honor 30 。解决方法就是 将 shortEdges 放到 style 标签内。如下:
找到 android/app/src/main/res/目录,新建 values-v27 目录(这个API从Android API 27 才开始支持),并在目录下创建 styles.xml ,内容从原来的 values/styles.xml 中拷贝出来,将 shortEdges 放到 style 标签内,如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- Theme applied to the Android Window while the process is starting --> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <!-- Show a splash screen on the activity. Automatically removed when Flutter draws its first frame --> <item name="android:windowBackground">@drawable/launch_background</item> <!-- 刘海,水滴屏幕全屏适配 --> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> <!-- Theme applied to the Android Window as soon as the process has started. This theme determines the color of the Android Window while your Flutter UI initializes, as well as behind your Flutter UI while its running. This Theme is only used starting with V2 of Flutter's Android embedding. --> <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <item name="android:windowBackground">@android:color/white</item> <!-- 刘海,水滴屏幕全屏适配 --> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources> |
- LaunchTheme 指的是启动页的主题,也就是我们常说的 Splash 页面。如果需要启动页全屏就放在里面。
- NormalTheme 代表正常页面的主题。
同样需要修改暗黑主题相关的风格文件,如下:
创建 android/app/src/main/res/values-night-v27 目录,新建 styles.xml ,将 shortEdges 放到 style 标签内 (这个API从Android API 27 才开始支持),如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- Theme applied to the Android Window while the process is starting --> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <!-- Show a splash screen on the activity. Automatically removed when Flutter draws its first frame --> <item name="android:windowBackground">@drawable/launch_background</item> <!-- 刘海,水滴屏幕全屏适配 --> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> <!-- Theme applied to the Android Window as soon as the process has started. This theme determines the color of the Android Window while your Flutter UI initializes, as well as behind your Flutter UI while its running. This Theme is only used starting with V2 of Flutter's Android embedding. --> <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> <item name="android:windowBackground">@android:color/white</item> <!-- 刘海,水滴屏幕全屏适配 --> <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> </style> </resources> |
参考链接
不同版本的TLS在Android/iOS中的支持情况
本文要解决的两类问题:
- 在Android4.1-Android5.0的系统上启用TLSv1.1和TLSv1.2
- java.lang.IllegalArgumentException: protocol TLSv1.1 is not supported
这两个问题比较具有代表性,在群里面讨论的时候见过的次数也是最多的,因此有些人遇到的问题也许协议名称不一样,但是本质都是类似的。
问题原因和解决思路分析
从 Android5.0 行为变更 可以看到 Android 5.0 开始默认启用了 TLSv1.1 和 TLSv1.2,但是从 Android 4.1 开始 TLSv1.1 和 TLSv1.2 其实就被支持了,只是默认没有启用而已。
我们最常用到的几种协议是SSLv3、TLSv1、TLSv1.1和 TLSv1.2,解决上面两个问题要搞清楚的是这几个协议在Android 系统中被支持的情况和被启用的情况,然后我们结合 minSdkVersion 和 targetSdkVersion 来选择协议即可,不同版本的 Android 系统对上述协议的支持情况:
客户端(SSLSocket)的支持情况:
| 协议 | 被支持(Api级别) | 被启用(Api级别) |
|---|---|---|
| SSLv3 | 1–25 | 1–22 |
| TLSv1 | 1+ | 1+ |
| TLSv1.1 | 16+ | 20+ |
| TLSv1.2 | 16+ | 20+ |
服务端(SSLServerSocket)的支持情况:
| 协议 | 被支持(Api级别) | 被启用(Api级别) |
|---|---|---|
| SSLv3 | 1–25 | 1–22 |
| TLSv1 | 1+ | 1+ |
| TLSv1.1 | 16+ | 16+ |
| TLSv1.2 | 16+ | 16+ |
数据来源:https://developer.android.com/reference/javax/net/ssl/SSLSocket
注意:这里说的客户端和服务端不是指Android端和JavaEE端/PHP端(还有Python、.NET等等),是指的在Android开发中的客户端Socket(SSLSocket)和服务端Socket(SSLServerSocket)。
到这里其实已经知道本文开始处的问题的原因了,TLSv1.1和TLSv1.2从Android4.1(Api级别16)开始才被支持,从Android4.4 Wear(Api级别20)才被启用(手机是Android5.0,Api级别21),因此在不同版本的Android系统中会出现需要被启用和启用时报不被支持的问题。
我们可以写一个TLS协议通用的兼容类,在所有的Android中强制启用已经被支持的所有协议,总结一下就是Android8.0及以上系统可以强制启用TLSv1、TLSv1.1和TLSv1.2协议,Android4.1-Android7.1.1系统可以强制启用SSLv3、TLSv1、TLSv1.1和TLSv1.2协议,其它版本系统可以强制启用SSLv3和TLSv1协议。
综上所述,如果开发者使用SSLv3协议,那么minSdkVersion不限制,targetSdkVersion不高于25,并且需要尽快更新为TLSv1.1协议或者TLS1.2协议;如果开发者使用TLSv1.1协议或者TLSv1.2协议,那么minSdkVersion应该不低于16,targetSdkVersion不限制;如果开发者使用TLSv1协议,那么目前不受限制。
代码实现
我们需要让SSLSocket启用对应的协议,代码对应的方法是:
|
1 |
SSLSocket#setEnabledProtocols(String[]); |
因此我们需要先在不同版本的Android系统中生成不同的协议数组:
|
1 2 3 4 5 6 7 8 9 10 11 |
private static final String PROTOCOL_ARRAY[]; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PROTOCOL_ARRAY = new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"}; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}; } else { PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1"}; } } |
SSLSocket是由SSLSocketFactory负责生成的,我们再写一个SSLSocketFactory的包装类,主要代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class TLSSocketFactory extends SSLSocketFactory { private static final String PROTOCOL_ARRAY[]; static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PROTOCOL_ARRAY = new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"}; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}; } else { PROTOCOL_ARRAY = new String[]{"SSLv3", "TLSv1"}; } } private SSLSocketFactory delegate; /** * 默认构造方法,直接生成启用所有协议的SSLSocket。 */ public TLSSocketFactory() { try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[]{DEFAULT_TRUST_MANAGERS}, new SecureRandom()); delegate = sslContext.getSocketFactory(); } catch (GeneralSecurityException e) { throw new AssertionError(); // The system has no TLS. Just give up. } } /** * 包装SSLSocketFactory的构造方法,让外部生成的SSLScoket启用所有协议。 */ public TLSSocketFactory(SSLSocketFactory factory) { this.delegate = factory; } /** * 如果是SSLSocket,启用所有协议。 */ private static void setSupportProtocolAndCipherSuites(Socket socket) { if (socket instanceof SSLSocket) { ((SSLSocket) socket).setEnabledProtocols(PROTOCOL_ARRAY); } } // TODO 下面省去每一个createSocket()方法调用setSupportProtocolAndCipherSuites()方法的代码。 } |
iOS客户端支持情况
iOS 客户端(SSLSocket)的支持情况:
TLS 1.2 从 iOS 5 开始支持(TLS 1.2 was first added to iOS in iOS 5)
另外 对于在 2020 年 9 月 1 日格林尼治标准时间/世界标准时间 00:00 或之后颁发的 TLS 服务器证书,其有效期不得超过 398 天。
参考链接
macOS Big Sur(11.6.7/Intel CPU)编译Android/iOS/macOS版本的RocksDB 6.29.5
从 RocksDB 7.0 开始,RocksDB 要求编译的 C++ 必须支持 C++ 17 ,( Dropping some compiler support in 7.0#9388)但是目前的Android/iOS版本显然暂时还不能大范围的适配 C++ 17,因此我们目前只能使用 6.x 版本。
Android:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# Android NDK 22.1.7171670 $ brew install git $ brew install cmake $ brew install sed $ git clone https://github.com/facebook/rocksdb.git $ git checkout v6.29.5 # 修改代码,消除警告视为错误,否则会编译不通过 $ gsed -i "s/-Werror//g" CMakeLists.txt # Android最低支持的版本是 Android 23 低于这个版本会出现API缺失导致编译失败 $ cmake . -DCMAKE_ANDROID_NDK=/Users/xxxx/Library/Android/sdk/ndk-bundle \ -DCMAKE_SYSTEM_NAME=Android \ -DCMAKE_SYSTEM_VERSION=23 \ -DCMAKE_ANDROID_STL_TYPE=c++_shared \ -DROCKSDB_LITE=ON \ -DPORTABLE=ON \ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ -DWITH_TESTS=OFF \ -DWITH_TOOLS=OFF \ -DWITH_GFLAGS=OFF \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a // -DCMAKE_ANDROID_ARCH_ABI=arm64-v8a // -DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a // -DCMAKE_ANDROID_ARCH_ABI=armeabi // -DCMAKE_ANDROID_ARCH_ABI=x86 // -DCMAKE_ANDROID_ARCH_ABI=x86_64 $ make # 在当前目录下生成 # librocksdb.so |
iOS/macOS ARM:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#Xcode Version 13.2.1 (13C100) $ brew install git $ brew install cmake $ git clone https://github.com/facebook/rocksdb.git $ git checkout v6.29.5 $ export PORTABLE=1 $ export TARGET_OS=IOS $ export ROCKSDB_LITE=1 $ export IOS_CROSS_COMPILE=1 # int128兼容支持,默认支持,但是在iOS设备上是不支持的 $ export TEST_UINT128_COMPAT=1 $ export DISABLE_WARNING_AS_ERROR=1 $ export DEBUG_LEVEL=0 $ export EXTRA_CXXFLAGS="-DNPERF_CONTEXT -DNIOSTATS_CONTEXT" # iOS目前只能构建静态库,不能构建动态库 $ make V=1 VERBOSE=1 -j16 static_lib |
macOS x86:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#Xcode Version 13.2.1 (13C100) $ brew install git $ brew install cmake $ git clone https://github.com/facebook/rocksdb.git $ git checkout v6.29.5 $ export PORTABLE=1 $ export ROCKSDB_LITE=1 # int128兼容支持,默认支持,但是在iOS设备上是不支持的 $ export TEST_UINT128_COMPAT=1 $ export DISABLE_WARNING_AS_ERROR=1 $ export DEBUG_LEVEL=0 $ export EXTRA_CXXFLAGS="-DNPERF_CONTEXT -DNIOSTATS_CONTEXT" $ make V=1 VERBOSE=1 -j16 static_lib # make V=1 VERBOSE=1 -j16 shared_lib 动态库 |
参考链接
- CMake could not find JNI
- Is there any document for building/integrating rocksdb for/into android/ios app ? #1193
- cmake-toolchains(7)
- Got message "Please install gflags to run rocksdb tools" while running ./db_bench #1775
- cmake Disable Werror
- NDK CMake
- Cross Compiling for iOS, tvOS, or watchOS
- https://github.com/facebook/rocksdb/blob/master/INSTALL.md
- rocksdb编译步骤——Java、Golang、mac
- RocksDB 7 may have C++17 dependency #75496
- Dropping some compiler support in 7.0#9388
- Cant build librocksdb.a for iOS platform in M1 #8416
- Performance impact from perf context with PerfLevel::kDisable #9372
- https://github.com/facebook/rocksdb/blob/c465509379d72068c19b55dbc69bf08d8c387fbe/build_tools/build_detect_platform#L136
- building for iOS Simulator, but linking in object file built for macOS, file '..customLib.a' for architecture x86_64
Android官方推荐: DialogFragment创建对话框
最近在使用官方的 Android Jetpack 库的时候,发现 Activity/Fragment 都有对应的 lifecycle(生命周期) 组件,比如ViewModel。唯独 Dialog 没有对应的组件,感觉非常奇怪。
搜索了一通,发现官方推荐 DialogFragment 替代直接使用 Dialog 。 DialogFragment 在 Android 3.0 时被引入,是一种特殊的 Fragment ,用于在 Activity 的内容之上展示一个模态的对话框。典型的用于:展示警告框,输入框,确认框等等。
在 DialogFragment 产生之前,我们创建对话框:一般采用 AlertDialog 和 Dialog 。注意:官方不推荐直接使用Dialog创建对话框。
另外, 更常见的在 logcat 中经常看到输出由于 Dialog 引起的 WindowLeaked 日志,这个日志产生的原因就是 Dialog 显示所依赖的 Activity 被销毁/重建的时候没有关闭显示在上层的 Dialog 。但是很多时候我们不方便在 Activity 的 onDestroy() 中查找全部的 Dialog 来逐个关闭,尤其是依赖的 Activity 是第三方的 SDK 创建的情况下。这种情况下,DialogFragment 不失为一个好的替代选项。
好处与用法
使用DialogFragment来管理对话框,当旋转屏幕和按下后退键时可以更好的管理其声明周期,它和Fragment有着基本一致的生命周期。且DialogFragment也允许开发者把Dialog作为内嵌的组件进行重用,类似Fragment(可以在大屏幕和小屏幕显示出不同的效果)。下面会通过例子展示这些好处~
使用DialogFragment至少需要实现onCreateView或者onCreateDIalog方法。onCreateView即使用定义的xml布局文件展示Dialog。onCreateDialog即利用AlertDialog或者Dialog创建出Dialog。
重写onCreateView创建Dialog
a)布局文件,我们创建一个设置名称的布局文件:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <TextView android:id="@+id/id_label_your_name" android:layout_width="wrap_content" android:layout_height="32dp" android:gravity="center_vertical" android:text="Your name:" /> <EditText android:id="@+id/id_txt_your_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_toRightOf="@id/id_label_your_name" android:imeOptions="actionDone" android:inputType="text" /> <Button android:id="@+id/id_sure_edit_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_below="@id/id_txt_your_name" android:text="ok" /> </RelativeLayout> |
b)继承DialogFragment,重写onCreateView方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.example.zhy_dialogfragment; import android.app.DialogFragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; public class EditNameDialogFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_edit_name, container); return view; } } |
c)测试运行:
Main方法中调用:
|
1 2 3 4 |
public void showEditDialog(View view) { final EditNameDialogFragment editNameDialog = new EditNameDialogFragment(); editNameDialog.show(getFragmentManager(), "EditNameDialog"); } |
效果图:
可以看到,对话框成功创建并显示出来,不过默认对话框有个讨厌的标题,我们怎么去掉呢:可以在onCreateView中调用getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);即可去掉。即:
|
1 2 3 4 5 6 7 8 9 10 |
public class EditNameDialogFragment extends DialogFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); final View view = inflater.inflate(R.layout.fragment_edit_name, container); return view; } } |
效果图:
很完美的去掉了讨厌的标题。
重写onCreateDialog创建Dialog
在 onCreateDialog 中一般可以使用 AlertDialog 或者 Dialog 创建对话框,不过既然 Google 不推荐直接使用 Dialog,我们就使用 AlertDialog 来创建一个登录的对话框。
a)布局文件
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" > <ImageView android:layout_width="match_parent" android:layout_height="64dp" android:background="#FFFFBB33" android:contentDescription="@string/app_name" android:scaleType="center" android:src="@drawable/title" /> <EditText android:id="@+id/id_txt_username" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_marginTop="16dp" android:hint="input username" android:inputType="textEmailAddress" /> <EditText android:id="@+id/id_txt_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_marginTop="4dp" android:fontFamily="sans-serif" android:hint="input password" android:inputType="textPassword" /> </LinearLayout> |
b)继承 DialogFragment 重写 onCreateDialog 方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.example.zhy_dialogfragment; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; public class LoginDialogFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); // Get the layout inflater final LayoutInflater inflater = getActivity().getLayoutInflater(); final View view = inflater.inflate(R.layout.fragment_login_dialog, null); // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout builder.setView(view) // Add action buttons .setPositiveButton("Sign in", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { } }).setNegativeButton("Cancel", null); return builder.create(); } } |
c)调用
|
1 2 3 4 |
public void showLoginDialog(View view) { final LoginDialogFragment dialog = new LoginDialogFragment(); dialog.show(getFragmentManager(), "loginDialog"); } |
效果图:
可以看到通过重写 onCreateDialog 同样可以实现创建对话框,效果还是很nice的。
传递数据给Activity
从dialog传递数据给Activity,可以使用“fragment interface pattern”的方式,下面通过一个改造上面的登录框来展示这种模式。
改动比较小,直接贴代码了:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package com.example.zhy_dialogfragment; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; public class LoginDialogFragment extends DialogFragment { private EditText mUsername; private EditText mPassword; public interface LoginInputListener { void onLoginInputComplete(String username, String password); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); // Get the layout inflater final LayoutInflater inflater = getActivity().getLayoutInflater(); final View view = inflater.inflate(R.layout.fragment_login_dialog, null); mUsername = (EditText) view.findViewById(R.id.id_txt_username); mPassword = (EditText) view.findViewById(R.id.id_txt_password); // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout builder.setView(view) // Add action buttons .setPositiveButton("Sign in", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { final LoginInputListener listener = (LoginInputListener) getActivity(); listener.onLoginInputComplete(mUsername.getText().toString(), mPassword.getText().toString()); } }).setNegativeButton("Cancel", null); return builder.create(); } } |
拿到username和password的引用,在点击登录的时候,把activity强转为我们自定义的接口:LoginInputListener,然后将用户输入的数据返回。
MainActivity中需要实现我们的接口LoginInputListener,实现我们的方法,就可以实现当用户点击登陆时,获得我们的帐号密码了:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.example.zhy_dialogfragment; import com.example.zhy_dialogfragment.LoginDialogFragment.LoginInputListener; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; public class MainActivity extends Activity implements LoginInputListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void showLoginDialog(View view) { final LoginDialogFragment dialog = new LoginDialogFragment(); dialog.show(getFragmentManager(), "loginDialog"); } @Override public void onLoginInputComplete(String username, String password) { Toast.makeText(this, "帐号:" + username + ", 密码 :" + password, Toast.LENGTH_SHORT).show(); } } |
效果:
DialogFragment做屏幕适配
我们希望,一个对话框在大屏幕上以对话框的形式展示,而小屏幕上则直接嵌入当前的Actvity中。这种效果的对话框,只能通过重写onCreateView实现。下面我们利用上面的EditNameDialogFragment来显示。
EditNameDialogFragment我们已经编写好了,直接在MainActivity中写调用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public void showDialogInDifferentScreen(View view) { final FragmentManager fragmentManager = getFragmentManager(); final EditNameDialogFragment newFragment = new EditNameDialogFragment(); final boolean mIsLargeLayout = getResources().getBoolean(R.bool.large_layout) ; Log.e("TAG", mIsLargeLayout+""); if (mIsLargeLayout ) { // The device is using a large layout, so show the fragment as a // dialog newFragment.show(fragmentManager, "dialog"); } else { // The device is smaller, so show the fragment fullscreen final FragmentTransaction transaction = fragmentManager.beginTransaction(); // For a little polish, specify a transition animation transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); // To make it fullscreen, use the 'content' root view as the // container // for the fragment, which is always the root view for the activity transaction.replace(R.id.id_ly, newFragment).commit(); } } |
可以看到,我们通过读取R.bool.large_layout,然后根据得到的布尔值,如果是大屏幕则直接以对话框显示,如果是小屏幕则嵌入我们的Activity布局中
这个R.bool.large_layout是我们定义的资源文件:
在默认的values下新建一个bools.xml
|
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <resources> <bool name="large_layout">false</bool> </resources> |
然后在res下新建一个values-large,在values-large下再新建一个bools.xml
|
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8"?> <resources> <bool name="large_layout">true</bool> </resources> |
最后测试:
左边为模拟器,右边为我的手机~~~~~
屏幕旋转
当用户输入帐号密码时,忽然旋转了一下屏幕,帐号密码不见了~~~是不是会抓狂
传统的 new AlertDialog 在屏幕旋转时,第一不会保存用户输入的值,第二还会报异常,因为 Activity 销毁前不允许对话框未关闭。而通过 DialogFragment 实现的对话框则可以完全不必考虑旋转的问题。
我们直接把上面登录使用 AlertDialog 创建的登录框,拷贝到 MainActivity 中直接调用:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public void showLoginDialogWithoutFragment(View view) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); // Get the layout inflater final LayoutInflater inflater = this.getLayoutInflater(); // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout builder.setView(inflater.inflate(R.layout.fragment_login_dialog, null)) // Add action buttons .setPositiveButton("Sign in", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { // sign in the user ... } }).setNegativeButton("Cancel", null).show(); } |
下面我分别点击两种方式创建的登录框,看效果图:
可以看到,传统的Dialog旋转屏幕时就消失了,且后台log会报异常~~~使用DialogFragment则不受影响。
参考链接
Flutter的ViewModel实现(状态管理)
2019 Google I/O 大会,Flutter 团队在“Pragmatic State Management in Flutter ”演讲上正式介绍了 Provider。自此,Provider 代替 Provide 成为官方推荐的状态管理方式之一。
一、为什么需要状态管理
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将数据映射成视图就可以了。你可能并不需要状态管理,就像下面这样。
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用可能会是这样。
随着你的应用需要共享多处统一状态时,我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式 — StatefulWidget。然而我们发现它仅适合用于在单个 Widget 内部维护其状态。当我们需要使用跨组件的状态时,StatefulWidget 将不再是一个好的选择。
State 属于某一个特定的 StatefulWidget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,很容易就增大代码耦合度。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。
二、什么是 Provider
那么我们该如何解决上面这种糟糕的情况呢。在上手了 Provider 之后,我可以说这个库是一个相当不错的解决方案。我们先来简单说一下 Provider 的基本作用。
Provider 从名字上就很容易理解,它用来提供数据,而它的优秀之处在于无论是在单个页面还是在整个 app 都有相应的解决方案,我们可以很方便的管理状态,并在合适的时机释放资源。可以说,Provider 的目标就是完全替代 StatefulWidget。
说了很多还是很抽象,我们先一起做一个最简单的例子。
三、创建一个简单计数器 app
这里我们用这个 Counter App 为例,给大家介绍如何在两个独立的页面中共享计数器(counter)的状态应该怎么做,具体会像下面这样。
- 两个页面中心字体共用了同一个字体大小。
- 第二个页面的按钮将会让数字增加(第一个页面的数字将会同步增加。)
3.1 第一步:添加依赖
在 pubspec.yaml 中添加 Provider 的依赖。
- 实际添加请参考:https://pub.flutter-io.cn/packages/provider/install
- 由于版本冲突添加失败请参考: https://juejin.cn/post/6844903667955400718
3.2 第二步:创建数据 Model
这里的 Model 实际上就是我们的状态,它不仅储存了我们的数据模型,而且还包含了更改数据的方法,并暴露出它想要暴露出的数据。
|
1 2 3 4 5 6 7 8 9 10 11 |
import 'package:flutter/material.dart'; class CounterModel with ChangeNotifier { int _count = 0; int get value => _count; void increment() { _count++; notifyListeners(); } } |
这个类意图很清晰,数据就是一个 int 类型的 _count,下划线代表私有。通过 get value 把 _count 值暴露出来。并提供 increment 方法用于更改数据。
这里使用了 mixin 混入了 ChangeNotifier,这个类能够帮助我们自动管理所有听众。
当调用 notifyListeners() 时,它会通知所有听众进行刷新。
3.3 第三步:创建顶层共享数据
我们在 main 方法中初始化全局数据:刚才编写的 CounterModel 以及 textSize。为了要在不同页面共享这个数据,我们就需要将其放入顶层节点(MaterialApp 之上)进行保存。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void main() { final counter = CounterModel(); final textSize = 48; runApp( Provider<int>.value( value: textSize, child: ChangeNotifierProvider.value( value: counter, child: MyApp(), ), ), ); } |
通过 Provider<T>.value 能够管理一个恒定的数据,并提供给子孙节点使用。我们只需要将数据在其 value 属性中声明即可。在这里我们将textSize 传入。
而 ChangeNotifierProvider<T>.value 不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有听众刷新。(通过之前我们说过的 notifyListeners)
此处的 <T> 泛型可省略。但是我建议大家还是进行声明,这会使你的应用更加健壮。
除上述几个属性之外 Provider<T>.value 还提供 UpdateShouldNotify Function,用于控制刷新时机。
typedef UpdateShouldNotify<T> = bool Function(T previous, T current);
我们可以在这里传入一个方法 (T previous, T current){...},并获得前后两个 Model 的实例,然后通过比较两个 Model 以自定义刷新规则,这个方法将返回 bool 表示是听众否需要刷新。(默认为 previous != current 则刷新。)
为了让各位思维连贯,我还是在这里放上这个平淡无奇的 MyApp Widget 代码。
|
1 2 3 4 5 6 7 8 9 |
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.dark(), home: FirstScreen(), ); } } |
3.4 第四步:在子页面中获取状态
在这里我们有两个页面,FirstScreen 和 SecondScreen。我们先来看 FirstScreen 的代码。
3.4.1 Provider.of<T>(context)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class FirstScreen extends StatelessWidget { @override Widget build(BuildContext context) { final _counter = Provider.of<CounterModel>(context); final textSize = Provider.of<int>(context).toDouble(); return Scaffold( appBar: AppBar( title: Text('FirstPage'), ), body: Center( child: Text( 'Value: ${_counter.value}', style: TextStyle(fontSize: textSize), ), ), floatingActionButton: FloatingActionButton( onPressed: () => Navigator.of(context) .push(MaterialPageRoute(builder: (context) => SecondPage())), child: Icon(Icons.navigate_next), ), ); } } |
获取顶层数据最简单的方法就是 Provider.of<T>(context);
这里的泛型 <T> 指定了获取 FirstScreen 向上寻找最近的储存了 T 的祖先节点的数据。我们通过这个方法获取了顶层的 CounterModel 及 textSize。并在 Text 组件中进行使用。
在 Provider.of(context) 中还有一个 bool 类型的 listen 参数,它代表了是否监听数据变化,默认为 true。
floatingActionButton 用来点击跳转到 SecondScreen 页面,和我们的主题无关,你可以忽略这部分代码。
3.4.2 Consumer
看到这里你可能会想,两个页面都是获取顶层状态,代码不都一样吗。别忙着跳到下一节,我们来看另外一种获取状态的方式,使用它能够改善应用程序性能。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Second Page'), ), body: Consumer2<CounterModel,int>( builder: (context, CounterModel counter, int textSize, _) => Center( child: Text( 'Value: ${counter.value}', style: TextStyle( fontSize: textSize.toDouble(), ), ), ), ), floatingActionButton: Consumer<CounterModel>( builder: (context, CounterModel counter, child) => FloatingActionButton( onPressed: counter.increment, child: child, ), child: Icon(Icons.add), ), ); } } |
这里我们要介绍的是第二种方式,使用 Consumer 获取祖先节点中的数据。
在这个页面中,我们有两处使用到了公共 Model。
- 应用中心的文字:使用 CounterModel 在 Text 中展示文字,以及通过 textSize 定义自身的大小。使用到了两个 Model 中的数据。
- 浮动按钮:使用 CounterModel 的
increment方法触发计数器的值增加。使用到了一个 Model。
(1) Single Model Consumer
我们先看 floatingActionButton,使用了一个 Consumer 的情况。
Consumer 使用了 Builder 模式,收到更新通知就会通过 builder 重新构建。Consumer<T> 代表了它要获取哪一个祖先中的 Model。
Consumer 的 builder 实际上就是一个 Function,它接收三个参数(BuildContext context, T model, Widget child)。
- context: context 就是 build 方法传进来的 BuildContext。
- T:T也很简单,就是获取到的最近一个祖先节点中的数据模型。
- child:它用来构建那些与 Model 无关的部分,在多次运行 builder 中,child 不会进行重建。
然后它会返回一个通过这三个参数映射的 Widget 用于构建自身。
在这个浮动按钮的例子中,我们通过 Consumer 获取到了顶层的CounterModel 实例。并在浮动按钮 onPressed 的 callback 中调用其increment 方法。
而且我们成功抽离出 Consumer 中不变的部分,也就是浮动按钮中心的Icon 并将其作为 child 参数传入 builder 方法中。
(2)Consumer2
现在我们再来看中心的文字部分。这时候你可能会有疑惑了,刚才我们讲的 Consumer 获取的只有一个 Model,而现在 Text 组件不仅需要 CounterModel 用以显示计数器,而且还需要获得 textSize 以调整字体大小,该怎么做呢?
遇到这种情况你可以使用 Consumer2<A,B>。使用方式基本上和 Consumer<T> 一致,只不过泛型改为了两个,并且 builder 方法也变成了Function(BuildContext context, A value, B value2, Widget child)。
从源码里面可以看到,这样的帮助类最多的是 Consumer6。如果还有个更多的需求,可以直接按照源码定制你的 Consumer。(感觉并不是很优雅)
(3)Provider.of<T>(context) 与 Consumer 的区别
那么,二者到底有什么差别呢?我们来看 Consumer 的内部实现。
|
1 2 3 4 5 6 7 8 |
@override Widget build(BuildContext context) { return builder( context, Provider.of<T>(context), child, ); } |
可以发现,Consumer 就是通过 Provider.of<T>(context) 来实现的。但是从实现来讲 Provider.of<T>(context) 比 Consumer 简单好用太多,为什么我要使用更加复杂的 Consumer?
实际上 Consumer 非常有用,它的经典之处在于能够在复杂项目中,极大地缩小你的控件刷新范围。Provider.of<T>(context) 将会把调用了该方法的 context 作为听众,并在 notifyListeners 的时候通知其刷新。
举个例子来说,我们的 FirstScreen 使用了 Provider.of<T>(context) 来获取数据,SecondScreen 则没有。
- 你在 FirstScreen 中的 build 方法中添加一个
print('first screen rebuild'); - 然后在 SecondScreen 中的 build 方法中添加一个
print('second screen rebuild'); - 点击第二个页面的浮动按钮,那么你会在控制台看到这句输出。
3.5 First screen rebuild
首先这证明了 Provider.of<T>(context) 会导致调用的 context 页面范围的刷新。
那么第二个页面刷新没有呢? 刷新了,但是只刷新了 Consumer 的部分,甚至连浮动按钮中的 Icon 的不刷新我们都给控制了。你可以在Consumer 的 builder 方法中验证,这里不再啰嗦。
假如你在你的应用的 页面级别 的 Widget 中,使用了 Provider.of<T>(context)。会导致什么后果已经显而易见了,每当其状态改变的时候,你都会重新刷新整个页面。虽然你有 Flutter 的自动优化算法给你撑腰,但你肯定无法获得最好的性能。
所以在这里我建议各位尽量使用 Consumer 而不是 Provider.of<T>(context) 获取顶层数据。
以上便是一个最简单的使用 Provider 的例子。
四、你还需要知道的
4.1 合理选择使用 Provider 的构造方法
在上面这个例子中,我们选择了使用 Provider<T>.value 的构造方法来创建祖先节点中的提供者。除了这种方式,我们还可以使用默认构造方法。
|
1 2 3 4 5 6 7 8 9 10 11 |
Provider({ Key key, @required ValueBuilder<T> builder, Disposer<T> dispose, Widget child, }) : this._( key: key, delegate: BuilderStateDelegate<T>(builder, dispose: dispose), updateShouldNotify: null, child: child, ); |
常规的 key/child 属性我们不在这里展开讲解了。先来看这个看上去相对教复杂一点的 builder。
4.1.1 ValueBuilder
相比起 .value 构造方式中直接传入一个 value 就 ok,这里的 builder 要求我们传入一个 ValueBuilder。这是什么东西呢?
|
1 |
typedef ValueBuilder<T> = T Function(BuildContext context); |
通过源码可以看到,ValueBuilder 其实很简单,就是传入一个 Function 返回一个数据而已。在上面这个例子中,你可以替换成这样。
|
1 2 3 4 |
Provider( builder: (context) => textSize, ... ) |
由于是 Builder 模式,这里默认需要传入 context,实际上我们的 Model(textSize)与 context 并没有关系,所以你完全可以这样写。
|
1 2 3 4 |
Provider( builder: (_) => textSize, ... ) |
4.1.2 Disposer
现在我们知道了 builder,那这个 dispose 方法又用来做什么的呢。实际上这才是 Provider 的点睛之笔。
|
1 |
typedef Disposer<T> = void Function(BuildContext context, T value); |
dispose 属性需要一个 Disposer<T>,而这个其实也是一个回调。
如果你之前使用过 BLoC 的话,相信你肯定遇到过一个头疼的问题。我应该在什么时候释放资源呢? BloC 使用了观察者模式,它旨在替代 StatefulWidget。然而大量的流使用完毕之后必须 close 掉,以释放资源。
然而 Stateless Widget 并没有给我们类似于 dispose 之类的方法,这便是 BLoC 的硬伤。你不得不为了释放资源而使用 StatefulWidget,这与我们的本意相违。而 Provider 则为我们解决了这一点。
当 Provider 所在节点被移除的时候,它就会启动 Disposer<T>,然后我们便可以在这里释放资源。
举个例子,假如我们有这样一个 BLoC。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ValidatorBLoC { StreamController<String> _validator = StreamController<String>.broadcast(); get validator => _validator.stream; validateAccount(String text) { //Processing verification text ... } dispose() { _validator.close(); } } |
这时候我们想要在某个页面提供这个 BLoC 但是又不想使用 StatefulWidget。这时候我们可以在页面顶层套上这个 Provider。
|
1 2 3 4 5 |
Provider( builder:(_) => ValidatorBLoC(), dispose:(_, ValidatorBLoC bloc) => bloc.dispose(), } ) |
我们在 dispose 回调中关闭不再使用的流,这样就完美解决了数据释放的问题!
现在我们可以放心的结合 BLoC 一起使用了,很赞有没有。但是现在你可能又有疑问了,在使用 Provider 的时候,我应该选择哪种构造方法呢。
我的推荐是,简单模型就选择 Provider<T>.value,好处是可以精确控制刷新时机。而需要对资源进行释放处理等复杂模型的时候,Provider()默认构造方式绝对是你的最佳选择。
其他几种 Provider 也遵循该模式,需要的时候可以自行查看源码。
4.2 我该使用哪种 Provider
如果你在 Provider 中提供了可监听对象(Listenable 或者 Stream)及其子类的话,那么你会得到下面这个异常警告。
你可以将本文中所使用到的 CounterModel 放入 Provider 进行提供(记得 hot restart 而不是 hot reload),那么你就能看到上面这个 FlutterError 了。
你也可以在 main 方法中通过下面这行代码来禁用此提示。
Provider.debugCheckInvalidValueType = null;
这是由于 Provider 只能提供恒定的数据,不能通知依赖它的子部件刷新。提示也说的很清楚了,假如你想使用一个会发生 change 的 Provider,请使用下面的 Provider。
- ListenableProvider
- ChangeNotifierProvider
- ValueListenableProvider
- StreamProvider
4.2.1 ListenableProvider / ChangeNotifierProvider
你可能会在这里产生一个疑问,不是说(Listenable 或者 Stream)才不行吗,为什么我们的 CounterModel 混入的是 ChangeNotifier 但是还是出现了这个 FlutterError 呢。
|
1 |
class ChangeNotifier implements Listenable |
我们再来看上面的这几个 Provider 有什么异同。先关注ListenableProvider / ChangeNotifierProvider 这两个类。
ListenableProvider 提供(provide)的对象是继承了 Listenable 抽象类的子类。由于无法混入,所以通过继承来获得 Listenable 的能力,同时必须实现其 addListener / removeListener 方法,手动管理收听者。显然,这样太过复杂,我们通常都不需要这样做。
而混入了 ChangeNotifier 的类自动帮我们实现了听众管理,所以 ListenableProvider 同样也可以接收混入了 ChangeNotifier 的类。
ChangeNotifierProvider 则更为简单,它能够对子节点提供一个 继承/混入/实现 了 ChangeNotifier 的类。通常我们只需要在 Model 中 with ChangeNotifier ,然后在需要刷新状态的时候调用 notifyListeners 即可。
那么 ChangeNotifierProvider 和 ListenableProvider 究竟区别在哪呢,ListenableProvider 不是也可以提供(provide)混入了 ChangeNotifier 的 Model 吗。
还是那个你需要思考的问题。你在这里的 Model 究竟是一个简单模型还是复杂模型。这是因为 ChangeNotifierProvider 会在你需要的时候,自动调用其 _disposer 方法。
|
1 |
static void _disposer(BuildContext context, ChangeNotifier notifier) => notifier?.dispose(); |
我们可以在 Model 中重写 ChangeNotifier 的 dispose 方法,来释放其资源。这对于复杂 Model 的情况下十分有用。
4.2.2 ValueListenableProvider
现在你应该已经十分清楚 ListenableProvider / ChangeNotifierProvider 的区别了。下面我们来看 ValueListenableProvider。
ValueListenableProvider 用于提供实现了 继承/混入/实现 了 ValueListenable 的 Model。它实际上是专门用于处理只有一个单一变化数据的 ChangeNotifier。
|
1 |
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> |
通过 ValueListenable 处理的类不再需要数据更新的时候调用notifyListeners。
好了,终于只剩下最后一个StreamProvider 了。
4.2.3 StreamProvider
StreamProvider 专门用作提供(provide)一条 Single Stream。我在这里仅对其核心属性进行讲解。
T initialData:你可以通过这个属性声明这条流的初始值。ErrorBuilder catchError:这个属性用来捕获流中的 error。在这条流 addError 了之后,你会能够通过T Function(BuildContext context, Object error)回调来处理这个异常数据。实际开发中它非常有用。updateShouldNotify:和之前的回调一样,这里不再赘述。
除了这三个构造方法都有的属性以外,StreamProvider 还有三种不同的构造方法。
StreamProvider(…):默认构造方法用作创建一个 Stream 并收听它。StreamProvider.controller(…):通过 builder 方式创建一个StreamController。并且在 StreamProvider 被移除时,自动释放 StreamController。StreamProvider.value(…):监听一个已有的 Stream 并将其 value 提供给子孙节点。
除了上面这五种已经提到过的 Provider,还有一种 FutureProvider,它提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新,这里不再详细介绍,需要的话自行查看 api 文档。
4.3 优雅地处理多个 Provider
在我们之前的例子中,我们使用了嵌套的方式来组合多个 Provider,但是这样看上去有些傻。这时候我们就可以使用一个非常 sweet 的组件 —— MultiProvider。
这时候我们刚才那个例子就可以改成这样。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void main() { final counter = CounterModel(); final textSize = 48; runApp( MultiProvider( providers: [ Provider.value(value: textSize), ChangeNotifierProvider.value(value: counter) ], child: MyApp(), ), ); } |
可以看到我们的代码意图清晰很多,而且与刚才的嵌套做法完全等价。
4.4 单元测试
直接执行原来的测试用例,一般会报错如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following ProviderNotFoundException was thrown building CountsWidget(dirty): Error: Could not find the correct Provider<Counter> above this CountsWidget Widget This happens because you used a `BuildContext` that does not include the provider of your choice. There are a few common scenarios: - You added a new provider in your `main.dart` and performed a hot-reload. To fix, perform a hot-restart. - The provider you are trying to read is in a different route. Providers are "scoped". So if you insert of provider inside a route, then other routes will not be able to access that provider. - You used a `BuildContext` that is an ancestor of the provider you are trying to read. Make sure that CountsWidget is under your MultiProvider/Provider<Counter>. This usually happens when you are creating a provider and trying to read it immediately. For example, instead of: ``` Widget build(BuildContext context) { return Provider<Example>( create: (_) => Example(), // Will throw a ProviderNotFoundError, because `context` is associated // to the widget that is the parent of `Provider<Example>` child: Text(context.watch<Example>().toString()), ); } ``` consider using `builder` like so: ``` Widget build(BuildContext context) { return Provider<Example>( create: (_) => Example(), // we use `builder` to obtain a new `BuildContext` that has access to the provider builder: (context, child) { // No longer throws return Text(context.watch<Example>().toString()); } ); } ``` If none of these solutions work, consider asking for help on StackOverflow: https://stackoverflow.com/questions/tagged/flutter |
修改后的测试用例如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:helloworld/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => CounterModel()), ], child: const MyApp())); // 由于国际化的引入,导致界面初始化延迟,需要让微代码都已经执行完成了,才能进行后续的测试。 await tester.pump(); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } |
五、Tips
5.1 保证 build 方法无副作用
Build 无副作用也通常被人叫做,build 保持 pure,二者是同一个意思。
通常我们经常会看到,为了获取顶层数据我们会在 build 方法中调用 XXX.of(context) 方法。你必须非常小心,你的 build 函数不应该产生任何副作用,包括新的对象(Widget 以外),请求网络,或作出一个映射视图以外的操作等。
这是因为,你的根本无法控制什么时候你的 build 函数将会被调用。(我可以说随时)每当你的 build 函数被调用,那么都会产生一个副作用。这将会发生非常恐怖的事情。
我这样说你肯定会感到比较抽象,我们来举一个例子。
假如你有一个 ArticleModel 这个 Model 的作用是 通过网络 获取一页 List 数据,并用 ListView 显示在页面上。
这时候,我们假设你在 build 函数中做了下面这些事情。
|
1 2 3 4 5 6 |
@override Widget build(BuildContext context) { final articleModel = Provider.of<ArticleModel>(context); mainCategoryModel.getPage(); // By requesting data from the server return XWidget(...); } |
我们在 build 函数中获得了祖先节点中的 articleModel,随后调用了 getPage 方法来获取第一页的数据。
这时候会发生什么事情呢?当我们请求成功获得了结果的时候,根据之前我们已经介绍过的,调用了 Provider.of<T>(context); 之后数据更改会重新运行其 build。这样 getPage 就又被执行了一次。
而你的 Model 中每次请求 getPage 都会导致 Model 中保存的当前请求页自增(第一次请求第一页的数据,第二次请求第二页的数据以此类推),那么每次 build 都会导致新的一次数据请求,并在新的数据 get 的时候请求下一页的数据。你的服务器挂掉那是迟早的事情。
所以你应该严格遵守这项原则,否则会导致一系列糟糕的后果。
那么怎么解决数据初始化这个问题呢,请看 Q&A 部分。
5.2 不要所有状态都放在全局
第二个小贴士是不要把你的所有状态都放在顶层。开发者为了图方便省事,再接触了状态管理之后经常喜欢把所有东西都放在顶层 MaterialApp 之上。这样看上去就很方便共享数据了,我要数据就直接去获取。
不要这么做。严格区分你的全局数据与局部数据,资源不用了就要释放!否则将会一定程度上影响你的应用性能。
5.3 尽量在 Model 中使用私有变量“_”
这可能是我们每个人在新手阶段都会出现的疑问。为什么要用私有变量呢,我在任何地方都能够操作成员不是很方便吗。
一个应用需要大量开发人员参与,你写的代码也许在几个月之后被另外一个开发看到了,这时候假如你的变量没有被保护的话,也许同样是让 count++,他会用 countController.sink.add(++_count) 这种原始方法,而不是调用你已经封装好了的 increment 方法。
虽然两种方式的效果完全一样,但是第一种方式将会让我们的业务逻辑零散的混入其他代码中。久而久之项目中就会大量充斥着这些垃圾代码增加项目代码耦合程度,非常不利于代码的维护以及阅读。
所以,请务必使用私有变量保护你的 Model。
5.4 控制你的刷新范围
在 Flutter 中,组合大于继承的特性随处可见。常见的 Widget 实际上都是由更小的 Widget 组合而成,直到基本组件为止。为了使我们的应用拥有更高的性能,控制 Widget 的刷新范围便显得至关重要。
我们已经通过前面的介绍了解到了,在 Provider 中获取 Model 的方式会影响刷新范围。所有,请尽量使用 Consumer 来获取祖先 Model,以维持最小刷新范围。
在不需要时刻监听状态变化的类中可以通过 Provider<T>.of(context, listen: false); 取消监听, 也是提升刷新效率的方式之一。
六、Q&A
在这里对一些大家可能会有疑问的常见问题做一个回答,如果你还有这之外的疑问的话,欢迎在下方评论区一起讨论。
6.1 Provider 是如何做到状态共享的
这个问题实际上得分两步。
6.1.1 获取顶层数据
实际上在祖先节点中共享数据都是通过系统的 InheritedWidget 进行实现的。
Provider 也不例外,在所有 Provider 的 build 方法中,返回了一个 InheritedProvider。
|
1 |
class InheritedProvider<T> extends InheritedWidget |
Flutter 通过在每个 Element 上维护一个 InheritedWidget 哈希表来向下传递 Element 树中的信息。通常情况下,多个
Element 引用相同的哈希表,并且该表仅在 Element 引入新的InheritedWidget 时改变。
所以寻找祖先节点的时间复杂度为 O(1) !
6.1.2 通知刷新
通知刷新这一步实际上在讲各种 Provider 的时候已经讲过了,其实就是使用了 Listener 模式。Model 中维护了一堆听众,每次调用Provider.of(context)的时候会进行注册 ,然后 notifiedListener 通知所有听众刷新。
6.2 为什么全局状态需要放在顶层 MaterialApp 之上
这个问题需要结合 Navigator 以及 BuildContext 来回答。由于 Flutter 本质上是一个单页面应用程序,所以必须放在 Navigator 的 Element 之上才能够在全局共享数据。
6.3 我应该在哪里进行数据初始化
对于数据初始化这个问题,我简单将其分为全局数据初始化与单页面数据初始化两种情况。
6.3.1 全局数据
当我们需要获取全局顶层数据(就像之前 CounterApp 例子一样)并需要做一些会产生额外结果的时候,main 函数是一个很好的选择。
我们可以在 main 方法中创建 Model 并进行初始化的工作,这样就只会执行一次。
6.3.2 单页面
如果我们的数据只是在这个页面中需要使用,那么你有这两种方式可以选择。
(1)StatefulWidget
第一种是页面级别还是使用 StatefulWidget,然后在其 State 的 didChangeDependence 生命周期中,做这些会产生额外结果的动作的事。由于 State 是长声明周期对象,在其存在期间,didChangeDependence 只会在创建的时候执行一次。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class FirstScreen extends StatefulWidget {···} class _FirstScreenState extends State<FirstScreen> { CounterModel _counter; double _textSize; @override void didChangeDependencies() { super.didChangeDependencies(); _counter = Provider.of<CounterModel>(context); _textSize = Provider.of<int>(context).toDouble(); _counter.increment(); } ... } |
(2)cascade
你也可以在使用 dart 的级连语法 ..do() 直接在页面的 StatelessWidget 成员变量声明时进行初始化。
|
1 2 3 4 5 |
class FirstScreen extends StatelessWidget { CounterModel _counter = CounterModel()..increment(); double _textSize = 48; ... } |
使用这种方式需要注意,当这个 StatelessWidget 重新运行 build 的时候,状态会丢失。这种情况在 TabBarView 中的子页面切换过程中就可能会出现。
所以建议还是使用第一种,在 State 中初始化数据。
6.4 我需要担心性能问题吗
是的,你需要随时注意应用性能是否会因为一些不当操作而降低。虽然 Flutter 可以在不做大量优化的情况下媲美原生应用的体验。然而当我们不遵守其行为规范的时候,会出现这样的情况。性能会因为你的各种不当操作而变得很糟糕。
然而 Provider 仅仅是对 InheritedWidget 的一个升级,你不必担心引入 Provider 会对应用造成性能问题。但是在使用过程中我有下面三个建议,以避免进入性能陷阱:
- 控制 Widget 刷新范围;
- 保持 build 方法 pure;
- 必要时,通过重写
UpdateShouldNotify进行性能优化。
6.5 为什么选择 Provider
Provider 不仅做到了提供数据,而且它拥有着一套完整的解决方案,覆盖了你会遇到的绝大多数情况。就连 BLoC 未解决的那个棘手的 dispose 问题,和 ScopedModel 的侵入性问题,它也都解决了。
然而它就是完美的吗,并不是,至少现在来说。Flutter Widget 构建模式很容易在 UI 层面上组件化,但是仅仅使用 Provider,Model 和 View 之间还是容易产生依赖。我们只有通过手动将 Model 转化为 ViewModel 这样才能消除掉依赖关系,所以假如各位有组件化的需求,还需要另外处理。
不过对于大多数情况来说,Provider 足以优秀,它能够让你开发出 简单、高性能、层次清晰、高可扩展性 的应用。
6.6 我应该如何选择状态管理
介绍了这么多状态管理,你可能会发现,一些状态管理之间职责并不冲突。例如 BLoC 可以结合 RxDart 库变得很强大,很好用。而 BLoC 也可以结合 Provider / ScopedModel 一起使用。那我应该选择哪种状态管理方式呢。
我的看法是,没有最好的,只有最合适的。根据你的业务来选择最合适的状态管理方式,面对不同复杂度的业务,往往会得出完全不同的结论。
我建议遵守以下几点:
1. 使用状态管理的目的是为了让编写代码变得更简单,任何会增加你的应用复杂度的状态管理,统统都不要用。
2. 选择自己能够 hold 住的,BLoC / Rxdart / Redux / Fish-Redux 这些状态管理方式都有一定上手难度,不要选自己无法理解的状态管理方式。
3. 在做最终决定之前,敲一敲 demo,真正感受各个状态管理方式给你带来的 好处/坏处 然后再做你的决定。
希望能够帮助到你。
七、源码浅析
7.1 Flutter 中的 Builder 模式
在 Provider 中,各种 Provider 的原始构造方法都有一个 builder 参数,这里一般就用 (_) => XXXModel() 就行了。感觉有点多此一举,为什么不能像 .value() 构造方法那样简洁呢。
实际上,Provider 为了帮我们管理 Model,使用到了 delegation pattern。
builder 声明的 ValueBuilder 最终被传入代理类 BuilderStateDelegate /SingleValueDelegate。 然后通过代理类才实现的 Model 声明周期管理。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class BuilderStateDelegate<T> extends ValueStateDelegate<T> { BuilderStateDelegate(this._builder, {Disposer<T> dispose}) : assert(_builder != null), _dispose = dispose; final ValueBuilder<T> _builder; final Disposer<T> _dispose; T _value; @override T get value => _value; @override void initDelegate() { super.initDelegate(); _value = _builder(context); } @override void didUpdateDelegate(BuilderStateDelegate<T> old) { super.didUpdateDelegate(old); _value = old.value; } @override void dispose() { _dispose?.call(context, value); super.dispose(); } } |
这里就仅放 BuilderStateDelegate,其余的请自行查看源码。
7.2 如何实现 MultiProvider
|
1 2 3 4 5 6 7 |
Widget build(BuildContext context) { var tree = child; for (final provider in providers.reversed) { tree = provider.cloneWithChild(tree); } return tree; } |
MultiProvider 实际上就是通过每一个 provider 都实现了的 cloneWithChild 方法,用循环把自己一层一层包裹起来。
|
1 2 3 4 5 6 7 8 |
MultiProvider( providers:[ AProvider, BProvider, CProvider, ], child: child, ) |
等价于:
|
1 2 3 4 5 6 7 |
AProvider( child: BProvider( child: CProvider( child: child, ), ), ) |
以上,是本人在状态管理方面遇到的心得的总结,希望能够给各位提供参考。
参考链接
如何逆向Flutter应用(反编译)
目前大多数使用Flutter的应用都是采用add2app的方式,在APP中与Flutter相关的内容主要有FlutterEngine、APP产物、资源文件。我们可以在应用市场上寻找一个接入Flutter的应用做实验。(apk可在各大应用市场下载,ipa下载可以在mac上安装Apple Configurator 2进行),apk和ipa中flutter相关产物目录如下:
iOS包文件为ipa,下载后将其后缀重命名为zip进行解压,解压后Payload下即可看到应用文件夹,其中FlutterEngine、APP产物、资源文件分别在如下位置:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
xxx.app └── Frameworks ├── App.framework │ ├── App(Dart APP产物) │ ├── Info.plist │ ├── SC_Info │ ├── _CodeSignature │ └── flutter_assets │ ├── flutter_assets │ ├── AssetManifest.json │ ├── FontManifest.json │ ├── LICENSE │ ├── fonts │ ├── images │ ├── mtf_module_info │ └── packages └── Flutter.framework ├── Flutter(FlutterEngine) ├── Info.plist ├── SC_Info ├── _CodeSignature └── icudtl.dat |
Android包文件为apk,下载后将其后缀重命名为zip进行解压,其中FlutterEngine、APP产物、资源文件分别在如下位置:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
xxx.apk ├── assets │ └── flutter_assets │ └── flutter_assets │ ├── AssetManifest.json │ ├── FontManifest.json │ ├── LICENSE │ ├── fonts │ ├── images │ ├── mtf_module_info │ └── packages └── lib └── armeabi ├── libapp.o(Dart APP产物) └── libflutter.so(FlutterEngine) |
FlutterEngine各个app都是使用官方或者在官方基础上进行修改,差别不多,我们暂不关心这部分的逆向。资源文件多是图片,字体等无需逆向即可查看的资源。我们主要关心的是使用Dart编写的业务逻辑或者某些框架代码,这部分代码在APP产物中。即:App.framework/App 或 armeabi/libapp.o这两个文件都是动态库,我们先简单看看里面包含什么?
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# 可以安装常用的 bin utils工具,如 brew update && brew install binutils ~/Downloads > objdump -t App App: 文件格式 mach-o-arm64 SYMBOL TABLE: 0000000001697e60 g 0f SECT 02 0000 [.const] _kDartIsolateSnapshotData 000000000000b000 g 0f SECT 01 0000 [.text] _kDartIsolateSnapshotInstructions 0000000001690440 g 0f SECT 02 0000 [.const] _kDartVmSnapshotData 0000000000006000 g 0f SECT 01 0000 [.text] _kDartVmSnapshotInstructions ~/Downloads > greadelf -s libapp.so Symbol table '.dynsym' contains 5 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00001000 12992 FUNC GLOBAL DEFAULT 1 _kDartVmSnapshot[...] 2: 00005000 0x127df60 FUNC GLOBAL DEFAULT 2 _kDartIsolateSna[...] 3: 01283000 22720 OBJECT GLOBAL DEFAULT 3 _kDartVmSnapshotData 4: 01289000 0x9fc858 OBJECT GLOBAL DEFAULT 4 _kDartIsolateSna[...] |
可以看到无论是Android还是iOS,Dart App产物中都包含4个程序段。(来自https://github.com/flutter/flutter/wiki/Flutter-engine-operation-in-AOT-Mode)
-
'_kDartVmSnapshotData': 代表 isolate 之间共享的 Dart 堆 (heap) 的初始状态。有助于更快地启动 Dart isolate,但不包含任何 isolate 专属的信息。
-
'_kDartVmSnapshotInstructions':包含 VM 中所有 Dart isolate 之间共享的通用例程的 AOT 指令。这种快照的体积通常非常小,并且大多会包含程序桩 (stub)。
-
'_kDartIsolateSnapshotData':代表 Dart 堆的初始状态,并包含 isolate 专属的信息。
-
'_kDartIsolateSnapshotInstructions':包含由 Dart isolate 执行的 AOT 代码。
看了上面可能还是一脸懵o((⊙﹏⊙))o,为什么分四块,Data与Instructions,Vm与Isolate是什么?为什么使用Snapshot(快照)命名。关于这些问题,推荐一篇博客https://mrale.ph/dartvm/ 。Data与Instructions,Vm与Isolate这些概念两两组合,正好对应上面四个段。也就是VmData、VmInstructions、IsolateData、IsolateInstructions。
先说一下Data与Instructions。首先我们知道的是Flutter编译运行在app上分为JIT和AOT模式,线上只能使用AOT模式,也就是Flutter引入的DartVM包含了执行AOT产物的功能。为了与JIT模式兼容,DartVM采用了所谓快照的方式,即JIT运行时编译后的基本结构与AOT编译的基本结构相同。将类信息、全局变量、函数指令直接以序列化的方式存在磁盘中,称为Snapshot(快照)。

由于快照的序列化格式针对性的为读取速率做了设计,从快照读取也大大提高代码的加载速度(创建所需类信息、全局数据等,可以类比OC Runtime启动加载元类、类信息等)。最开始快照中是不包含机器代码的(即函数体内部的执行逻辑),后来随着AOT模式的开发这部分被加入到快照中了,这些后来者也就是前面说的Instructions。

这里要补充的是,Instructions指的是可执行汇编指令,在.o文件中必须放在text段里,标记为可执行(否则iOS无法加载并执行它)。类信息、全局变量这些内容可以放在data端作为普通数据被加载。(字节的优化50%包体积也是基于此,有兴趣可以看一下文章:https://juejin.im/post/6844904014170030087)。
接着说DartVmSnapshot 与DartIsolateSnapshot。这就涉及Data虚拟机是如何运行业务代码。虚拟是Data代码运行的载体,VM中运行的逻辑都跑在一个抽象的叫做Isolate(隔离)的实体中。你可以把Isolate当做OC里一个带有Runloop的Thread看待(至于他们之间的关系又是一个令人头疼的面试题,这里不展开了)。简要来说Isolate中维护了堆栈变量,函数调用栈帧,用于GC、JIT等辅助任务的子线程等, 而这里的堆栈变量就是要被序列化到磁盘上的东西,即IsolateSnapshot。此外像dart预置的全局对象,比如null,true,false等等等是由VMIsolate管理的,这些东西需序列化后即VmSnapshot。

到这里大致了解Flutter APP产物中的结构。那如何读取他们呢?我们可以从clustered_snapshot.cc中的FullSnapshotReader:: 函数看起,看他是如何反序列化的。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void Deserializer::ReadIsolateSnapshot(ObjectStore* object_store) { Array& refs = Array::Handle(); Prepare(); { NoSafepointScope no_safepoint; HeapLocker hl(thread(), heap_->old_space()); // N.B.: Skipping index 0 because ref 0 is illegal. const Array& base_objects = Object::vm_isolate_snapshot_object_table(); for (intptr_t i = 1; i < base_objects.Length(); i++) { AddBaseObject(base_objects.At(i)); } Deserialize(); // Read roots. RawObject** from = object_store->from(); RawObject** to = object_store->to_snapshot(kind_); for (RawObject** p = from; p <= to; p++) { *p = ReadRef(); } #if defined(DEBUG) int32_t section_marker = Read<int32_t>(); ASSERT(section_marker == kSectionMarker); #endif refs = refs_; refs_ = NULL; } thread()->isolate()->class_table()->CopySizesFromClassObjects(); heap_->old_space()->EvaluateSnapshotLoad(); #if defined(DEBUG) Isolate* isolate = thread()->isolate(); isolate->ValidateClassTable(); isolate->heap()->Verify(); #endif for (intptr_t i = 0; i < num_clusters_; i++) { clusters_[i]->PostLoad(refs, kind_, zone_); } // Setup native resolver for bootstrap impl. Bootstrap::SetupNativeResolver(); } |
要看懂这部分也是十分费力,另一个大神的分析文章可能会为我们带来很多启示:https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/
我们要看如何读取RawObject对象

每个对象均以包含以下标记的uint32_t开头:

原则上我们自己可以写一个读取的程序进行分析,但是网上有一个使用Python写好的读取程序(只支持读取ELF格式文件,也就是只支持Android包产物的分析):https://github.com/hdw09/darter 基于这个读取工具提供的API我们可以写一个导出应用所有类定义的工具。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
from darter.file import parse_elf_snapshot, parse_appjit_snapshot from darter.asm.base import populate_native_references import re from collections import defaultdict import os import shutil def get_funciont(fun_index, s, span=False): spanStr = '' if span: spanStr = ' ' fun_str = '\n'+spanStr+'// 函数索引:' + '{0}'.format(fun_index)+'\n' returnTypeStr = '' if '_class' in s.refs[fun_index].x['result_type'].x.keys(): returnTypeStr = s.refs[fun_index].x['result_type'].x['_class'].x['name'].x['value'] elif 'name' in s.refs[fun_index].x['result_type'].x.keys(): returnTypeStr = str(s.refs[fun_index].x['result_type']) else: returnTypeStr = s.refs[fun_index].x['result_type'].x['value'] fun_str = fun_str+spanStr + returnTypeStr fun_str = fun_str + ' ' + s.refs[fun_index].x['name'].x['value']+'(' parameterCount = 0 if type(s.refs[fun_index].x['parameter_types'].x['value']) != type(''): for parameterName in s.refs[fun_index].x['parameter_names'].x['value']: parType = '' if '_class' in s.refs[fun_index].x['parameter_types'].x['value'][parameterCount].x.keys(): parType = s.refs[fun_index].x['parameter_types'].x['value'][parameterCount].x['_class'].x['name'].x['value'] else: parType = s.refs[fun_index].x['parameter_types'].x['value'][parameterCount].x['value'] fun_str = fun_str + parType + ' ' fun_str = fun_str + parameterName.x['value'] + ', ' parameterCount = parameterCount + 1 fun_str = fun_str + ') \n'+spanStr+'{ \n' for nrefsItem in s.refs[fun_index].x['code'].x['nrefs']: fun_str = fun_str + spanStr + ' {0}'.format(nrefsItem) + '\n' fun_str = fun_str + spanStr+'}' return fun_str def get_classDis(clas_index, s): class_str = '\n// 类索引:' + '{0}'.format(clas_index)+' 使用s.refs[xxxx].x跟查\n' superName = '' if '_class' in s.refs[clas_index].x['super_type'].x.keys(): superName = s.refs[clas_index].x['super_type'].x['_class'].x['name'].x['value'] else: superName = s.refs[clas_index].x['super_type'].x['value'] class_str = class_str + \ 'class {0} : {1} {2}\n'.format( s.refs[clas_index].x['name'].x['value'], superName, '{') if type(s.refs[clas_index].x['functions'].x['value']) != type(''): for fun in s.refs[clas_index].x['functions'].x['value']: class_str = class_str+'\n'+get_funciont(fun.ref, s, True) return class_str+'\n\n}' def get_lob_class(lib, s): all_class = '' for item in lib.src: if 'name' in item[0].x.keys(): all_class = all_class + get_classDis(item[0].ref, s) + '\n' if '类索引' in all_class: return all_class else: return '没有获得任何信息' def show_lob_class(lib, s): print(get_lob_class(lib, s)) def writeStringInPackageFile(packageFile, content): packageFile = packageFile.replace('dart:', 'package:dart/') filename = packageFile.replace('package:', 'out/') filePath = filename[0:filename.rfind('/')] content = '// {0} \n'.format(packageFile)+content if os.path.exists(filePath) == False: os.makedirs(filePath) file = open(filename, 'w') file.write(content) file.close() def getFiles(elfFile, filter): s = parse_elf_snapshot(elfFile) populate_native_references(s) allLibrary = sorted(s.getrefs('Library'), key=lambda x: x.x['url'].x['value']) for tempLibrary in allLibrary: name = tempLibrary.x['url'].x['value'] if filter in name: print(name + '开始生成....') writeStringInPackageFile( name, get_lob_class(s.strings[name].src[1][0], s)) print(name + '生成成功✅') # 开始执行 getFiles('samples/arm-app.so', '') |
这个脚本最终会提取所有指定文件的源码,其中对友商app其中一个类的导出结果如下:

其中标注了类对象 与函数的索引,可以在控制台使用s.refs[xxxxx].x继续跟查。
参考链接
Dart Mixin介绍
关于 Dart mixin 的一些理解。理解 mixin 概念的关键在于理解中间类。
Mixins are a way of reusing code in multiple class hierarchies
先来看一个简单例子:
|
1 2 3 4 5 6 7 8 9 10 |
class Piloted { int astronauts = 1; void describeCrew() { print('Number of astronauts: $astronauts'); } } class PilotedCraft extends Spacecraft with Piloted { // ··· } |
PilotedCraft 拥有 astronauts 字段和 describeCrew() 方法。
mixin 是什么?
维基百科中这样定义 mixin:
In object-oriented programming languages, a Mixin is a class that contains methods for use by other classes without having to be the parent class of those other classes.
即,mixin 是另外一个普通类,我们可以在不继承这个类的情况下从这个类”借用”方法和变量。
Support for the mixin keyword was introduced in Dart 2.1. Code in earlier releases usually used abstract class instead.
从这个角度来讲,mixin 不过是 abstract class。
Java tries to make up for this by using Interfaces, but that is not as useful or flexible as mixins.
从这个角度来讲,可以认为 mixin 是带实现的接口。
小节
- mixin 有点类似
abstract class - mixin 有点类似
interface - 不能继承 mixin
- 可以使用
mixin,abstract class,class来作为 mixin
如何使用 mixin?
使用 mixin 的方法很简单:with 关键字后面跟上 mixin 的名字即可。
|
1 2 3 4 5 6 7 8 9 10 11 |
class Musician extends Performer with Musical { // ··· } class Maestro extends Person with Musical, Aggressive, Demented { Maestro(String maestroName) { name = maestroName; canConduct = true; } } |
实现 mixin 的方法同样也很简单:创建一个继承自 Object 的类并且不要声明构造方法。如果想让 mixin 作为普通类使用,使用 class 关键字;如果不想让 mixin 作为普通类使用,使用 mixin 关键字代替 class。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
mixin Musical { bool canPlayPiano = false; bool canCompose = false; bool canConduct = false; void entertainMe() { if (canPlayPiano) { print('Playing piano'); } else if (canConduct) { print('Waving hands'); } else { print('Humming to self'); } } } |
on 的用法
The keyword on is used to restrict our mixin’s use to only classes which either extends or implements the class it is declared on. In order to use the on keyword, you must declare your mixin using the mixin keyword
|
1 2 3 4 5 6 7 |
class B {} mixin Y on B { void hi() { print('hi'); } } class Q with Y {} |
则有如下错误提示:
Error: 'Object' doesn't implement 'B' so it can't be used with 'Y'.
on 关键字限制了 Y 的使用范围:Y 只能用于继承或实现了 B 的类。修复方式是让 Q 继承自 B:
|
1 |
class Q extends B with Y {} |
mixin 解决了什么问题?
mixin 解决了多重继承中的 Deadly Diamond of Death(DDD) 问题。
多重继承问题简单描述。各个类的继承关系如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Performer { abstract void perform(); } class Dancer extends Performer { void perform() {} } class Singer extends Performer { void perform() {} } class Musician extends Dancer, Singer { } |
问题来了,当调用 Musician.perform() 时,到底会调用哪个 perform() 方法是模糊的。
来看 mixin 如何解决这个问题。见 Dart for Flutter : Mixins in Dart - Flutter Community - Medium
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Performer { abstract void perform(); } mixin Dancer { void perform() {} } mixin Singer { void perform() {} } class Musician extends Performer with Dancer, Singer { } |
现在,当调用 Musician.perform() 时,到底会调用哪个 perform() 方法是确定的。在这里是调用 Singer.perform()。
mixin 有一套明确的机制来选择调用哪个方法。
假设 Musician 类使用多个 mixin (Dancer, Singer)。该类有个方法名为 perform(),Musician 类继承自 Performer 类。
- 首先,将
Performer类置于栈顶 - 其次,后声明的 mixin 优先于先声明的 mixin。按顺序将 mixin 置于栈中,在这里分别是
Dancer,Singer - 最后,将
Musician类自己置于栈中。Musician类中的perform()被优先调用
Dart 使用的是单重继承 (Java 也是单重继承,C++ 是多重继承)。多重继承更为强大,但会引起 Deadly Diamond of Death(DDD) 问题。
Java 使用接口(interface)来部分实现多重继承。多重继承的问题是需要在每个类中实现接口(interface),所以并不是一个好的方案。(实际上 Java 已经通过默认方法修复了这个问题)
所以 Dart 中就有了 mixin。
理解 mixin
Mixins in Dart work by creating a new class that layers the implementation of the mixin on top of a superclass to create a new class — it is not “on the side” but “on top” of the superclass, so there is no ambiguity in how to resolve lookups.
Mixins is not a way to get multiple inheritance in the classical sense. Mixins is a way to abstract and reuse a family of operations and state. It is similar to the reuse you get from extending a class, but it is compatible with single-inheritance because it is linear.
StackOverflow
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class A { String getMessage() => 'A'; } class B { String getMessage() => 'B'; } class P { String getMessage() => 'P'; } class AB extends P with A, B {} class BA extends P with B, A {} void main() { String result = ''; AB ab = AB(); result += ab.getMessage(); BA ba = BA(); result += ba.getMessage(); print(result); } |
以上这段代码输出 BA。
从语义上讲以上这段代码等同于:
|
1 2 3 4 5 6 7 8 9 |
class PA = P with A; class PAB = PA with B; class AB extends PAB {} class PB = P with B; class PBA = PB with A; class BA extends PBA {} |
继承结构图是这样的:
Since each mixin application creates a new class, it also creates a new interface (because all Dart classes also define interfaces). As described, the new class extends the superclass and includes copies of the mixin class members, but it also implements the mixin class interface.
In most cases, there is no way to refer to that mixin-application class or its interface; the class for Super with Mixin is just an anonymous superclass of the class declared like class C extends Super with Mixin {}. If you name a mixin application like class CSuper = Super with Mixin {}, then you can refer to the mixin application class and its interface, and it will be a sub-type of both Super and Mixin.
理解 mixin 的关键在于它是线性的。
使用场景
- 在没有共同父类的各个类中共享代码时
- 在父类中实现某种方法无意义时
在 Dart 语言中,我们经常可以看到对 mixin 关键字的使用,根据字面理解,就是混合的意思。那么,mixin 如何使用,它的使用场景是什么呢。


























