文档地址:https://flutter.io/docs/get-started/flutter-for/android-devs
译者:Haocxx
这篇文章是为了让Android开发工程师利用已有的Android知识快速理解基于Flutter的APP开发。如果你对Android framework有一定的理解,你可以把这篇文档作为一个迈向Flutter开发大门的跳板。
你已有的Android开发知识非常有益于学习开发Flutter工程,因为Flutter依赖于许多移动操作系统的原理和架构。Flutter是一种新的移动开发框架,同时也提供了与Android/IOS通信的插件系统。如果你精通Android,你可以迅速上手Flutter开发。
这篇文档适用于刚转向Flutter的开发者,你可以把它作为一个字典,用于查找刚上手Flutter最有可能遇到的问题及困惑。
声明式UI的介绍:https://flutter.io/docs/get-started/flutter-for/declarative
在Android中,View时显示在屏幕上所有控件图像的基础。按钮,工具栏,各种输入控件都UI都是一个View。在Flutter中,与View最相似的是Widget。Widget并不完全等于Android的View,但是刚开始的时候你也可以把它们当作是一个声明和构建UI的组件。
但是,Widget还是跟Android中的View有很多区别的。首先,Widget有不同的生命周期:一个Widget是不可变的,当它们的状态被改变,Flutter framework会重新创建Widget的实例树。而在Android上,一个View只会绘制一次,除非调用invalidate。
Flutter的Widget是不可变的轻量级的组件。因为Widget并不是视图本身,也不直接进行绘制,它更像是对真正视图对象的描述和定义。
Flutter包含了MaterialDesign的库,MaterialDesign是一个面向所有平台(包括IOS)的灵活设计系统。
Flutter具有良好的灵活性和设计表现力。比如在IOS上,你可以使用Cupertino组件来实现一个类似IOS原生的界面。
在Android中,你可以直接修改一个View。但在Flutter中,Widget是不能直接改变,而是需要通过改变Widget的状态来实现。
这就是有状态Widget(StatefulWidget)和无状态Widget(StatelessWidget)概念的由来。无状态Widget正如名字定义的那样,是一个没有状态信息的Widget。
无状态Widget在描述对非本Widget配置数据无依赖的控件时是很好用的。
比如,在Android中,我们习惯把一个Logo图片放在一个ImageView中。这个图片在运行过程中不会发生改变,所以在Flutter中会使用无状态Widget来描述它。
如果你想要根据网络接口拉取的数据或者相应用户来动态地改变UI,那就必须使用有状态Widget,并将Widget的改变通知Flutter的framework。
需要注意的是,有状态Widget和无状态WIdget其实本质是一样的,他们每一帧都会重新创建。不同的是,有状态WIdget持有一个状态State对象来存储帧之间的信息。
如果还是不太理解的话,只要记住一条规则就行了:如果一个Widget会因响应用户操作等而改变,它就是有状态Widget。另外,一个Widget要改变的话,包含它的父Widget如果自身不变的话仍然可以是无状态Widget。
下面的例子告诉我们如何去定义一个无状态Widget。常见的一个无状态Widget是Text。如果查看Text的实现关系的话就会发现它是StatelessWidget的子类。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
正如你所看到的,Text没有任何与之相关联的状态信息,它指根据传入其构造方法的参数来渲染,没有其他的操作。
但是如果你想让“I Like Flutter”动态地变化,比如响应按钮的点击该怎么实现呢?
想要实现这一点,需要把Text包装进StatefulWidget并在用户点击按钮的时候更新它。
比如:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State {
// Default placeholder text
String textToShow = "I Like Flutter";
void _updateText() {
setState(() {
// update the text
textToShow = "Flutter is Awesome!";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(child: Text(textToShow)),
floatingActionButton: FloatingActionButton(
onPressed: _updateText,
tooltip: 'Update Text',
child: Icon(Icons.update),
),
);
}
}
在Android中,你需要把布局写在XML文件中。但在Flutter中你需要把它写在视图树(Widget Tree)中。
下面的例子告诉你如何用padding属性来展示一个简单的视图:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: MaterialButton(
onPressed: () {},
child: Text('Hello'),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
),
),
);
}
更多的布局属性可以参照官方文档:https://flutter.io/docs/development/ui/widgets/layout
在Android中,你可以在父View上调用addChild或者removeChild来动态添加删除VIew。在Flutter中,因为Widget是不可变的,所以没有像Android一样直接添加删除的方法。相对应地,你可以传入一个返回Widget对象的方法,通过boolean值来控制子视图的创建。
比如,下面的代码实现了点击按钮切换两个Widget:
import 'package:flutter/material.dart';
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Sample App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SampleAppPage(),
);
}
}
class SampleAppPage extends StatefulWidget {
SampleAppPage({Key key}) : super(key: key);
@override
_SampleAppPageState createState() => _SampleAppPageState();
}
class _SampleAppPageState extends State {
// Default value for toggle
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
_getToggleChild() {
if (toggle) {
return Text('Toggle One');
} else {
return MaterialButton(onPressed: () {}, child: Text('Toggle Two'));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: Center(
child: _getToggleChild(),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
tooltip: 'Update Text',
child: Icon(Icons.update),
),
);
}
}
在Android中,你可以使用XML或者调用animate()方法来为一个View添加动画。在Flutter中,Widget的动画是通过将自身包装到animation库中的动画Widget来实现的。
在Flutter中,使用AnimationController(继承自Animation
比如,你可以使用CurvedAnimation来实现一个插值曲线动画效果。此时,AnimationController就是整个动画的主资源,CurvedAnimation会根据插值计算出一条曲线来替代控制器默认的线性运动。就好像Widget在演剧本一样。
在Widget树build的时候可以为一个Widget指定一个动画属性,比如指定FadeTransition的不透明度,并将Widget包装到里面,然后通知Controller开始动画。
下面的例子演示了如何写一个点击按钮,点击出现淡出Logo的效果:
import 'package:flutter/material.dart';
void main() {
runApp(FadeAppTest());
}
class FadeAppTest extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fade Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyFadeTest(title: 'Fade Demo'),
);
}
}
class MyFadeTest extends StatefulWidget {
MyFadeTest({Key key, this.title}) : super(key: key);
final String title;
@override
_MyFadeTest createState() => _MyFadeTest();
}
class _MyFadeTest extends State with TickerProviderStateMixin {
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
child: FadeTransition(
opacity: curve,
child: FlutterLogo(
size: 100.0,
)))),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade',
child: Icon(Icons.brush),
onPressed: () {
controller.forward();
},
),
);
}
}
关于动画的详细内容有三篇官方文档:
Animation & Motion widgets:https://flutter.io/docs/development/ui/widgets/animation
Animations tutorial:https://flutter.io/docs/development/ui/animations/tutorial
Animation overview:https://flutter.io/docs/development/ui/animations
在Android中,你可以使用Canvas和Drawable来绘制图像。Flutter也有一个类似的Canvas API,因为它是同样是基于Skia渲染引擎的,所以Android开发者在使用的时候会感到得心应手。
Flutter有两个类来实现Canvas:CustomPaint和CustomPainter,你可以使用它们来实现自己的绘制算法。
Collin在StackOverFlow中的回答演示了在Flutter中如何实现实现一个签名Painter:
https://stackoverflow.com/questions/46241071/create-signature-area-for-mobile-app-in-dart-flutter
import 'package:flutter/material.dart';
void main() => runApp(MaterialApp(home: DemoApp()));
class DemoApp extends StatelessWidget {
Widget build(BuildContext context) => Scaffold(body: Signature());
}
class Signature extends StatefulWidget {
SignatureState createState() => SignatureState();
}
class SignatureState extends State {
List _points = [];
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
);
}
}
class SignaturePainter extends CustomPainter {
SignaturePainter(this.points);
final List points;
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);
}
}
bool shouldRepaint(SignaturePainter other) => other.points != points;
}
在Android中,你通常会继承一个View,或使用已有的View并复写它的方法来实现自己想要的效果。
在Flutter中,自定义View是通过对已有View的组合来实现的,而不是继承它们。这有点像Android中实现一个子元素全都是已有View的自定义ViewGroup。
比如,怎样实现一个带有标签的自定义按钮?只需要写一个CustomButton Widget类,内部包含带有标签的RaisedButton即可。而不必继承RaisedButton。
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}
你可以像使用其他Widget一样使用它:
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}