Flutter for Android developers
本文檔適用於那些希望應用現有Android知識來使用Flutter構建移動應用的Android開發人員。如果你瞭解Android框架的基礎知識,那麼可以將此文檔用作Flutter開發的快速入門。
當使用Flutter構建時,你的Android知識和技能非常有價值,因爲Flutter依賴移動操作系統來實現衆多功能和配置。Flutter是一個爲手機構建UI界面的新方式,但是它有一個插件系統來和Android或者iOS系統進行非UI人物的交互。
視圖(Views)
在Flutter中等同於 View 的是什麼?
在Android中,視圖(Views)是屏幕上顯示的所有內容的基礎。按鈕、工具欄和輸入框,它們都是視圖。在Flutter中,略等價於視圖的是控件(Widget).控件並不嚴格對應於Android中的視圖,但是當你熟悉Flutter的工作方式後,你可以將它們視爲“你聲明和構建UI的方式”。
然而,這些與視圖有一些區別。首先,控件有一個不同的生命週期:它們是不可變的,只有在需要改變之前才存在。每當控件或其狀態發生變化時,Flutter的框架層創建一個新的控件實例樹。作爲對比,一個Android的視圖(View)只繪製一次,直達 invalidate 被調用纔會重新繪製。
Flutter的控件是輕量級的,部分原因就是它們的不可變性。因爲它們本身不是視圖,並且不直接繪製任何東西,而是對UI及其語義的描述,這些描述被解析到引擎下的實際視圖對象中。
Flutter包含 Material Components 庫,它們是實現了 Material Design 準則的控件。 Material Design 是一個靈活的設計系統,適用於所有平臺,包括iOS。
但Flutter具有足夠的靈活性和表現力,可以實現任何設計語言。例如,在iOS上,你可以使用Cupertino控件來生成看起來像Apple的iOS設計語言的界面。
我要如何更新控件( Widgets )
在Android中,你可以通過直接更改視圖來更新你的視圖。然而,在Flutter中, Widgets 是不可變的並且無法直接更新,相反,你必須操作控件的狀態來更新控件。
這就是有狀態(Stateful)控件和無狀態(Stateless)控件構想的來源。一個無狀態控件 StatelessWidget 就是它看起來的樣子–一個沒有狀態信息的控件。
當你描述的部分用戶界面不依賴於對象中的配置信息意外的任何內容時, StatelessWidget 就變的非常有用。
例如,在Android中,這類似於使用ImageView來顯示你的logo。這個logo在運行時不會改變,所以在Flutter中使用無狀態控件( StatelessWidget )。
如果你想基於進行網絡請求後接收到的數據或者用戶交互來動態改變UI,那麼你必須使用有狀態控件 StatefulWidget ,並且告訴Flutter框架層控件的狀態發生更新,所以它需要更新這個控件。
這裏需要注意的重要的一點是,無狀態和有狀態控件的核心行爲都是一樣的。它們重建每一幀,區別在於有狀態控件( StatefulWidget )有一個狀態( State )對象來跨幀存儲狀態數據並且恢復它。
如果你很疑惑,那麼請記住這個規則:如果一個控件是可變的(例如由於用戶的交互發生變化),那麼它是有狀態的。但是,如果控件對更改發生變化,則包含該控件的父控件任然可以是無狀態的,如果父控件本身不對更改發生變化。
下面的例子展示瞭如何使用無狀態控件( StatelessWidget )。一個常用的無狀態控件( StatelessWidget )是 Text 控件。如果你查看 Text 控件的實現,你會發現它是 StatelessWidget 的子類。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
正如你所看到的, Text 控件沒有與之關聯的狀態信息,它僅僅通過構造器傳入的參數來渲染它,沒有其他任何信息。
但是,如果你想使“I LIke Flutter”動態改變,例如,當點擊一個 FloatingActionButton 按鈕時?
要實現這個,包裹 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<SampleAppPage> {
// 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),
),
);
}
}
我要如何佈局我的控件?我的 XML 佈局文件在哪裏?
在Android中,你在 XML 文件中編寫佈局,但是在Flutter中你使用空間樹來編寫佈局。
下面的例子展示瞭如何顯示一個帶有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),
),
),
);
}
你可以在 widget catalog 中查看Flutter提供的佈局
我要如何從我的佈局中添加或者刪除一個組件?
在Android中,你可以在父佈局中動態的調用 addChild() 或者 removeChild() 方法來添加或者刪除視圖。在Flutter中,因爲控件是不可變的,所以沒有與 addChild() 相同 的方法。取而代之,你可以傳入一個返回控件的方法,並且通過一個標誌來控制子控件的創建。
例如:
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<SampleAppPage> {
// 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創建一個動畫,要麼調用 view 的 animate() 方法。在Flutter中,爲控件設置動畫要使用動畫庫,它通過包裝控件到一個動畫控件中。
在Flutter中,要使用動畫控制器 AnimationController ,它是一個可以暫停,快進,停止和倒轉的 Animation<double> 類型的動畫。它依賴 Ticker 來指示vsync何時發生,並且在它運行時在每一幀上產生一個線性的0到1之間的插值器。然後,你創建一個或多個動畫並將它們與該控制器關聯起來。
例如,你可以使用 CurvedAnimation 沿插值曲線來實現一個動畫。在這個場景下,控制器是動畫進度的主要來源,並且由 CurvedAnimation 計算曲線來代替控制器默認的線性動畫。
當構建一個控件樹時,你指定 Animation 給某個控件的一個動畫屬性,例如 FadeTransition 的透明(opacity )屬性,並且告訴控制器開始動畫。
下面的例子展示瞭如何寫一個漸變動畫( FadeTransition ),當你點擊一個按鈕時,一個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<MyFadeTest> with TickerProviderStateMixin {
AnimationController controller;
CurvedAnimation curve;
@override
void initState() {
super.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,Animations tutorial 和 Animations overview
。
我要如何使用 Canvas 來繪製和寫?
在Android中,你可能使用 Canvas 和 Drawable 來在屏幕上繪製圖片和形狀。Flutter同樣也有一個類似的 Canvas API,因爲它基於相同的底層渲染引擎Skia。這就導致,在Flutter的Canvas中繪製對Android開發者來說非常的熟悉。
Flutter有兩個類來幫助你在canvas上繪製: CustomPaint 和 CustomPainter ,後者是實現了繪製到畫布的算法。
要想知道在Flutter中如何實現一個簽名繪製,請參考StackOverflow 中 Collin 的回答。
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<Signature> {
List<Offset> _points = <Offset>[];
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<Offset> 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 類,或者使用已經存在的類,來覆蓋或者實現方法來達到想要的功能。
在Flutter中,構建一個自定義控件通過 組合( composing )一些更小的控件(而不是繼承他們)。它有點類似於在Android中實現自定義ViewGroup,其中所有構建塊都已經存在,但是你提供一個不同的行爲–例如,自定義佈局邏輯。
例如,你如何構建一個在構造函數中使用標籤的 CustomButton ?創建一個CustomButton ,它組合一個帶有標籤的 RaisedButton ,而不是通過繼承 RaisedButton :
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: () {}, child: Text(label));
}
}
然後使用 CustomButton ,就像使用任何其他Flutter小控件一樣。
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}
意圖(Intents)
在Flutter中 Intent 的等價物是什麼?
在Android中, Intent 有兩種主要的使用場景:在Activity之間實現導航以及組件之間進行通信。在Flutter中,通過另一種方式,它沒有 Intent 的概念,儘管你仍然可以使用一個插件( a plugin)來啓動 Intent 。
Flutter沒有一個與activity和fragment真正的等價物;相反,在Flutter中,你可以使用導航器( Navigator )和路由( Routes)來實現屏幕的導航,就和activity一樣。
一個 Route 是app中一個屏幕或者一個頁面的抽象,而 Navigator 是一個來管理routes的控件。一個route大致對應一個Activity,但它沒有相同的含義。一個導航器( Navigator )可以在屏幕之間壓入(push)或者彈出(pop)route。導航器( Navigator )的工作方式類似於一個堆棧,你可以 push() 一個新的route到你想要導航到的route,當你想要回退時你可以 pop() route。
在Android中,你在app的 AndroidManifest.xml 文件中聲明你的所有的 activities。
在Flutter中,你有幾個選項來在頁面之間導航:
- Specify a Map of route names. (MaterialApp)
- Directly navigate to a route. (WidgetApp)
下面的例子就構建了一個 Map.
void main() {
runApp(MaterialApp(
home: MyAppHome(), // becomes the route named '/'
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => MyPage(title: 'page A'),
'/b': (BuildContext context) => MyPage(title: 'page B'),
'/c': (BuildContext context) => MyPage(title: 'page C'),
},
));
}
導航到一個route通過push它的名稱到導航器:
Navigator.of(context).pushNamed('/b');
後續章節完善中……