前语
这是笔者作为一个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 和序列化数据
