1 获取Flutter SDK
1.下载安装包
2.将压缩包解压,然后把其中的 flutter 目录整个放在你想放置 Flutter SDK 的路径中
勿将 Flutter 安装在需要高权限的文件夹内,例如 C:\Program Files\。
2 配置环境变量
2.1 更新path环境变量
Environment Variables->User Variables->PATH->New加入 flutter\bin 目录的完整路径
配置国内镜像,新增加环境变量
2.2 配置Android Studio
File > Settings > Plugins下载Flutter和Dart插件
配置国内依赖
android/build.gradle替换如下内容
repositories {
//google()
//jcenter()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
}
插件版本和gradle版本匹配
查看gradle版本
android/build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
android/gradle\wrapper\gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
3 热重载(hot reload)
保持app运行状态,点击菜单栏的闪电进行热重载。
4 创建应用
在pubspec.yaml中保持如下设置,使用更多 Material 的特性
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
5 视图(Views)
5.1 视图在Flutter中的对应概念
View是Android中显示在屏幕上的一切基础;
Widget大致是Flutter中的对应的View。
差异①:
widget 有着不一样的生命周期:它们是不可变的,一旦需要变化则生命周期终止。任何时候 widget 或它们的状态变化时, Flutter 框架都会创建一个新的 widget 树的实例。
Android View 只会绘制一次,除非调用 invalidate 才会重绘。
Flutter 的 widget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象的语义的描述。
5.2 更新widgets
差异②:
Android中可以直接更新View;
Flutter中Widget是不可变的,不能直接更新,需要操作Widget的状态。
StatelessWidget用于用户界面的一部分不依赖于除了对象中的配置信息以外的任何东西的场景。如Android中的ImageView,这个图标运行中不会改变,在Flutter中即StatelessWidget。
Text(
'I like Flutter!',
style: TextStyle(fontWeight: FontWeight.bold),
);
StatefulWidget用于用户界面的一部分需要和用户动态交互的部分场景。如根据 HTTP 请求返回的数据或者用户的交互来动态地更新界面,并告诉 Flutter 框架 Widget 的状态 (State) 更新了,以便 Flutter 可以更新这个 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<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),
),
);
}
}
这里需要着重注意的是,无状态和有状态的 Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget 有一个跨帧存储和恢复状态数据的 State 对象。
如果一个 Widget 会变化(例如由于用户交互),它是有状态的。然而,如果一个 Widget 响应变化,它的父 Widget 只要本身不响应变化,就依然是无状态的。
5.3 布局Widget
差异③:
Android中使用XML文件定义布局;
Flutter中使用Widget树定义布局。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.only(left: 20.0, right: 30.0),
),
onPressed: () {},
child: Text('Hello'),
),
),
);
}
5.4 在布局中添加或删除组件
差异④:
Android中通过调用父 View 的 addChild() 或 removeChild() 方法动态地添加或者删除子 View;
Flutter中由于 Widget 是不可变的,所以没有 addChild() 的直接对应的方法。可以给返回一个 Widget 的父 Widget 传入一个方法,并通过布尔标记值toggle控制子 Widget 的创建。
class _SampleAppPageState extends State<SampleAppPage> {
// Default value for toggle.
bool toggle = true;
void _toggle() {
setState(() {
toggle = !toggle;
});
}
Widget _getToggleChild() {
if (toggle) {
return Text('Toggle One');
} else {
return ElevatedButton(
onPressed: () {},
child: Text('Toggle Two'),
);
}
}
5.5 Widget实现动画
差异⑤:
Android中既可以通过 XML 文件定义动画,也可以调用 View 对象的 animate() 方法;
Flutter中使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。
Flutter 通过 Animation 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后就可以创建一个或多个 Animation,并将它们绑定到控制器上。
下面的例子展示了如何实现一个点击 FloatingActionButton 的时候将一个 Widget 渐变为一个图标的 FadeTransition:
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: FadeTransition(
opacity: curve,
child: FlutterLogo(
size: 100.0,
),
),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Fade',
onPressed: () {
controller.forward();
},
child: Icon(Icons.brush),
),
);
}
}
5.6 使用Canvas绘制动画
差异⑥:
Android中使用 Canvas 和 Drawable 将图片和形状绘制到屏幕上;
Flutter中使用类似于 Canvas 的 API,因为它基于相同的底层渲染引擎 Skia。
Flutter 有两个用画布 (canvas) 进行绘制的类:CustomPaint 和 CustomPainter,后者可以实现自定义的绘制算法。
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;
}
5.7 创建自定义Widget
差异⑦:
Android中通过继承 View 类,或者使用已有的视图类,再覆写或实现可以达到特定效果的方法;
Flutter中通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。
创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承 RaisedButton:
class CustomButton extends StatelessWidget {
final String label;
CustomButton(this.label);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text(label),
);
}
}
使用CustomButton:
@override
Widget build(BuildContext context) {
return Center(
child: CustomButton("Hello"),
);
}
5 Intents
5.1 Intent在Flutter中对应的概念
差异⑧:
Android中Intent 主要有两个使用场景:在 Activity 之前进行导航,以及组件间通信。 Flutter 却没有 intent 这样的概念,但是你依然可以通过原生集成 (插件) 来启动 intent;
Flutter没有 Activity 和 Fragment 的对应概念。在 Flutter 中需要使用 Navigator 和 Route 在同一个 Activity 内的不同界面间进行跳转。
Route 是应用内屏幕和页面的抽象,Navigator 是管理路径 route 的工具。
一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。
Navigator 可以通过对 route 进行压栈和弹栈操作实现页面的跳转。Navigator 的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push()),想要返回的时候将 route 弹栈 (pop())。
差异⑨:
Android 中,在应用的 AndroidManifest.xml 文件中声明 Activity。
Flutter 中,有多种不同的方式在页面间导航:1)定义一个 route 名字的 Map。(MaterialApp) 2)直接导航到一个 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 中来跳转到这个 route:
Navigator.of(context).pushNamed('/b');
5.2 Flutter处理从外部应用获取接收的Intent
Flutter 可以通过直接和 Android 层通信并请求分享的数据来处理接收到的 Android intent。
示例:
首先在 Android 原生层面(在我们的 Activity 中)处理分享的文本数据,然后 Flutter 再通过使用 MethodChannel 获取这个数据。
在 AndroidManifest.xml 中注册 intent 过滤器:
<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">
<!-- ... -->
<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,提取出其它 intent 分享的文本并保存。当 Flutter 准备好处理的时候,它会使用一个平台通道请求数据,数据便会从原生端发送过来:
public class MainActivity extends FlutterActivity {
private String sharedText;
private static final String CHANNEL = "app.channel.shared.data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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); // Handle text being sent
}
}
}
@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;
}
}
);
}
void handleSendText(Intent intent) {
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
}
}
当 Widget 渲染的时候,从 Flutter 这端请求数据:
import 'package:flutter/material.dart';
import 'package:flutter/services.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 Shared App Handler',
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> {
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)));
}
void getSharedText() async {
var sharedData = await platform.invokeMethod('getSharedText');
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
}
5.3 startActivityForResult()的对应方法
Navigator 类负责 Flutter 的导航,并用来接收被压栈的 route 的返回值。这是通过在 push() 后返回的 Future 上 await 来实现的。
打开一个让用户选择位置的 route:
Map coordinates = await Navigator.of(context).pushNamed('/location');
在你的位置 route 内,一旦用户选择了位置,你就可以弹栈 (pop) 并返回结果:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
6 异步UI
6.1 runOnUiThread()的对应方法
Flutter 的事件循环对应于 Android 里的主 Looper— 也即绑定到主线程上的 Looper。
除非创建一个 Isolate
,否则你的 Dart 代码会运行在主 UI 线程,并被一个事件循环所驱动。
差异⑩:
Android中Android 中需要你时刻保持主线程空闲;
Flutter中使用 Dart 语言提供的异步工具,例如 async/await 来执行异步任务。
使用 async/await 来运行网络代码而且不会导致 UI 挂起,同时让 Dart 来处理背后的繁重细节:
Future<void> loadData() async {
String dataURL = 'https://jsonplaceholder.typicode.com/posts';
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
用 await 修饰的网络操作完成,再调用 setState() 更新 UI,这会触发 widget 子树的重建并更新数据。
异步加载数据并展示在 ListView 内:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@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> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
String dataURL = 'https://jsonplaceholder.typicode.com/posts';
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
6.2 任务转移到后台
差异11:
Android中访问一个网络资源却又不想阻塞主线程并避免 ANR 的时候,一般会将任务放到一个后台线程中运行。使用一个 AsyncTask、一个 LiveData、一个 IntentService、一个 JobScheduler 任务或者通过 RxJava 的管道用调度器将任务切换到后台线程中。
Flutter中单线程并且运行一个事件循环,无须担心线程的管理以及后台线程的创建。在执行和 I/O 绑定的任务时,例如存储访问或者网络请求,可以安全地使用 async/await。再例如,执行消耗 CPU 的计算密集型工作,将其转移到一个 Isolate 上以避免阻塞事件循环,就像 Android 中会将任何任务放到主线程之外一样。
对于和 I/O 绑定的任务,将方法声明为 async 方法,并在方法内 await 一个长时间运行的任务:
Future<void> loadData() async {
String dataURL = 'https://jsonplaceholder.typicode.com/posts';
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
Android 中继承 AsyncTask 的时候,一般会覆写三个方法:
onPreExecute()
、doInBackground()
和onPostExecute()
;
Flutter 中没有对应的 API,只需要 await 一个耗时方法调用, Dart 的事件循环就会帮你处理剩下的事情。
在 Flutter 中,可以通过使用 Isolate 来利用多核处理器的优势执行耗时或计算密集的任务。Isolate 是独立执行的线程,不会和主执行内存堆分享内存。这意味着你无法访问主线程的变量,或者调用 setState() 更新 UI。不同于 Android 中的线程,Isolate 如其名所示,它们无法分享内存(例如通过静态变量的形式)。
Isolate 将数据分享给主线程来更新 UI 的示例:
Future<void> loadData() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(dataLoader, receivePort.sendPort);
// The 'echo' isolate sends its SendPort as the first message.
SendPort sendPort = await receivePort.first;
List msg = await sendReceive(
sendPort,
"https://jsonplaceholder.typicode.com/posts",
);
setState(() {
widgets = msg;
});
}
// The entry point for the isolate.
static Future<void> dataLoader(SendPort sendPort) async {
// Open the ReceivePort for incoming messages.
ReceivePort port = ReceivePort();
// Notify any other isolates what port this isolate listens to.
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(dataURL);
// Lots of JSON to parse
replyTo.send(jsonDecode(response.body));
}
}
Future sendReceive(SendPort port, msg) {
ReceivePort response = ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
6.3 网络请求
差异12:
Android中使用OKHttp;
Flutter中使用http包,虽然 http 包没有 OkHttp 中的所有功能,但是它抽象了很多通常会自己实现的网络功能,这使其本身在执行网络请求时简单易用。
先在 pubspec.yaml 文件中添加依赖:
dependencies:
...
http: ^0.11.3+16
发起一个网络请求,在异步 (async) 方法 http.get() 上调用 await 即可:
import 'dart:convert';
import 'package:http/http.dart' as http;
// ...
Future<void> loadData() async {
String dataURL = 'https://jsonplaceholder.typicode.com/posts';
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
6.4 耗时任务显示进度
差异13:
Android中在后台执行耗时任务时显示一个ProgressBar在页面上;
Flutter中使用 ProgressIndicator widget。通过代码逻辑使用一个布尔标记值控制进度条的渲染。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(SampleApp());
}
class SampleApp extends StatelessWidget {
@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> {
List widgets = [];
@override
void initState() {
super.initState();
loadData();
}
Widget getBody() {
bool showLoadingDialog = widgets.isEmpty;
if (showLoadingDialog) {
return getProgressDialog();
} else {
return getListView();
}
}
Widget getProgressDialog() {
return Center(child: CircularProgressIndicator());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: getBody(),
);
}
ListView getListView() {
return ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
},
);
}
Widget getRow(int i) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Text("Row ${widgets[i]["title"]}"),
);
}
Future<void> loadData() async {
String dataURL = 'https://jsonplaceholder.typicode.com/posts';
http.Response response = await http.get(dataURL);
setState(() {
widgets = jsonDecode(response.body);
});
}
}
7 工程与资源文件
7.1 放置分辨率相关图片文件
差异14:
Android中区分对待资源文件 (resources) 和资产文件 (assets);
Flutter中只有资产文件 (assets)。
Android 密度修饰符 | Flutter 像素比例 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
Flutter 遵循一个简单的类似 iOS 的密度相关的格式。文件可以是一倍 (1.0x)、两倍 (2.0x)、三倍 (3.0x) 或其它的任意倍数。 Flutter 没有 dp 单位,但是有逻辑像素尺寸,基本和设备无关的像素尺寸是一样的。名称为 [devicePixelRatio][] 的尺寸表示在单一逻辑像素标准下设备物理像素的比例。
原生端访问Flutter assets文件:
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
Flutter不能访问原生端的资源文件,Flutter中添加图片需要将基础图片(1.0x)放在 images 文件夹中,并将其它倍数的图片放入以特定倍数作为名称的子文件夹中:
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
在 pubspec.yaml 文件中定义这些图片:
assets:
- images/my_icon.jpeg
使用 AssetImage 访问你的图片了:
AssetImage('images/my_icon.jpeg');
或者通过 Image widget 直接访问:
@override
Widget build(BuildContext context) {
return Image.asset('images/my_image.png');
}
7.2 字符串本地化
Flutter 当下并没有一个特定的管理字符串的资源管理系统。最好的办法是将字符串作为静态域存放在类中,并通过类访问它们。例如:
class Strings {
static String welcomeMessage = 'Welcome To Flutter';
}
访问字符串方法:
Text(Strings.welcomeMessage)
8 Activity和Fragment
8.1 Flutter中的Activity和Fragment
差异15:
Android中一个 Activity 代表用户可以完成的一件独立任务,一个 Fragment 代表一个行为或者用户界面的一部分;
Flutter中这两个概念都对应于 Widget。
8.2 监听Activity的生命周期
差异16:
Android中覆写 Actvity 的生命周期方法来监听其生命周期,或在 Application 上注册 ActivityLifecycleCallbacks;
Flutter中通过绑定 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 的变化事件来监听生命周期。
生命周期事件:
事件 | 解释 |
---|---|
inactive | 应用处于非活跃状态并且不接收用户输入 |
detached | 应用依然保留 flutter engine,但是它会脱离全部宿主 view |
paused | 应用当前对用户不可见,无法响应用户输入,并运行在后台。这个事件对应于 Android 中的 onPause() |
resumed | 应用对用户可见并且可以响应用户的输入。这个事件对应于 Android 中的 onPostResume() |
suspending | 应用暂时被挂起。这个事件对应于 Android 中的 onStop(); iOS 上由于没有对应的事件,因此不会触发此事件 |
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_lastLifecycleState = state;
});
}
@override
Widget build(BuildContext context) {
if (_lastLifecycleState == null) {
return Text(
'This widget has not observed any lifecycle changes.',
textDirection: TextDirection.ltr,
);
}
return Text(
'The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
textDirection: TextDirection.ltr,
);
}
}
void main() {
runApp(Center(child: LifecycleWatcher()));
}
9 布局
9.1 Flutter中的LinearLayout
差异17:
Android中LinearLayout 用于线性布局 widget 的水平或者垂直;
Flutter中使用 Row 或者 Column Widget 来实现相同的效果。
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
9.2 Flutter中的RelativeLayout
RelativeLayout 通过 Widget 的相互位置对它们进行布局。
通过组合使用 Column、Row 和 Stack Widget 实现 RelativeLayout 的效果。或在 Widget构造器内声明孩子相对父亲的布局规则。
9.3 Flutter中ScrollView
差异18:
Android中使用 ScrollView 布局 widget—如果用户的设备屏幕比应用的内容区域小,用户可以滑动内容;
Flutter中使用 ListView widget,ListView widget 既是一个 ScrollView,也是一个 Android 中的ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
9.4 Flutter中屏幕旋转
在AndroidManifest.xml
中声明:
android:configChanges="orientation|screenSize"
10 手势监听和触摸事件处理
10.1 Widget添加监听器
差异19:
Android中通过调用 setOnClickListener 方法在按钮这样的 View 上添加点击监听器;
Flutter中有两种添加触摸监听器的方法。
方法一:
如果 Widget 支持事件监听,那么向它传入一个方法并在方法中处理事件。例如,RaisedButton 有一个 onPressed 参数:
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
print('click');
},
child: Text('Button'),
);
}
方法二:
如果 Widget 不支持事件监听,将 Widget 包装进一个 GestureDetector 中并向 onTap 参数传入一个方法:
class SampleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
print('tap');
},
child: FlutterLogo(
size: 200.0,
),
),
),
);
}
}
10.2 处理其他手势
使用GestureDetector监听的手势:
Tap
onTapDown | 一个可能产生点击事件的指针触摸到屏幕的特定位置 |
---|---|
onTapUp | 一个产生了点击事件的指针停止触摸屏幕的特定位置 |
onTap | 一个点击事件已经发生 |
onTapCancel | 之前触发了 onTapDown 事件的指针不会产生点击事件 |
Double tap
onDoubleTap | 用户在屏幕同一位置连续快速地点击两次 |
---|
Long press
onLongPress | 指针在屏幕的同一位置保持了一段较长时间的触摸状态 |
---|
Vertical drag
onVerticalDragStart | 指针已经触摸屏幕并可能开始垂直移动 |
---|---|
onVerticalDragUpdate | 触摸屏幕的指针在垂直方向移动了更多的距离 |
onVerticalDragEnd | 之前和屏幕接触并垂直移动的指针不再继续和屏幕接触,并且在和屏幕停止接触的时候以一定的速度移动 |
Horizontal drag
onHorizontalDragStart | 指针已经触摸屏幕并可能开始水平移动 |
---|---|
onHorizontalDragUpdate | 触摸屏幕的指针在水平方向移动了更多的距离 |
onHorizontalDragEnd | 之前和屏幕接触并水平移动的指针不再继续和屏幕接触,并且在和屏幕停止接触的时候以一定的速度移动 |
双击旋转 Flutter 标志的 GestureDetector:
11 Listviews和adapters
11.1 Flutter中的ListView
差异20:
Android中创建一个 adapter 并将其传给 ListView, ListView 渲染 adapter 返回的每一行内容。要确保回收了每一行视图,否则会遇到各种奇怪的界面和内存问题;
Flutter中要向 ListView 传入一组 widget, Flutter 会保证滑动的快速顺畅。
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(Padding(
padding: EdgeInsets.all(10.0),
child: Text('Row $i'),
));
}
return widgets;
}
}
11.2 监听点击列表项
差异21:
Android中ListView 有一个可以定位哪个列表项被点击了的方法;
Flutter中使用传入 widget 的触摸监听。
class _SampleAppPageState extends State<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: ListView(children: _getListData()),
);
}
List<Widget> _getListData() {
List<Widget> widgets = [];
for (int i = 0; i < 100; i++) {
widgets.add(
GestureDetector(
onTap: () {
print('row tapped');
},
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text('Row $i'),
),
),
);
}
return widgets;
}
}
11.3 动态更新ListView
差异22:
Android中要更新 adapter 并调用notifyDataSetChanged
;
Flutter中在 setState() 里创建一个新的 List,并将数据从旧列表拷贝到新列表。如果在 setState() 里更新一组 widget,数据并没有更新到界面上。这是因为当 setState() 被调用的时候, Flutter 渲染引擎会查看 Widget 树是否有任何更改。当引擎检查到 ListView,他会执行 == 检查,并判断两个 ListView 是一样的。没有任何更改,所以也就不需要更新。
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: ListView(children: widgets),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets = List.from(widgets);
widgets.add(getRow(widgets.length));
print('row $i');
});
},
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text('Row $i'),
),
);
}
}
高效且有效的创建一个列表的方法是使用 ListView.Builder,这个方法非常适用于动态列表或者拥有大量数据的列表。这基本上就是 Android 里的 RecyclerView,会为你自动回收列表项:
class _SampleAppPageState extends State<SampleAppPage> {
List<Widget> widgets = [];
@override
void initState() {
super.initState();
for (int i = 0; i < 100; i++) {
widgets.add(getRow(i));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: ListView.builder(
itemCount: widgets.length,
itemBuilder: (BuildContext context, int position) {
return getRow(position);
},
),
);
}
Widget getRow(int i) {
return GestureDetector(
onTap: () {
setState(() {
widgets.add(getRow(widgets.length));
print('row $i');
});
},
child: Padding(
padding: EdgeInsets.all(10.0),
child: Text('Row $i'),
),
);
}
}
建接收两个参数的 ListView.Builder,两个参数分别是列表的初始长度和一个 ItemBuilder 方法。ItemBuilder 方法和 Android adapter 里的 getView 方法类似;它通过位置返回你期望在这个位置渲染的列表项。
需要注意 onTap()
方法不再重建列表项,但是会执行 .add
操作。
12 文字处理
12.1 Text Widget 设置自定义字体
差异23:
Android 中可以创建一个字体资源文件并将其传给 TextView 的 FontFamily 参数;
Flutter中将字体文件放入一个文件夹,并在 pubspec.yaml 文件中引用它,就和导入图片一样。
fonts:
- family: MyCustomFont
fonts:
- asset: fonts/MyCustomFont.ttf
- style: italic
将字体赋值给你的 Text Widget:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: Center(
child: Text(
'This is a custom font text',
style: TextStyle(fontFamily: 'MyCustomFont'),
),
),
);
}
12.2 更改Text Widget样式
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing
13 表单输入
13.1 Input中的hint
Flutter中通过向 Text Widget 构造器的 decoration 参数传入一个 InputDecoration 对象来为输入框展示一个“提示”或占位文本:
body: Center(
child: TextField(
decoration: InputDecoration(hintText: 'This is a hint'),
)
)
12.2 显示验证错误的信息
class _SampleAppPageState extends State<SampleAppPage> {
String _errorText;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample App'),
),
body: Center(
child: TextField(
onSubmitted: (String text) {
setState(() {
if (!isEmail(text)) {
_errorText = 'Error: This is not an email';
} else {
_errorText = null;
}
});
},
decoration: InputDecoration(
hintText: 'This is a hint',
errorText: _getErrorText(),
),
),
),
);
}
String _getErrorText() {
return _errorText;
}
bool isEmail(String em) {
String emailRegexp =
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
RegExp regExp = RegExp(emailRegexp);
return regExp.hasMatch(em);
}
}
14 数据库和本地储存
14.1 使用SharedPreference
差异24:
Android中使用 SharedPreferences API 来存储少量的键值对;
Flutter中使用 Shared_Preferences 插件 实现此功能。这个插件同时包装了 Shared Preferences 和 NSUserDefaults(iOS 平台对应 API)的功能。
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment Counter'),
),
),
),
),
);
}
void _incrementCounter() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
prefs.setInt('counter', counter);
}
14.2 使用SQLite
差异25:
Android中使用 SQLite 来存储可以通过 SQL 进行查询的结构化数据;
Flutter中使用 SQFlite 插件实现此功能。
添加依赖:
dependencies:
...
sqflite: ^1.3.0
导入sqflite.dart
:
import 'package:sqflite/sqflite.dart';
打开数据库:
SQLite数据库是一个被路径定义在文件系统中的文件,可以通过getDatabasesPath()方法获取该路径。
var databasesPath = await getDatabasesPath();
var path = join(databasesPath, dbName);
// Make sure the directory exists
try {
await Directory(databasesPath).create(recursive: true);
} catch (_) {}
打开
var db = await openDatabase('my_db.db');
读写模式:读写模式是默认模式
配置:onConfigure
是第一个可选的回调调用。它允许执行数据库初始化,如支持级联删除
_onConfigure(Database db) async {
// Add support for cascade delete
await db.execute("PRAGMA foreign_keys = ON");
}
var db = await openDatabase(path, onConfigure: _onConfigure);
预加载数据:
_onCreate(Database db, int version) async {
// Database is created, create the table
await db.execute(
"CREATE TABLE Test (id INTEGER PRIMARY KEY, value TEXT)");
// populate data
await db.insert(...);
}
// Open the database, specifying a version and an onCreate callback
var db = await openDatabase(path,
version: 1,
onCreate: _onCreate);
只读模式
// open the database in read-only mode
var db = await openReadOnlyDatabase(path);
Handle Corruption
/// Check if a file is a valid database file
///
/// An empty file is a valid empty sqlite file
Future<bool> isDatabase(String path) async {
Database db;
bool isDatabase = false;
try {
db = await openReadOnlyDatabase(path);
int version = await db.getVersion();
if (version != null) {
isDatabase = true;
}
} catch (_) {} finally {
await db?.close();
}
return isDatabase;
}
防止数据库被锁
如果多次使用singleInstance: false
打开同一个数据库可能会出现:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
避免同时访问:
class Helper {
final String path;
Helper(this.path);
Future<Database> _db;
Future<Database> getDb() {
_db ??= _initDb();
return _db;
}
// Guaranteed to be called only once.
Future<Database> _initDb() async {
final db = await openDatabase(this.path);
// do "tons of stuff in async mode"
return db;
}
}
如果不需要数据库资源可以关闭释放:
await db.close();
迁移数据库:
第一版创建一个带有name列的Column表
/// Create tables
void _createTableCompanyV1(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute('''CREATE TABLE Company (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
)''');
}
// First version of the database
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
var batch = db.batch();
_createTableCompanyV1(batch);
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete));
第二版向Column实体中加入一个Employee和description列
/// Let's use FOREIGN KEY constraints
Future onConfigure(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
}
/// Create Company table V2
void _createTableCompanyV2(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Company');
batch.execute('''CREATE TABLE Company (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)''');
}
/// Update Company table V1 to V2
void _updateTableCompanyV1toV2(Batch batch) {
batch.execute('ALTER TABLE Company ADD description TEXT');
}
/// Create Employee table V2
void _createTableEmployeeV2(Batch batch) {
batch.execute('DROP TABLE IF EXISTS Employee');
batch.execute('''CREATE TABLE Employee (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
companyId INTEGER,
FOREIGN KEY (companyId) REFERENCES Company(id) ON DELETE CASCADE
)''');
}
// 2nd version of the database
db = await factory.openDatabase(path,
options: OpenDatabaseOptions(
version: 2,
onConfigure: onConfigure,
onCreate: (db, version) async {
var batch = db.batch();
// We create all the tables
_createTableCompanyV2(batch);
_createTableEmployeeV2(batch);
await batch.commit();
},
onUpgrade: (db, oldVersion, newVersion) async {
var batch = db.batch();
if (oldVersion == 1) {
// We update existing table and create the new tables
_updateTableCompanyV1toV2(batch);
_createTableEmployeeV2(batch);
}
await batch.commit();
},
onDowngrade: onDatabaseDowngradeDelete));
Raw SQL查询:
// Get a location using getDatabasesPath
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');
// Delete the database
await deleteDatabase(path);
// open the database
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
// When creating the db, create the table
await db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
// Insert some records in a transaction
await database.transaction((txn) async {
int id1 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
print('inserted1: $id1');
int id2 = await txn.rawInsert(
'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
['another name', 12345678, 3.1416]);
print('inserted2: $id2');
});
// Update some record
int count = await database.rawUpdate(
'UPDATE Test SET name = ?, value = ? WHERE name = ?',
['updated name', '9876', 'some name']);
print('updated: $count');
// Get the records
List<Map> list = await database.rawQuery('SELECT * FROM Test');
List<Map> expectedList = [
{'name': 'updated name', 'id': 1, 'value': 9876, 'num': 456.789},
{'name': 'another name', 'id': 2, 'value': 12345678, 'num': 3.1416}
];
print(list);
print(expectedList);
assert(const DeepCollectionEquality().equals(list, expectedList));
// Count the records
count = Sqflite
.firstIntValue(await database.rawQuery('SELECT COUNT(*) FROM Test'));
assert(count == 2);
// Delete a record
count = await database
.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
assert(count == 1);
// Close the database
await database.close();
SQL帮助:
final String tableTodo = 'todo';
final String columnId = '_id';
final String columnTitle = 'title';
final String columnDone = 'done';
class Todo {
int id;
String title;
bool done;
Map<String, Object?> toMap() {
var map = <String, Object?>{
columnTitle: title,
columnDone: done == true ? 1 : 0
};
if (id != null) {
map[columnId] = id;
}
return map;
}
Todo();
Todo.fromMap(Map<String, Object?> map) {
id = map[columnId];
title = map[columnTitle];
done = map[columnDone] == 1;
}
}
class TodoProvider {
Database db;
Future open(String path) async {
db = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute('''
create table $tableTodo (
$columnId integer primary key autoincrement,
$columnTitle text not null,
$columnDone integer not null)
''');
});
}
Future<Todo> insert(Todo todo) async {
todo.id = await db.insert(tableTodo, todo.toMap());
return todo;
}
Future<Todo> getTodo(int id) async {
List<Map> maps = await db.query(tableTodo,
columns: [columnId, columnDone, columnTitle],
where: '$columnId = ?',
whereArgs: [id]);
if (maps.length > 0) {
return Todo.fromMap(maps.first);
}
return null;
}
Future<int> delete(int id) async {
return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]);
}
Future<int> update(Todo todo) async {
return await db.update(tableTodo, todo.toMap(),
where: '$columnId = ?', whereArgs: [todo.id]);
}
Future close() async => db.close();
}
读取结果
Assuming the following read results:
List<Map<String, Object?>> records = await db.query('my_table');
Resulting map items are read-only
// get the first record
Map<String, Object?> mapRead = records.first;
// Update it in memory...this will throw an exception
mapRead['my_column'] = 1;
// Crash... `mapRead` is read-only
You need to create a new map if you want to modify it in memory:
// get the first record
Map<String, Object?> map = Map<String, Object?>.from(mapRead);
// Update it in memory now
map['my_column'] = 1;
事务(Transaction)
不要使用数据库,而只使用事务中的事务对象访问数据库。
await database.transaction((txn) async {
// Ok
await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');
// DON'T use the database object in a transaction
// this will deadlock!
await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});
如果回调不抛出错误,则会进行事务。如果出现错误,则事务将被取消。因此,单向回滚事务的一种方式就是抛出一个异常。