https://flutter.io/flutter-for-android/
[toc]
View 在 Flutter 里什么等价于 View View 是一切 Android 控件的基石。无论是 Button, Toolbar 或者 Input 都是 View。Flutter 中的 View 是 Widget,但有几点不同。首先,widget 的生命周期只有一帧,flutter 框架会为每一帧都创建一个 widget 实例树。相比之下, View 绘制之后只有 invalidate 方法被调用才会再次绘制。
Android 中,我们直接修改 view 即可更新。但是 Flutter 的 Widget 是不可变的,不能直接修改,只能修改 Widget 的状态来更新。 这也是 Stateful 和 Stateless Widget 这两个概念的由来。StatelessWidget 就是一个没有状态信息的 widget 。
当你所需的用户界面不依赖对象里的任何配置信息时,可以选择使用 StatelessWidget。
比如 Android 里的这样一个场景,一个只显示 logo 的 ImageView ,运行时 logo 也不变这种的。
再比如,要是你想根据一个 http 请求的返回结果动态的对 UI 进行修改,你就得用 StatefulWidget 了,然后告诉 Flutter 框架状态更新了,他就会自己处理后面的事了。
需要注意,本质上 Stateless 和 Stateful 还是会在每一帧重建的。不同的是 StatefulWidget 有一个 State 对象保存状态数据并在新的一帧中恢复。
如果你对此还有疑惑,请记住:如果 widget 可以改变(比如响应用户操作) 就是 stateful 的。child widget 是 Stateful 并不意味他的 parent widget 就是 Stateful 的,一切都要看当前 widget 是否响应变化。
看看具体代码,Text widget 一个普通的 StatelessWidget 。看源码就知道 Text Widget 是 StatelessWidget 的子类。
1 2 3 4 new Text( 'I like Flutter!' , style: new TextStyle(fontWeight: FontWeight.bold), );
可以看到 Text 并没有相关的状态信息,除了构造函数传入的参数就啥都没有了。
那你想了啊,我要是想点个按钮让那段字变一下怎么整?
用 StatelessWidget 包一下 Text 就行啦:
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 import 'package:flutter/material.dart' ;void main() { runApp(new SampleApp()); } class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Sample App' , theme: new ThemeData( primarySwatch: Colors.blue, ), home: new SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super (key: key); @override _SampleAppPageState createState() => new _SampleAppPageState(); } class _SampleAppPageState extends State <SampleAppPage > { String textToShow = "I Like Flutter" ; void _updateText() { setState(() { textToShow = "Flutter is Awesome!" ; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Sample App" ), ), body: new Center(child: new Text(textToShow)), floatingActionButton: new FloatingActionButton( onPressed: _updateText, tooltip: 'Update Text' , child: new Icon(Icons.update), ), ); } }
怎么写布局?还用 xml 嘛? 没有 xml,我们直接写 widget tree。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Sample App" ), ), body: new Center( child: new MaterialButton( onPressed: () {}, child: new Text('Hello' ), padding: new EdgeInsets.only(left: 10.0 , right: 10.0 ), ), ), ); }
You can view all the layouts that Flutter has to offer here: https://flutter.io/widgets/layout/
如何对 layout 组件进行增删 Android 里用 parent 的addChild
或 removeChild
方法对child 进行增删。但是 Flutter 的 widget 是不可变的,这些方法都没有,创建 widget 树时你可以用一个函数返回 widget,再通过传入的参数判断怎么显示 widget。
栗子: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 import 'package:flutter/material.dart' ;void main() { runApp(new SampleApp()); } class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Sample App' , theme: new ThemeData( primarySwatch: Colors.blue, ), home: new SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super (key: key); @override _SampleAppPageState createState() => new _SampleAppPageState(); } class _SampleAppPageState extends State <SampleAppPage > { bool toggle = true ; void _toggle() { setState(() { toggle = !toggle; }); } _getToggleChild() { if (toggle) { return new Text('Toggle One' ); } else { return new MaterialButton(onPressed: () {}, child: new Text('Toggle Two' )); } } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Sample App" ), ), body: new Center( child: _getToggleChild(), ), floatingActionButton: new FloatingActionButton( onPressed: _toggle, tooltip: 'Update Text' , child: new Icon(Icons.update), ), ); } }
Flutter 提供了 animation library 实现动画。
Andorid 中,我们通过 xml 或是直接调用 View 的 animate()
方法创建动画,而 Flutter 是将 widget 包装到一个 Animation 之中。
和 Android 一样, Flutter 也提供 AnimationController 和一个Interpolator (继承自 Animation 类),比如 CurvedAnimation。使用时将 controller 和 Animation 传入 AnimationWidget ,之后调用 controller 启动动画。
一个 fade 动画:
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 import 'package:flutter/material.dart' ;void main() { runApp(new FadeAppTest()); } class FadeAppTest extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Fade Demo' , theme: new ThemeData( primarySwatch: Colors.blue, ), home: new MyFadeTest(title: 'Fade Demo' ), ); } } class MyFadeTest extends StatefulWidget { MyFadeTest({Key key, this .title}) : super (key: key); final String title; @override _MyFadeTest createState() => new _MyFadeTest(); } class _MyFadeTest extends State <MyFadeTest > with TickerProviderStateMixin { AnimationController controller; CurvedAnimation curve; @override void initState() { controller = new AnimationController(duration: const Duration (milliseconds: 2000 ), vsync: this ); curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: new Center( child: new Container( child: new FadeTransition( opacity: curve, child: new FlutterLogo( size: 100.0 , )))), floatingActionButton: new FloatingActionButton( tooltip: 'Fade' , child: new Icon(Icons.brush), onPressed: () { controller.forward(); }, ), ); } }
See https://flutter.io/widgets/animation/ and https://flutter.io/tutorials/animation for more specific details.
如何使用 Canvas 进行 draw/paint Android 中,使用 Canvas 即可绘制自定义图形。 Flutter 提供两个类 CustomPaint 和 CustomPainter 帮助你进行绘制,
来自 StackOverFlow 的热门问题,实现一个画板:https://stackoverflow.com/questions/46241071/create-signature-area-for-mobile-app-in-dart-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 import 'package:flutter/material.dart' ;class SignaturePainter extends CustomPainter { SignaturePainter(this .points); final List <Offset> points; void paint(Canvas canvas, Size size) { var paint = new 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; } class Signature extends StatefulWidget { SignatureState createState() => new SignatureState(); } class SignatureState extends State <Signature > { List <Offset> _points = <Offset>[]; Widget build(BuildContext context) { return new GestureDetector( onPanUpdate: (DragUpdateDetails details) { setState(() { RenderBox referenceBox = context.findRenderObject(); Offset localPosition = referenceBox.globalToLocal(details.globalPosition); _points = new List .from(_points)..add(localPosition); }); }, onPanEnd: (DragEndDetails details) => _points.add(null ), child: new CustomPaint(painter: new SignaturePainter(_points)), ); } } class DemoApp extends StatelessWidget { Widget build(BuildContext context) => new Scaffold(body: new Signature()); } void main() => runApp(new MaterialApp(home: new DemoApp()));
安卓怎么自定义 View 大家都很熟了,不多说。 Flutter 里自定义一个 widget 往往不是直接继承,而是通过组合小的 widget。
我们来实现一个 CustomButton ,直接在构造函数传入一个 label。这里将其与一个 RaisedButton 组合,而不是继承一个 RaisedButton,再重写实现新的方法。1 2 3 4 5 6 7 8 9 class CustomButton extends StatelessWidget { final String label; CustomButton(this .label); @override Widget build(BuildContext context) { return new RaisedButton(onPressed: () {}, child: new Text(label)); } }
使用1 2 3 4 5 6 7 @override Widget build(BuildContext context) { return new Center( child: new CustomButton("Hello" ), ); } }
Intents Flutter 里什么等价于 Intent Android 使用 Intent 主要是用来切换 Activity,和调用外部组件。 Flutter 里没有 Intent 的概念,但必要时仍可以通过 Flutter 提供的方法调用 native 层发送 Intent。
Flutter 使用 router 绘制新的 widget 从而达到切换 screen 的需求。这里有两个核心概念,同时也是 Flutter 提供用来管理多个 screen 的类:Router 和 Navigator。Router 是一个应用中对应 screen 或 page 的抽象(想想 Activity),Navigator 是一个用来管理 router 的 widget。Navigator 通过对 router 进行 push/pop 操作帮助用户在 screen 之间切换。
Flutter 中,可以将一个 Mapname:String,router:Router 传给顶级 MaterialApp 实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void main() { runApp(new MaterialApp( home: new MyAppHome(), routes: <String , WidgetBuilder> { '/a' : (BuildContext context) => new MyPage(title: 'page A' ), '/b' : (BuildContext context) => new MyPage(title: 'page B' ), '/c' : (BuildContext context) => new MyPage(title: 'page C' ), }, )); } ``` 就可以通过 Navigator 根据 router 的名字对 router 进行操作。 ```dart Navigator.of(context).pushNamed('/b' );
Intent 的其他使用场景(比如启动相机,选择文件),则需要创建中间层与 native 进行交互(或是用别人写好的库)。
See [Flutter Plugins] to learn how to build a native platform integration.
Flutter 如何处理外部程序发来的 Intent Flutter 可以直接与 Android 层交互,获取共享的数据,从而处理外部发来的 Intent。
看例子: 基本流程就是先在 Android 端接收 text,然后等待 Flutter 通过 MethodChannel 请求将数据发送给它。
我们先在 AndroidManifest.xml 注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <activity android:name =".MainActivity" android:launchMode ="singleTop" android:theme ="@style/LaunchTheme" android:configChanges ="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection" android:hardwareAccelerated ="true" android:windowSoftInputMode ="adjustResize" > <meta-data android:name ="io.flutter.app.android.SplashScreenUntilFirstFrame" android:value ="true" /> <intent-filter > <action android:name ="android.intent.action.MAIN" /> <category android:name ="android.intent.category.LAUNCHER" /> </intent-filter > <intent-filter > <action android:name ="android.intent.action.SEND" /> <category android:name ="android.intent.category.DEFAULT" /> <data android:mimeType ="text/plain" /> </intent-filter > </activity >
MainActivity 中,我们先从 intent 中取出数据,等待 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 package com.yourcompany.shared;import android.content.Intent;import android.os.Bundle;import java.nio.ByteBuffer;import io.flutter.app.FlutterActivity;import io.flutter.plugin.common.ActivityLifecycleListener;import io.flutter.plugin.common.MethodCall;import io.flutter.plugin.common.MethodChannel;import io.flutter.plugins.GeneratedPluginRegistrant;public class MainActivity extends FlutterActivity { String sharedText; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this ); Intent intent = getIntent(); String action = intent.getAction(); String type = intent.getType(); if (Intent.ACTION_SEND.equals(action) && type != null ) { if ("text/plain" .equals(type)) { handleSendText(intent); } } new MethodChannel(getFlutterView(), "app.channel.shared.data" ).setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall (MethodCall methodCall, MethodChannel.Result result) { if (methodCall.method.contentEquals("getSharedText" )) { result.success(sharedText); sharedText = null ; } } }); } void handleSendText (Intent intent) { sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); } }
最后,当 Flutter 渲染 view 时请求数据
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 import 'package:flutter/material.dart' ;import 'package:flutter/services.dart' ;void main() { runApp(new SampleApp()); } class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Sample Shared App Handler' , theme: new ThemeData( primarySwatch: Colors.blue, ), home: new SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super (key: key); @override _SampleAppPageState createState() => new _SampleAppPageState(); } class _SampleAppPageState extends State <SampleAppPage > { static const platform = const MethodChannel('app.channel.shared.data' ); String dataShared = "No data" ; @override void initState() { super .initState(); getSharedText(); } @override Widget build(BuildContext context) { return new Scaffold(body: new Center(child: new Text(dataShared))); } getSharedText() async { var sharedData = await platform.invokeMethod("getSharedText" ); if (sharedData != null ) { setState(() { dataShared = sharedData; }); } } }
那么 startActivityForResult? Flutter 可以使用 Navigator 获取一个 router 返回的数据。直接 await 一个 push 的返回值。如下:1 Map coordinates = await Navigator.of(context).pushNamed('/location' );
在位置选择 router 里,当用户操作完毕,就可以带着返回值 pop1 Navigator.of(context).pop({"lat" :43.821757 ,"long" :-79.226392 });