给Android工程师的Flutter入门手册(一)
前言
这是笔者作为一个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 本质上是行为一致的。它们每一帧都会f重建,不同之处在于
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;
});
}
}
}
异步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/…
转载自:https://juejin.cn/post/7199840152217600057