Flutter与Android中的对应
Flutter与Android中的对应
View相关
Flutter使用的是声明式UI,而Win32、Android或iOS框架中使用的是命令式UI,具体差异为:命令式UI需要开发者手动构建和控制一个UI控件的所有功能,然后在需要变更UI时,使用方法和设置器修改。而声明式UI只需要明确一个UI控件的当前状态即可,剩下的UI变换交给框架完成。
举个例子:
ViewB下包含的两个ViewC(c1和c2)需要变更成ViewB下包含一个ViewC(c3),并修改背景色。
命令式UI的写法应该是:
b.setColot(red);b.clearChildren();ViewC c3 = new View(...);b.add(c3);
而声明式UI的写法则是:
return ViewB { color: red, child: ViewC(...)}
View对应Flutter中的什么
在Android框架中,View是屏幕上显示的所有内容的基础,所有的控件(Button、Input等)都是一个View。在Flutter中,可以粗略的将Widget看做View的等价物,因为并不能完全映射到Android中的View。
- Widget没有生命周期,一经创建就是不可变的。
- 当Widget需要变更时,Flutter会重建一个新的Widget实例树。
- Widget本身不是视图,不直接绘制任何东西,只是对UI及其语义的描述。
Flutter包含了Material组件库和Cupertino组件库来构建Android或iOS风格的界面。
如何更新widget
在Android中,我们可以直接修改View来更新他们。然而在Flutter中,Widget是不可变的并且不能被直接更新,而是修改Widget的状态。
Flutter中的Widget分为有状态和无状态两种。
-
StatelessWidget 无状态控件。创建后就不会在运行中发生变更的UI。如用户的头像。
Text( 'I like Flutter!', style: TextStyle(fontWeight: FontWeight.bold),);
-
StatefulWidget 带状态控件。创建后需要依赖外部信息而动态修改的UI。如根据网络请求返回结果显示不同内容。
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), ), ); }}
如何对多个widget布局?有没有xml布局文件?
Flutter中没有xml布局文件,而是通过widget树布局。
@overrideWidget 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), ), ), );}
如何在布局中添加或删除文件
在Android中,可以通过父视图的addChild()或removeChild()方法来动态的添加、删除子视图。
在Flutter中,可以通过父视图的布尔值和返回值来修改绘制内容。
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), ), ); }}
如何对widget设置动画
在Android中可以使用XML创建动画或调用View的animate()方法。
在Flutter中,则使用动画库中支持动画的widget包裹想要实现动画的widget即可。
在Flutter中,使用AnimationController,它是一个动画,可以暂停,搜索,停止和反转动画。它需要一个Ticker,当vsync发生时发出信号,并在每个帧运行时产生0到1之间的线性插值。然后,您创建一个或多个动画并将它们附加到控制器。
例如,您可以使用CurvedAnimation沿插值曲线实现动画。从这个意义上说,控制器是动画进度的“主”源,而CurvedAnimation则计算代替控制器默认线性运动的曲线。像小部件一样,Flutter中的动画与组合一起工作。
构建窗口小部件树时,将动画分配给窗口小部件的动画属性,例如FadeTransition的不透明度,并告诉控制器启动动画。
下面的示例演示如何编写FadeTransition,当您按下FloatingActionButton时,该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 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(); }, ), ); }}
如何使用Canvas来绘制内容
在Android中,可以使用Drawable和Canvas来绘制图片和图形。Flutter也有相似的Canvas API,而且是基于更底层的绘制引擎Skia。Flutter有两个类来帮助在Canvas上绘制:CustomPaint和CustomPainter。后者是向Canvas绘制的算法实现。
下面是一个使用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;}
如何自定义构建widget
在Android中,经常使用自定义View子类或使用现有View,自定义实现方法的方式来达到自定义行为。
在Flutter中,通过将多个widget组合起来,而不是继承他们来构建一个自定义的widget。
下面的示例中自定义了一个显示指定文本的按钮。我们是创建了一个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)); }}@overrideWidget build(BuildContext context) { return Center( child: CustomButton("Hello"), );}
Intent相关
Intent对应Flutter中的什么
在Android中,Intent主要有两个作用:在Activity之间导航;和其他组件通信。
在Flutter中,没有Intent的概念,但是仍可以通过集成原生插件来发送一个Intent。
在Flutter中,并没有Activity和Fragment的直接对应。而是使用Navigator和Routes在多个界面之间导航,所有界面实际上在同一个Activity中。
Route是一个App的屏幕或页面的抽象,Navigator是管理routes的小部件。Route可以近似看做一个Activity,但是含义是不一样的。Navigator像一个栈,可以压入和弹出routes来切换屏幕显示。
在Android中,是在应用的AndroidManifest.xml文件中生命你的Activity。
在Flutter中,通过两个步骤在多个页面中导航:
- 构建一个routes,实际上是一个页面名称和页面内容的Map
- 通过Navigator直接导航到指定名称的页面
void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named '/' routes: { '/a': (BuildContext context) => MyPage(title: 'page A'), '/b': (BuildContext context) => MyPage(title: 'page B'), '/c': (BuildContext context) => MyPage(title: 'page C'), }, ));}Navigator.of(context).pushNamed('/b');
而其他经常使用的Intent的场景,如打开相机、文件选择器,则需要添加原生平台的插件。
如何在Flutter中响应其他App发送的Intent
Flutter可以直接处理请求进来的Intent,因为接受Intent的部分是通过Android原生层处理的。
然后需要用MethodChannel类将Intent传递过来的参数,等回调给Flutter层。
package com.example.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 { private 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); // Handle text being sent } } //添加一个"app.channel.shared.data"标识的回调 new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { //通过getSharedText标识共享数据 if (call.method.contentEquals("getSharedText")) { result.success(sharedText); sharedText = null; } } }); } void handleSendText(Intent intent) { sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); }}
然后在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 { 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 Scaffold(body: Center(child: Text(dataShared))); } getSharedText() async { //通过getSharedText标识获取共享数据 var sharedData = await platform.invokeMethod("getSharedText"); if (sharedData != null) { setState(() { dataShared = sharedData; }); } }}
startActivityForResult()对应什么
在Flutter中,Navigator类处理页面路由和页面返回时获取返回参数。获取页面返回参数就是通过等待push()操作的返回值实现的。
如下实例,打开一个定位页面,并等待返回定位结果。
Map coordinates = await Navigator.of(context).pushNamed('/location');
当定位完成后,在pop方法中添加经纬度参数。
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
异步UI
Flutter中的runOnUiThread()
Flutter中有一个单线程执行模型Isolate(它支持在其他的线程中运行Dart代码)、一个事件循环Looper、和异步执行。除非使用Isolate,否则所有Dart代码均在主线程中执行,而且也是由事件循环驱动的。Flutter中的Looper和Android主线程的Looper是等价的,也是被附加在主线程的。
在Flutter中,不用像再Android中那样,把所有耗时的操作都放到异步线程中执行。而是可以使用Dart提供的异步执行特性,例如async/await。例如,你可以使用async/await执行网络请求,而不会引起UI线程挂起。
loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); });}
一但等待的网络请求完成后,通过setState()方法来更新UI,触发重新绘制整个视图子树并更新数据。下面和这个例子模拟了一个网络请求后,将数据显示到列表中。
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 { 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"]}") ); } loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}
如何将任务移至后台线程
Flutter是一个单线程并且运行一个事件循环(类似于Node.js),我们不用担心线程挂你或线程切换。如果是简单的IO阻塞任务,例如磁盘访问、网络请求,仅仅使用async/await就可以了。
如果async/await不能满足需求,例如需要计算密集型任务,会导致CPU忙碌,你可以将它移动到Isolate来避免阻塞主线程的事件循环。
Isolate是单独执行线程,它不会共享主线程的堆内存。这也就意味着不能再主线程中访问Isolate线程的变量,或者在Isolate线程中调用setState()方法更新UI。
下面的示例中展示了一个简单的isolate,如果和主线程分享数据并更新UI。
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; });}static 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(json.decode(response.body)); }}Future sendReceive(SendPort port, msg) { ReceivePort response = ReceivePort(); port.send([msg, response.sendPort]); return response.first;}
dataLoader()是Isolate在单独线程执行的入口,可以在里面执行CPU密集型运算,如json解析、加解密等。
下面是一个可以执行的完整示例。
import 'dart:convert';import 'package:flutter/material.dart';import 'package:http/http.dart' as http;import 'dart:async';import 'dart:isolate';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 { List widgets = []; @override void initState() { super.initState(); loadData(); } showLoadingDialog() { if (widgets.length == 0) { return true; } return false; } getBody() { if (showLoadingDialog()) { return getProgressDialog(); } else { return getListView(); } } getProgressDialog() { return Center(child: CircularProgressIndicator()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: getBody()); } ListView getListView() => 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"]}")); } 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 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(json.decode(response.body)); } } Future sendReceive(SendPort port, msg) { ReceivePort response = ReceivePort(); port.send([msg, response.sendPort]); return response.first; }}
在Flutter中OkHttp的等价物
在Flutter中,可以使用一个流行的http package非常容易的创建一个网络请求。虽然http package没有实现OkHttp中所有的特性,但它抽象了你通常自己实现的大部分网络,使其成为一种简单的网络呼叫方式。
在pubspec.yaml中添加依赖。
dependencies: ... http: ^0.11.3+16
在http.get()方法上调用await发起一个网络请求。
import 'dart:convert';import 'package:flutter/material.dart';import 'package:http/http.dart' as http;[...] loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}
为长时间的任务显示进度
在Flutter中使用ProgressIndicator小部件来显示任务的执行进度。
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 { List widgets = []; @override void initState() { super.initState(); loadData(); } showLoadingDialog() { return widgets.length == 0; } getBody() { if (showLoadingDialog()) { return getProgressDialog(); } else { return getListView(); } } getProgressDialog() { return Center(child: CircularProgressIndicator()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: getBody()); } ListView getListView() => 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"]}")); } loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = json.decode(response.body); }); }}
工程结构和资源
在哪存储与分辨率相关的图片文件
在Android中,resources和assets是两种不同类型的资源,也会被不同的对待,而在Flutter app中只有assets。
Flutter遵循简单的基于屏幕密度的格式,类似于iOS。assets可能是1.0x,2.0x,3.0x或其他倍数。Flutter没有dp单位,但是有逻辑像素,这和与设备无关的像素基本相同。其中,devicePixelRatio表示单个逻辑像素中的物理像素的比率。
下面表格说明了Android中的分辨率限定符和Flutter像素比率的对应关系。
Android密度限定符 | Flutter像素比率 |
---|---|
ldpi | 0.75x |
mdpi | 1.0x |
hdpi | 1.5x |
xhdpi | 2.0x |
xxhdpi | 3.0x |
xxxhdpi | 4.0x |
在Flutter1.0 beta2之前,Flutter中定义的资源不能被原生代码访问,原生资源也能在Flutter中访问,因为它们位于不同的文件夹中。
从Flutter beta2开始,assets资源被存储在原生的asset文件夹中,可以在Android中使用原生API AssetManager访问它们。
val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")
为了在Flutter中也能访问它们,需要在pubspec.yaml文件中逐一添加声明。
在Flutter中的资源分为两种,一种是项目自有资源,第二种是外部依赖包中的资源。当使用第二种资源时需要指定包名,而第一种不能指定。
//项目资源路径为{project}/images/a_dot_burr.jpeg//实际资源路径为{apk}/assets/flutter_assets/images/a_dot_burr.jpegreturn AssetImage("images/a_dot_burr.jpeg");//资源路径为{project}/packages/extrapackagename/images/a_dot_burr.jpeg//实际资源路径为{apk}/assets/flutter_assets/packages/extrapackagename/images/a_dot_burr.jpegreturn AssetImage("images/a_dot_burr.jpeg", package:'extrapackagename');
在哪存储字符串资源并处理国际化
在Flutter中并没有类似Android中的资源管理系统,所以当前比较好的处理方式是将字符串资源统一写到一个类中,并以静态常量方式去访问。
class Strings { static String welcomeMessage = "Welcome To Flutter";}Text(Strings.welcomeMessage)
至于国际化和本地化的处理可以使用intl库。该软件包提供国际化和本地化功能,包括文本转换,复数和性别,日期/数字格式和解析以及双向文本。
字符串资源国际化实现简单来说分为三步:
- 追踪当前设备的语言
- 根据语言动态加载字符串资源到一个Map集合中
- 使用key,从Map中获取指定字符串
Gradle的等价物是什么以及如何添加依赖
在Android中,使用Gradle构建脚本添加依赖。
在Flutter中,使用Dart自带的构建系统和Pub包管理。这个工具代理了Android和iOS各自平台构建原生代码的过程。
当需要为原生平台单独添加依赖时,可以在Flutter工程下对应的android/iOS目录下找到对应的依赖文件。
一般情况下,只需要在pubspec.yaml文件中添加Flutter依赖。
Activity和Fragment
Activity和Fragment在Flutter中的等价物
在Flutter中,没有Activity和Fragment,所有页面均是在一个Activity(FlutterActivity)显示的,显示的控件继承自SurfaceView,SurfaceView里显示的所有内容都是由widget组合出来的。页面切换和控件操作均是通过设置一个或多个widget不同的状态和返回值做到的。
如何监听Android中Activity的声明周期
在Android中,我们可以通过覆盖Activity的声明周期方法或在Application中注册ActivityLifecycleCallbacks回调来获取Activity的声明周期变化。
当然,Flutter的设计者也将原生应用的声明周期事件传递到了Flutter中,只不过对大部分事件做了屏蔽,最终可监测到的事件只有四个:
- inactive 应用处于未活动状态,不会接受用户输入。该事件是iOS专用的,Android平台没有对应的映射状态
- paused 应用对用户不可见,不会响应用户输入,并且在后台运行。对应Android中的onPause()事件。
- resumed 应用可见并且可以响应用户输入。对应Android中onPostResume()事件
- suspending 应用被短暂的挂起,对应Android中的onStop()事件。但是在iOS中没有对应映射,所以在iOS平台上不会触发此事件
为什么Flutter没有将所有生命周期事件开放给调用者?
因为Flutter负责为开发者启动和关闭绘制引擎。在大多数情况下,几乎没有理由需要观察Flutter侧的活动生命周期。如果需要依据生命周期获取或释放native资源,那么应该去native侧去做。
下面代码展示了如何监测仅有的四个生命周期。
import 'package:flutter/widgets.dart';class LifecycleWatcher extends StatefulWidget { @override _LifecycleWatcherState createState() => _LifecycleWatcherState();}class _LifecycleWatcherState extends State 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()));}
布局
LinearLayout的替代品
在Flutter中,使用Row widget和Column widget达到和LinearLayout相同的效果。
@overrideWidget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], );}@overrideWidget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Column One'), Text('Column Two'), Text('Column Three'), Text('Column Four'), ], );}
RelativeLayout的替代品
在Flutter中有多钟方式可以实现RelativeLayout布局的效果。
- 使用Column和Row的组合
- 使用Stack小部件
下面是使用Stack的示例
class _HomeScreenState extends State { PageController _pageController = new PageController(initialPage: 2); @override build(BuildContext context) { final Map pages = { 'My Music': new Center( child: new Text('My Music not implemented'), ), 'Shared': new Center( child: new Text('Shared not implemented'), ), 'Feed': new Feed(), }; TextTheme textTheme = Theme .of(context) .textTheme; return new Stack( children: [ new Container( decoration: new BoxDecoration( gradient: new LinearGradient( begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, colors: [ const Color.fromARGB(255, 253, 72, 72), const Color.fromARGB(255, 87, 97, 249), ], stops: [0.0, 1.0], ) ), child: new Align( alignment: FractionalOffset.bottomCenter, child: new Container( padding: const EdgeInsets.all(10.0), child: new Text( 'T I Z E', style: textTheme.headline.copyWith( color: Colors.grey.shade800.withOpacity(0.8), fontWeight: FontWeight.bold, ), ), ) ) ), new Scaffold( backgroundColor: const Color(0x00000000), appBar: new AppBar( backgroundColor: const Color(0x00000000), elevation: 0.0, leading: new Center( child: new ClipOval( child: new Image.network( 'http://i.imgur.com/TtNPTe0.jpg', ), ), ), actions: [ new IconButton( icon: new Icon(Icons.add), onPressed: () { // TODO: implement }, ), ], title: const Text('tofu\'s songs'), bottom: new CustomTabBar( pageController: _pageController, pageNames: pages.keys.toList(), ), ), body: new PageView( controller: _pageController, children: pages.values.toList(), ), ), ], ); }}
ScrollView的替代品
在Flutter中使用ListView小部件达到ScrollView和RecycleView的效果。
@overrideWidget build(BuildContext context) { return ListView( children: [ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], );}
在Flutter中如何处理屏幕朝向
如果AndroidManifest.xml包含以下内容,FlutterView将处理配置更改:
android:configChanges="orientation|screenSize"
手势检测和触摸事件处理
如何为widget添加点击事件
在Android中可以通过setOnClinkListener为View添加点击事件。
在Flutter中有两种方式添加触摸事件:
-
对于支持事件监测的widget,直接添加一个回调方法即可。如RaisedButton包含一个onPressed参数。
@overrideWidget build(BuildContext context) { return RaisedButton( onPressed: () { print("click"); }, child: Text("Button"));}
-
如果widget不支持事件检测,则需要包裹一个GestureDetector小部件,然后给onTap参数设置一个回调方法。
class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: GestureDetector( child: FlutterLogo( size: 200.0, ), onTap: () { print("tap"); }, ), )); }}
如何为widget检测其他手势
使用GestureDetector除了可以检测点击事件,还可以检测其他几种手势。
-
Tap 单击
1.1 onTapDown 按下
1.2 onTapUp 抬起
1.3 onTap 点击
1.4 onTapCancel 点击取消 -
Double tap 双击
2.1 onDoubleTap
-
Long press 长按
3.1 onLongPree
-
Vertical drag 垂直拖拽
4.1 onVerticalDragStart 开始拖拽
4.2 onVerticalDragUpdate 拖拽位置更新
4.3 onVerticalDragEnd 拖拽结束 -
Horizontal drag 水平拖拽
5.1 onHorizontalDragStart 开始
5.2 onHorizontalDragUpdate 更新
5.3 onHorizontalDragEnd 结束
下面代码显示了使用GestureDetector的双击手势翻转一个图片。
AnimationController controller;CurvedAnimation curve;@overridevoid initState() { controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this); curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);}class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: GestureDetector( child: RotationTransition( turns: curve, child: FlutterLogo( size: 200.0, )), onDoubleTap: () { if (controller.isCompleted) { controller.reverse(); } else { controller.forward(); } }, ), )); }}
列表和适配器
ListView的替代品
前面在ScrollView的部分说到,在Flutter中的ListView的替代品也是 ListView。
Android中的ListView用到了Item布局重用和回收的技术,防止出现错乱和内存占用的问题。
在Flutter中,由于widget不可变的特性,将由Flutter负责快速平滑的滚动。
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 { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } _getListData() { List widgets = []; for (int i = 0; i < 100; i++) { widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i"))); } return widgets; }}
如何知道点击哪一个Item
在Android中有两种为列表设置点击事件的方式:第一种是为ListView添加点击事件,由ListView计算当前点击位置。第二种是每个ItemView添加点击事件。
在Flutter中使用的是第二种方式。
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 { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } _getListData() { List widgets = []; for (int i = 0; i < 100; i++) { widgets.add(GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { print('row tapped'); }, )); } return widgets; }}
异步刷新ListView
在Android中,可以通过更新Adapter的数据源,然后调用notifyDataSetChanged方法通知页面刷新。
在Flutter中,如果要更新setState()中的小部件列表,您会很快发现您的数据没有直观地改变。 这是因为当调用setState()时,Flutter渲染引擎会查看窗口小部件树以查看是否有任何更改。 当它到达ListView时,它执行==检查,并确定两个ListView是相同的。 没有任何改变,因此不需要更新。
有关更新ListView的简单方法,请在setState()内创建一个新List,并将旧列表中的数据复制到新列表中。 虽然这种方法很简单,但不建议用于大型数据集,如下一个示例所示。
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 { List 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( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { setState(() { widgets = List.from(widgets); widgets.add(getRow(widgets.length + 1)); print('row $i'); }); }, ); }}
当数据集比较大或列表动态加载时,需要使用更高效的方式ListView.Builder。这种方式基本上相当于Android的RecycleView,可以为你动态回收列表元素。
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 { List 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( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i")), onTap: () { setState(() { widgets.add(getRow(widgets.length + 1)); print('row $i'); }); }, ); }}
使用ListView.builder更高效的原因是:1.没有重新创建ListView。2.没有重新创建item列表,而是直接添加。
ListView.builder中的itemBuilder参数类似于Android 列表适配器中的getView方法,它根据指定位置并返回需要渲染的widget。
使用文本
在Text视图上自定义字体
在Android SDK(从Android O开始)中,您创建一个Font资源文件并将其传递给TextView的FontFamily参数。
在Flutter中,将字体文件放在文件夹中,并在pubspec.yaml文件中引用它,类似于导入图像的方式。
fonts: - family: MyCustomFont fonts: - asset: fonts/MyCustomFont.ttf - style: italic
然后给Text小部件赋值字体。
@overrideWidget 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'), ), ), );}
如何设置Text视图的样式
除了字体,你可以为Text小部件自定义其他样式属性。
- fontSize
- fontStyle
- fontFamily
- fontWeight
- color
- height
- inherit 继承
- letterSpacing 字符间距
- textBaseline 文本基线
- wordSpacing 字间距
- hashCode
- decoration
- decorationColor
- decorationStyle
表单输入
输入框中hint的等价物
使用InputDecoration为一个Text小部件添加一个hintText属性。
body: Center( child: TextField( decoration: InputDecoration(hintText: "This is a hint"), ))
如何显示文本非法的错误描述
类似于hint,向InputDecoration添加一个errorText参数。
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 { 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()), ), ), ); } _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); }}
Flutter插件
访问GPS传感器
使用geolocator插件
这个插件提供了访问各个平台的定位服务的功能,具体包含如下属性:
- 获取当前设备位置
- 获取上一次定位
- 获取位置的持续更新
- 检查设备的定位服务是否开启
- 地址与地理坐标的相互转换
- 计算两个坐标之间的距离
- 在Android平台上,插件Google Play服务是否可用
添加依赖
dependencies: geolocator: '^2.1.1'
定位
//获取当前位置import 'package:geolocator/geolocator.dart';Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);//获取上次定位位置Position position = await Geolocator().getLastKnownPosition(desiredAccuracy: LocationAccuracy.high);//监听位置更新var geolocator = Geolocator();var locationOptions = LocationOptions(accuracy: LocationAccuracy.high, distanceFilter: 10);StreamSubscription positionStream = geolocator.getPositionStream(locationOptions).listen( (Position position) { print(_position == null ? 'Unknown' : _position.latitude.toString() + ', ' + _position.longitude.toString()); }); //检查定位服务是否开启GeolocationStatus geolocationStatus = await Geolocator().checkGeolocationPermissionStatus();//如果不使用google的定位服务,可以设置强制使用Android平台的定位管理Geolocator geolocator = Geolocator()..forceAndroidLocationManager = true;GeolocationStatus geolocationStatus = await geolocator.checkGeolocationPermissionStatus();
地址转换
import 'package:geolocator/geolocator.dart';//地址转经纬度List placemark = await Geolocator().placemarkFromAddress("Gronausestraat 710, Enschede");//经纬度转地址List placemark = await Geolocator().placemarkFromCoordinates(52.2165157, 6.9437819);
上面两个方法还可以接受一个可选参数,用来指定语言和国家,示例格式如下:
区域标识 | 描述 |
---|---|
en | 全英 |
en_US | 美式英语 |
en_UK | 英式英语 |
nl_NL | |
nl_BE |
距离计算
import 'package:geolocator/geolocator.dart';//两个经纬度位置的距离 startLatitude,startLongitude,endLatitude,endLongitudedouble distanceInMeters = await Geolocator().distanceBetween(52.2165157, 6.9437819, 52.3546274, 4.8285838);
权限声明
Android
iOS
NSLocationAlwaysUsageDescription This app needs access to location when in the background. NSLocationAlwaysAndWhenInUseUsageDescription This app needs access to location when open and in the background.
定位经度说明
Android | iOS | |
---|---|---|
lowest | 500m | 3000m |
low | 500m | 1000m |
medium | 100 - 500m | 100m |
high | 0 - 100m | 10m |
best | 0 - 100m | ~0m |
bestForNavigation | 0 - 100m | Optimized for navigation |
访问相机
使用image_picker来从相册中选择图片或通过相机拍照。
添加依赖
dependencies: image_picker: '^0.4.10'
iOS设备还要添加额外的keys到info.plist文件中。
- NSPhotoLibraryUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor.
- NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
- NSMicrophoneUsageDescription - describe why your app needs access to the microphone, if you intend to record videos. This is called Privacy - Microphone Usage Description in the visual editor.
示例
import 'package:image_picker/image_picker.dart';class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => new _MyHomePageState();}class _MyHomePageState extends State { File _image; Future getImage() async { var image = await ImagePicker.pickImage(source: ImageSource.camera); setState(() { _image = image; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Image Picker Example'), ), body: new Center( child: _image == null ? new Text('No image selected.') : new Image.file(_image), ), floatingActionButton: new FloatingActionButton( onPressed: getImage, tooltip: 'Pick Image', child: new Icon(Icons.add_a_photo), ), ); }}
在Flutter应用中使用NDK
当前在Flutter还不支持直接访问c/c++代码。
只能通过编写插件,先调用Android的代码,再通过jni调用c/c++代码。
主题
如何为App设置主题
开箱即用,Flutter带有一个漂亮的Material Design实现,它可以满足您通常所需的大量样式和主题需求。与在Android中声明主题然后使用AndroidManifest.xml将其分配给应用程序的Android不同,在Flutter中,您可以在顶级窗口小部件中声明主题。
要在应用程序中充分利用Material Components,您可以将顶级窗口小部件MaterialApp声明为应用程序的入口点。 MaterialApp是一个便利小部件,它包含了许多小部件,这些小部件通常是实现Material Design的应用程序所必需的。它通过添加特定于材料的功能构建在WidgetsApp上。
您还可以使用WidgetApp作为应用程序小部件,它提供了一些相同的功能,但不如MaterialApp丰富。
要自定义任何子组件的颜色和样式,请将ThemeData对象传递给MaterialApp小部件。例如,在下面的代码中,主样本设置为蓝色,文本选择颜色为红色。
class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, textSelectionColor: Colors.red ), home: SampleAppPage(), ); }}
数据库和本地存储
如何访问SharedPreferences
在Android中,您可以使用SharedPreferences API存储一小组键值对。
在Flutter中,使用Shared_Preferences插件访问此功能。 此插件包含共享首选项和NSUserDefaults(iOS等效项)的功能。
import 'package:flutter/material.dart';import 'package:shared_preferences/shared_preferences.dart';void main() { runApp( MaterialApp( home: Scaffold( body: Center( child: RaisedButton( onPressed: _incrementCounter, child: Text('Increment Counter'), ), ), ), ), );}_incrementCounter() async { SharedPreferences prefs = await SharedPreferences.getInstance(); int counter = (prefs.getInt('counter') ?? 0) + 1; print('Pressed $counter times.'); prefs.setInt('counter', counter);}
如何访问SQLite
使用SQFlite插件,同时支持Android和iOS。
- 支持事务和批处理
- 开启时自动版本管理
- 更删改查的帮助类
- 在后台线程执行数据库操作
添加依赖
dependencies: ... sqflite: any
原始sql查询
// Get a location using getDatabasesPathvar databasesPath = await getDatabasesPath();String path = join(databasesPath, 'demo.db');// Delete the databaseawait deleteDatabase(path);// open the databaseDatabase 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 transactionawait 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 recordint count = await database.rawUpdate( 'UPDATE Test SET name = ?, VALUE = ? WHERE name = ?', ['updated name', '9876', 'some name']);print('updated: $count');// Get the recordsList
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 toMap() { var map = { columnTitle: title, columnDone: done == true ? 1 : 0 }; if (id != null) { map[columnId] = id; } return map; } Todo(); Todo.fromMap(Map 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 insert(Todo todo) async { todo.id = await db.insert(tableTodo, todo.toMap()); return todo; } Future getTodo(int id) async { List 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 delete(int id) async { return await db.delete(tableTodo, where: '$columnId = ?', whereArgs: [id]); } Future update(Todo todo) async { return await db.update(tableTodo, todo.toMap(), where: '$columnId = ?', whereArgs: [todo.id]); } Future close() async => db.close();}
事务
不要在事务过程中直接操作数据库。
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)');});
批处理
为避免在dart和原生代码中频繁通信,可以使用批处理:
batch = db.batch();batch.insert('Test', {'name': 'item'});batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);batch.delete('Test', where: 'name = ?', whereArgs: ['item']);results = await batch.commit();
如果不关心批处理的返回值,可以为commit添加一个参数:
await batch.commit(noResult: true);
当把批处理添加到事务中时,批处理的执行时机是等待事务的提交。
await database.transaction((txn) async { var batch = txn.batch(); // ... // commit but the actual commit will happen when the transaction is commited // however the data is available in this transaction await batch.commit(); // ...});
在批处理过程中,可以设置忽略错误而继续执行。
await batch.commit(continueOnError: true);
表名和列名
一般来说,最好避免使用SQLite关键字作为实体名字。
"add","all","alter","and","as","autoincrement","between","case","check","collate","commit","constraint","create","default","deferrable","delete","distinct","drop","else","escape","except","exists","foreign","from","group","having","if","in","index","insert","intersect","into","is","isnull","join","limit","not","notnull","null","on","or","order","primary","references","select","set","table","then","to","transaction","union","unique","update","using","values","when","where"
支持的SQLite数据类型
SQLite不支持DateTime和bool类型,可以用int/string和INTEGER代替
INTERGET int类型,-263~263-1
REAL num类型
TEXT String类型
BLOB Unit8List类型
通知
如何设置推送通知
目前只能使用firebase_messaging插件来使用Firebase云消息推送。
更多相关文章
- Android中Touch事件分析--解决HorizontalScrollView滑动和按钮事
- Android中aar和jar文件的认识
- Android中的脑残设计
- 在android中使用proguard混淆代码出现“Conversion to Dalvik fo
- 整合谷歌Admob在Unity3D的Android和iOS游戏
- android view 上下左右滑动 事件
- android ContentProvider 使用实例
- Android模仿QQ空间图片上传——原理
- android click事件注解