前语
这是笔者作为一个Android
工程师入门Flutter
的学习笔记,笔者不想经过一种循规蹈矩的办法来学习:先学Dart
言语,然后学习Flutter
的基本运用,再到实践运用这样的步骤。这样的办法有点无趣且效率较低。
笔者觉得关于已经有Android
根底的来说,经过类比Android
的办法来学习Flutter
,掌握核心根底概念后,直接开发实践运用,在这个过程中去学习其间的知识比如Dart
语法、深入的知识点。这是笔者的一次学习尝试,并将其记录下来:
本篇是该系列的第一篇,主要内容是:
(1)视图在 Flutter
中对应什么概念?怎么布局Widget?
(2)Android
中的Intent
在 Flutter
中的对应什么?
(3) Flutter
中怎么在页面间导航?与Activity
层的数据怎么传递?
(4) Flutter
中怎么完成网络恳求和数据处理?
视图
Android
中的 View
是显示在屏幕上的一切的根底。常见的控件比如按钮、东西栏、输入框都是 View
。
而 Flutter
中有个概念叫 Widget
,它是Flutter
中声明和构建 UI 的办法,能够粗略比照成 Android 中的 View,但 Widget
并非完全对应于 Android
中的 View
,它们是有差异的:
widget
有着不相同的生命周期:它们是不可变的,一旦需求变化则生命周期停止。任何时候widget
或它们的状况变化时,Flutter
结构都会创立一个新的 widget 树的实例
而Android
中的View
一般状况下只会制作一次,除非调用 invalidate
才会重绘。
Flutter
的widget
很轻量,部分原因在于它们的不可变性。由于它们自身既非视图,也不会直接制作任何内容,而是 UI 及其底层创立真正视图目标语义的描述。
Widget状况
在Android
中,你能够直接操作更新View
。然而在Flutter
中,Widget
是不可变的,无法被直接更新,你需求操作 Widget
的状况。有两种状况的Widget
:
- StatelessWidget(无状况): 没有状况信息的 Widget,用于描述用户界面的一部分,不依赖于除了目标中的装备信息以外的任何东西的场景,类似
Android
中 一个展示图标的ImageView
,整个过程是不会变的。
- StatefulWidget(有状况):比如依据
HTTP
恳求回来的数据或许用户的交互来动态地更新界面,那么你就必须运用StatefulWidget
,并告诉Flutter
结构Widget
的“状况(
State) 更新了,以便
Flutter能够更新这个
Widget`。
无状况Widget和有状况Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于
StatefulWidget
有一个跨帧存储和恢复状况数据的State
目标。
比如 Text
Widget 就是一个一般的 StatelessWidget
, 它没有相关联的状况信息,仅仅渲染传入结构器的信息,所以内部的数据是没办法更新的。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
如果想动态更新内部的文本就需求凭借StatefulWidget
,将 Text Widget
嵌入一个 StatefulWidget
中,例如下面的Scaffold
是StatefulWidget
:
class _SampleAppPageState extends State<SampleAppPage> {
String textToShow = 'I Try Learn Flutter';
void updateText(){
setState(() {
textToShow = "I Like Flutter!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold( // Scaffold 是 StatefulWidget
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: updateText,
tooltip: 'Update Text',
child: const Icon(Icons.update),
),
);
}
}
Widget布局
在 Android
中,经过XML
文件界说布局,可是在Flutter
中,要经过一个 widget
树来界说布局的
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.only(left: 20.0, right: 30.0),
),
onPressed: () {},
child: const Text('Hello'),
),
),
);
}
怎么添加/删去一个Widget?
在 Android
中,你经过调用父 View
的 addChild()
或 removeChild()
办法动态地添加或许删去子 View
。
在Flutter
中,由于Widget
是不可变的,所以没有类似 addChild()
这样的办法。
不过,我们能够给回来一个 Widget
的父Widget
传入一个办法,并经过布尔标记值操控子Widget
的创立。
举个例子:点击一个 FloatingActionButton 时在两个 widget 之间切换
class _SampleAppPageState extends State<SampleAppPage> {
bool toggle = true;
int a = 1;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return const Text("Toggle One");
} else {
a++;
return ElevatedButton(onPressed: () {}, child: Text('Toggle $a'));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample App')),
body: Center(child: _getToggleChild()),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Widget',
child: const Icon(Icons.update)),
);
}
}
Widget动画
Android
既能够经过XML
文件界说动画,也能够调用View
目标的 animate()
办法。
在 Flutter
里,则运用动画库,经过将 Widget
嵌入一个动画 Widget
的办法完成 Widget
的动画作用。
AnimationController
是个特殊的 Animation
目标,每当硬件准备新帧时,他都会生成一个新值。默认状况下,AnimationController
在给定期间内会线性生成从 0.0 到 1.0 的数字。运用 .forward()
办法启动动画。
创立 AnimationController
的一起,也赋予了一个 vsync
参数。 vsync
的存在避免后台动画耗费不必要的资源。您能够经过添加 SingleTickerProviderStateMixin
或许TickerProviderStateMixin
到类界说,将有状况的目标用作 vsync
。
SingleTickerProviderStateMixin
只适用于单个AnimationController
的状况,如需运用多个AnimationController
,请运用TickerProviderStateMixin
举个例子:完成一个淡出的动画
class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
late AnimationController controller;
late CurvedAnimation curvedAnimation;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 2000));
curvedAnimation =
CurvedAnimation(parent: (controller), curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: FadeTransition(
opacity: curvedAnimation,
child: const FlutterLogo(size: 100.0),
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade Animation',
onPressed: () {
controller.forward();
},
child: const Icon(Icons.brush),
),
);
}
}
上述代码解说:
1.with作用:Dart 支持 Mixin ,而 Mixin 能够更好的解决 多承继 中简略出现的问题, 如: 办法优先顺序紊乱、参数冲突、类结构变得复杂化等等。定论上简略来说,就是相同办法被覆盖了,而且 with 后边的会覆盖前面的。
2.TickerProviderStateMixin 作用:运用Animation controller时,需求在操控器初始化时传递一个vsync参数,此时需求用到TickerProvider SingleTickerProviderStateMixin只适用于单个AnimationController的状况,如需运用多个AnimationController,请运用TickerProviderStateMixin
3.CurvedAnimation: 为非线性曲线
4.override initState:覆盖此办法以履行初始化,这取决于此目标插入树中的位置(即 [context])或用于装备此目标的小部件(即 [widget])。
Canvas进行制作
在Android
中,你能够运用 Canvas
和 Drawable
将图片和形状制作到屏幕上。
Flutter
也有一个类似于 Canvas
的 API,由于它基于相同的底层渲染引擎Skia
。
Flutter
有两个帮助你用画布 (canvas) 进行制作的类: CustomPaint
和 CustomPainter
,后者能够完成自界说的制作算法。
举个例子:完成一个手写笔迹功用
/// ..的作用:级联运算符 (.. or ?..) 能够让你在同一个目标上连续调用多个目标的变量或办法。
class SignatureState extends State<Signature> {
List<Offset?> _points = <Offset>[];
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) {
setState(() {
RenderBox? renderBox = context.findRenderObject() as RenderBox;
Offset localPosition =
renderBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (details) => _points.add(null),
child: CustomPaint(
painter: SignaturePainter(_points), size: Size.infinite));
}
}
/// 自界说制作算法,完成手写笔迹
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List<Offset?> points;
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
}
}
/// 如果新实例表示与旧实例不同的信息,则该办法应回来 true,不然应回来 false
@override
bool shouldRepaint(SignaturePainter oldDelegate) =>
oldDelegate.points != points;
}
自界说 Widget
在 Android
中,一般经过承继 View
类,或许运用已有的视图类,再重载或完成以到达特定作用的办法。
在 Flutter
中,经过 组合 更小的 Widget 来创立自界说 Widget(而不是承继它们)。
举个例子:自界说一个带标签的按钮
经过组合 ElevatedButton
和一个标签来创立自界说按钮,而不是承继 ElevatedButton
:
class CustomButton extends StatelessWidget {
final String label;
const CustomButton(this.label, {super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(label),
);
}
}
之后就能够和其他Widget
相同运用了:
@override
Widget build(BuildContext context) {
return const Center(
child: CustomButton('自界说Widget'),
);
}
目的(Intent)
在Android
中,Intent
主要有两个运用场景:在Activity
之间进行导航,以及组件间通信。
Flutter
实际上并没有Activity
和Fragment
的对应概念。在Flutter
中你需求运用 Navigator
和 Route
在同一个 Activity
内的不同界面间进行跳转。
Navigator和Route
Route
是运用内屏幕和页面的笼统,Navigator
是管理路径route
的东西。一个 route
目标大致对应于一个 Activity
,可是它的意义是不相同的。 Navigator 能够经过对route
进行压栈和弹栈操作完成页面的跳转。
Navigator
的工作原理和栈相似,你能够将想要跳转到的 route
压栈 (push办法),想要回来的时候将route
出栈 (pop办法)
在 Flutter
中,你有多种不同的办法在页面间导航:
- 界说一个
route
姓名的Map
(MaterialApp) - 直接导航到一个
route
(WidgetApp)
Flutter接纳原生Activity的数据
在Android
原生层面(在我们的 Activity
中)处理共享的文本数据,然后Flutter
再经过运用 MethodChannel
获取这个数据。
Android
端 Activity
:configureFlutterEngine
里经过 call 在办法名getSharedText
里处理共享的数据,再经过 result
回掉给最终结果
class MainActivity : FlutterActivity() {
companion object {
private const val CHANNEL = "app.channel.shared.data"
}
...
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.contentEquals("getSharedText")) {
result.success(sharedText); //
sharedText = null;
}
}
);
}
}
Flutter
运用一个渠道通道恳求数据,数据便会从原生端发送过来:
class _SampleAppPageState extends State<SampleAppPage> {
static const platform = MethodChannel('app.channel.shared.data');
String dataShared = 'No data';
@override
void initState() {
super.initState();
getSharedText();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text(dataShared)));
}
// 调用Activity的共享数据办法
Future<void> getSharedText() async {
var sharedData = await platform.invokeMethod('getSharedText');
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
}
遇到的问题:Unresolved reference: FlutterActivity
异步UI
Dart
有一个单线程履行的模型,Dart
的单线程模型并不意味着你需求以会导致 UI 冻住的堵塞操作的办法来运转一切代码。
能够运用 Dart 言语供给的异步东西,例如 async/await
来履行异步任务,用 await
润饰的网络操作完成,再调用 setState()
更新 UI,就会触发 widget
子树的重建并更新数据。
Future<void> loadData() async {
var dataURL = Uri.parse('https://jsonplaceholder.typicode.com/posts');
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
Isolate
有时候你可能需求处理许多的数据并挂起你的 UI。在Flutter
中,能够经过运用 Isolate
来运用多核处理器的优势履行耗时或计算密布的任务。
Dart
一起也支持 Isolate
(在另一个线程运转 Dart 代码的办法),它是一个事情循环和异步编程办法。除非你创立一个 Isolate
,不然你的Dart
代码会运转在主 UI 线程,并被一个事情循环所驱动。Flutter
的事情循环对应于Android
里的主 Looper
—即绑定到主线程上的 Looper
。
Isolate
之间通讯的办法:port 端口,能够很便利的完成Isolate
之间的双向通讯,原理是向对方的队列里写入任务
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
widgets = msg;
});
}
static Future<void> dataLoader(SendPort sendPort) async {
// 打开ReceivePort接纳音讯
ReceivePort port = ReceivePort();
// 告诉任何其他这个 isolate 监听的端口。
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
replyTo.send(jsonDecode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
网络数据处理
尽管http
包没有 OkHttp
中的一切功用,可是它笼统了许多一般你会自己完成的网络功用,这使其自身在履行网络恳求时简略易用。
运用 async
和 await
的代码是异步的,可是看起来有点像同步代码。必须在带有 async 关键字的 异步函数 中运用 await
:
Future<void> checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
尽管 async
函数可能会履行一些耗时操作,可是它并不会等候这些耗时操作完成,相反,异步函数履行时会在其遇到第一个 await
表达式时回来一个Future
目标,然后等候 await
表达式履行结束后持续履行。 Future
目标代表一个“承诺”, await
表达式会堵塞直到需求的目标回来。
数据序列化
在 Flutter
中根底的序列化JSON
非常简略的。Flutter
有一个内置的 dart:convert
的库,这个库包含了一个简略的 JSON
编码器和解码器。将JSON
字符串作为办法的参数,调用 jsonDecode()
办法来解码 JSON。
不过这种办法尽管简略,但不好的是,jsonDecode()
回来一个 Map<String, dynamic>
,这意味着你在运转时以前都不知道值的类型。运用这个办法,你失去了大部分的静态类型言语特性:类型安全、主动补全以及最重要的编译时异常。你的代码会当即变得愈加简略犯错。
Flutter
中也有可运用第三方库json_serializable
来完成主动序列化JSON数据,能够参阅JSON 和序列化数据
class _SampleAppPage extends State<SampleAppPage> {
// TODO:能够优化,widgets弄成实体目标,经过主动序列化
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: getBody());
}
Future<void> loadData() async {
// 创立 receivePort 接受端口
ReceivePort receivePort = ReceivePort();
// 创立 Isolate,由于这是个异步操作,所以加上 await
await Isolate.spawn(dataLoader, receivePort.sendPort);
// 创立 SendPort 发送端口
SendPort sendPort = await receivePort.first;
// 发送
List msg = await sendReceive(
sendPort,
'https://jsonplaceholder.typicode.com/posts',
);
setState(() {
widgets = msg;
});
}
Future sendReceive(SendPort sendPort, address) {
ReceivePort response = ReceivePort();
sendPort.send([address, response.sendPort]);
return response.first;
}
static Future<void> dataLoader(SendPort sendPort) async {
ReceivePort port = ReceivePort();
sendPort.send(port.sendPort);
await for (var msg in port) {
String data = msg[0];
SendPort replyTo = msg[1];
String dataURL = data;
http.Response response = await http.get(Uri.parse(dataURL));
replyTo.send(jsonDecode(response.body));
developer.log(response.body);
}
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return const Center(child: CircularProgressIndicator());
} else {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (context, position) {
return getRow(position);
},
);
}
}
Widget getRow(int i) {
return Padding(
padding: const EdgeInsets.all(10.0),
child: Text("Row$i: ${widgets[i]["title"]}"),
);
}
}
参阅
源代码地址:github.com/Kingwentao/…
给 Android 开发者的 Flutter 指南
Widget 动画
Dart之Mixins的with用法
JSON 和序列化数据