Flutter是Google一個(gè)新的用于構(gòu)建跨平臺(tái)的手機(jī)App的SDK。寫一份代碼,在Android 和iOS平臺(tái)上都可以運(yùn)行。
一.Flutter項(xiàng)目結(jié)構(gòu)
配好環(huán)境 Android Studio安裝好插件可以直接創(chuàng)建Flutter項(xiàng)目,項(xiàng)目主要有以下幾個(gè)目錄:

打開android文件夾就可以看到比較熟悉的Android項(xiàng)目結(jié)構(gòu)
并且其內(nèi)容只有一個(gè)繼承于FlutterActivity的MainActivity
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
也就是說不管Flutter包含多少個(gè)界面,其在Android上都是在一個(gè)Ativity中完成
那么問題來了,F(xiàn)lutter和activity里的界面是什么關(guān)系,怎么承載的,可以看到FlutterActivity的onCreate()方法:在確定Activity視圖的時(shí)候調(diào)用了createFlutterView()方法
protected void onCreate(@Nullable Bundle savedInstanceState) {
....
this.setContentView(this.createFlutterView());
....
}
createFlutterView()方法 返回了一個(gè)View作為Activity視圖
@NonNull
private View createFlutterView() {
return this.delegate.onCreateView((LayoutInflater)null, (ViewGroup)null, (Bundle)null);
}
再進(jìn)一步可以看到onCreateView()源碼中返回了一個(gè)通過FlutterView創(chuàng)建的FlutterSplashView對(duì)象。
FlutterView是繼承于FrameLayout的一個(gè)自定義View。
所以Flutter的所有界面只存放于Activity的一個(gè)View,也就是說可以通過FlutterView來將Flutter項(xiàng)目界面引入原生Android。
在原生Android項(xiàng)目中創(chuàng)建Flutter Module,并配置好依賴引入項(xiàng)目后,可以通過以下方式,完成原生Android和Flutter的交互:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 通過FlutterView引入Flutter編寫的頁面
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
layout.leftMargin = 100;
layout.topMargin = 200;
addContentView(flutterView, layout);
}
在Flutter.createView(this, getLifecycle(), "route1");傳入了三個(gè)參數(shù)分別是context,生命周期和路由值
在Flutter項(xiàng)目中就可以通過這個(gè)路由值來確定要顯示哪個(gè)界面。

問題1.數(shù)據(jù)怎么傳遞?
問題2.方法調(diào)用?Flutter調(diào)用原生的JAVA或者Kotlin方法?例如Flutter調(diào)用原生Android網(wǎng)絡(luò)請(qǐng)求的方法
二.Flutter入口和界面構(gòu)建
2.1 Flutter項(xiàng)目入口
Flutter項(xiàng)目的代碼全部都在lib文件夾下,其入口就是main.dart

這里可以看到,在main函數(shù)啟動(dòng)app后,構(gòu)建了一個(gè)MaterialApp對(duì)象,并通過構(gòu)造函數(shù)指定參數(shù)名的方式,指定了一些屬性。它繼承于Widget,所以可以在build中直接返回。
Widget就像是Android開發(fā)中的View,但是它的概念比View更廣。不光是所有的界面控件都是繼承與Widget,還包括布局比如線性布局,層疊布局。甚至還包括一些定位的控件比如Center。
在構(gòu)建MaterialApp對(duì)象的時(shí)候,通過其home字段指定了主界面內(nèi)容HomePage(),這個(gè)就是自己創(chuàng)建的主界面了
2.2 Flutter界面構(gòu)建
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context); //demo app 無設(shè)計(jì)稿
return Scaffold(
//將Demo入口列表封裝在一個(gè)方法中 以后好改
body: _getDemoList());
}
Widget _getDemoList() {
return Container(
padding: EdgeInsets.all(2),
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5, //橫軸間隔
mainAxisSpacing: 5, //主軸間隔
),
children: <Widget>[
OutlineButton.icon(
padding: EdgeInsets.all(0),
icon: Icon(Icons.http),
label: Text("網(wǎng)絡(luò)請(qǐng)求"),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return WeatherPage();
}));
},
),
OutlineButton.icon(
padding: EdgeInsets.all(0),
icon: Icon(Icons.format_align_justify),
label: Text("數(shù)據(jù)庫操作"),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return DatabasePage();
}));
},
),
OutlineButton.icon(
padding: EdgeInsets.all(0),
icon: Icon(Icons.camera_alt),
label: Text("拍照\n(文件操作)",textAlign: TextAlign.center,),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return CameraPage();
}));
},
),
OutlineButton.icon(
padding: EdgeInsets.all(0),
icon: Icon(Icons.settings_overscan),
label: Text("掃碼"),
onPressed: () {},
),
],
),
);
}
}

在數(shù)百個(gè)Widget中靈活選擇使用可以構(gòu)建出好看的界面:

理論上Flutter所有代碼都可以寫在一個(gè)dart文件中,但這會(huì)導(dǎo)致代碼可讀性非常低,界面構(gòu)建代碼出現(xiàn)大堆的縮進(jìn),非?;靵y。所以還是要多封裝一下。
2.3 Flutter界面狀態(tài)管理
界面也是Widget,Widget分為“有狀態(tài)的"和"沒有狀態(tài)的",比較一下MyApp和HomePage:
//無狀態(tài)
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
.............
}
}
//有狀態(tài)
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
..................
}
}
StatelessWidget 中直接build構(gòu)建界面。而StatefulWidget 中則是通過繼承與State的類來build界面并進(jìn)行界面狀態(tài)管理。
StatelessWidget 界面構(gòu)建時(shí)的什么樣就一直是什么樣
例如在屏幕中間顯示一句話:
//無狀態(tài)
class TestPage extends StatelessWidget {
var msg="Hello world";
@override
Widget build(BuildContext context) {
return Center(child: Text(msg),);
}
}
這個(gè)時(shí)候中間的內(nèi)容無法改變
StatefulWidget 則可以setState
//有狀態(tài)
class TestPage extends StatefulWidget {
@override
_TestPage State createState() => _TestPage State();
}
class _TestPage State extends State<TestPage > {
var msg="Hello world";
@override
Widget build(BuildContext context) {
return Center(child: Text(msg),);
}
void changeMsg(){
setState(() {
msg="Nice to meet you!";
});
}
}
二.Flutter中的網(wǎng)絡(luò)請(qǐng)求
http://m.itdecent.cn/p/8ed5283de696
三.Flutter持久化存儲(chǔ)
2.1 單個(gè)字段存儲(chǔ)
封裝了一個(gè)工具類:
import 'dart:convert';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synchronized/synchronized.dart';
class MySpHelper {
static MySpHelper _intance;
static SharedPreferences _prefs;
static Lock _lock = Lock();
static Future<MySpHelper> getInstance() async {
if (_intance == null) {
await _lock.synchronized(() async {
if (_intance == null) {
// 保持本地實(shí)例直到完全初始化。
var instance = MySpHelper._();
await instance._init();
_intance = instance;
}
});
}
return _intance;
}
// 私有構(gòu)造函數(shù)
MySpHelper._();
Future _init() async {
_prefs = await SharedPreferences.getInstance();
}
/// put object.
static Future<bool> putObject(String key, Object value) {
if (_prefs == null) return null;
return _prefs.setString(key, value == null ? "" : json.encode(value));
}
/// get obj.
static T getObj<T>(String key, T f(Map v), {T defValue}) {
Map map = getObject(key);
return map == null ? defValue : f(map);
}
/// get object.
static Map getObject(String key) {
if (_prefs == null) return null;
String _data = _prefs.getString(key);
return (_data == null || _data.isEmpty) ? null : json.decode(_data);
}
/// put object list.
static Future<bool> putObjectList(String key, List<Object> list) {
if (_prefs == null) return null;
List<String> _dataList = list?.map((value) {
return json.encode(value);
})?.toList();
return _prefs.setStringList(key, _dataList);
}
/// get obj list.
static List<T> getObjList<T>(String key, T f(Map v),
{List<T> defValue = const []}) {
List<Map> dataList = getObjectList(key);
List<T> list = dataList?.map((value) {
return f(value);
})?.toList();
return list ?? defValue;
}
/// get object list.
static List<Map> getObjectList(String key) {
if (_prefs == null) return null;
List<String> dataLis = _prefs.getStringList(key);
return dataLis?.map((value) {
Map _dataMap = json.decode(value);
return _dataMap;
})?.toList();
}
/// get string.
static String getString(String key, {String defValue = ''}) {
if (_prefs == null) return defValue;
return _prefs.getString(key) ?? defValue;
}
/// put string.
static Future<bool> putString(String key, String value) {
if (_prefs == null) return null;
return _prefs.setString(key, value);
}
/// get bool.
static bool getBool(String key, {bool defValue = false}) {
if (_prefs == null) return defValue;
return _prefs.getBool(key) ?? defValue;
}
/// put bool.
static Future<bool> putBool(String key, bool value) {
if (_prefs == null) return null;
return _prefs.setBool(key, value);
}
/// get int.
static int getInt(String key, {int defValue = 0}) {
if (_prefs == null) return defValue;
return _prefs.getInt(key) ?? defValue;
}
/// put int.
static Future<bool> putInt(String key, int value) {
if (_prefs == null) return null;
return _prefs.setInt(key, value);
}
/// get double.
static double getDouble(String key, {double defValue = 0.0}) {
if (_prefs == null) return defValue;
return _prefs.getDouble(key) ?? defValue;
}
/// put double.
static Future<bool> putDouble(String key, double value) {
if (_prefs == null) return null;
return _prefs.setDouble(key, value);
}
/// get string list.
static List<String> getStringList(String key,
{List<String> defValue = const []}) {
if (_prefs == null) return defValue;
return _prefs.getStringList(key) ?? defValue;
}
/// put string list.
static Future<bool> putStringList(String key, List<String> value) {
if (_prefs == null) return null;
return _prefs.setStringList(key, value);
}
/// get dynamic.
static dynamic getDynamic(String key, {Object defValue}) {
if (_prefs == null) return defValue;
return _prefs.get(key) ?? defValue;
}
/// have key.
static bool haveKey(String key) {
if (_prefs == null) return null;
return _prefs.getKeys().contains(key);
}
/// get keys.
static Set<String> getKeys() {
if (_prefs == null) return null;
return _prefs.getKeys();
}
/// remove.
static Future<bool> remove(String key) {
if (_prefs == null) return null;
return _prefs.remove(key);
}
/// clear.
static Future<bool> clear() {
if (_prefs == null) return null;
return _prefs.clear();
}
///Sp is initialized.
static bool isInitialized() {
return _prefs != null;
}
}
用法:異步操作
void testSPHelper() async {
//初始化
await MySpHelper.getInstance();
//存儲(chǔ)
MySpHelper.putString("data_key", "10010");
//讀取
String msg = MySpHelper.getString("data_key");
print(msg);
}
和Android基本一樣
2.2 本地?cái)?shù)據(jù)庫
搭建兩個(gè)基本的工具類
SqlManager: 創(chuàng)建數(shù)據(jù)庫,管理數(shù)據(jù)庫名,管理數(shù)據(jù)庫版本等操作
BaseDbProvider:表管理基類,確定數(shù)據(jù)庫,完成一些表的基本操作比如創(chuàng)建表、查詢所有數(shù)據(jù)、清空表等等
為每張表的管理創(chuàng)建一個(gè)Provider,繼承與BaseDbProvider ,在這里配置表字段和完成一些業(yè)務(wù)上的處理。
class ProductDbProvider extends BaseDbProvider {
final String table = "ProductInfo"; //表名 產(chǎn)品信息
final String id = "id"; //id
final String name = "name"; //產(chǎn)品名稱
final String price = "price"; //價(jià)格
@override
createTableString() {
//建表sql
return '''
create table $table (
$id integer primary key,$name text not null,
$price integer not null)
''';
}
@override
tableName() {
return table;
}
//查 根據(jù)主鍵查詢
Future<List<Map<String, dynamic>>> queryByKey(
//查詢語句通常都是查詢多行 多個(gè)結(jié)果 每行的結(jié)果以一個(gè)map的形式表現(xiàn)
int key) async {
Database db = await getDataBase();
List<Map<String, dynamic>> maps =
await db.rawQuery("select * from $table where $id = $key");
return maps;
}
//改
Future<void> update(ProductEntity entity) async {
//將一個(gè)id更新
Database database = await getDataBase();
await database.rawUpdate('''update $table set
$name = ?,
$price = ?
where $id= ?''', [
entity.name,
entity.price,
entity.id,
]);
}
//增
Future insert(ProductEntity entity) async {
print("本次添加的數(shù)據(jù)name${entity.name}");
Database db = await getDataBase();
//檢查此主鍵是否已有數(shù)據(jù)
if (queryByKey(entity.id) != null) {
//有兩種方式操作數(shù)據(jù)庫 一種如下 還有一種是直接執(zhí)行原生的sql語句
//如果已存在數(shù)據(jù) 則刪除
await db.delete(table, where: "$id = ?", whereArgs: [entity.id]);
}
//返回插入數(shù)據(jù)的值 行數(shù)
return await db.rawInsert('''insert into $table(
$id,
$name,
$price
) values (?,?,?)''', [
entity.id,
entity.name,
entity.price,
]);
}
//delete by id
Future<void> delete(int deleteId) async {
Database db = await getDataBase();
//檢查此主鍵是否已有數(shù)據(jù)
if (queryByKey(deleteId) == null) {
return;
}
return db.delete(table, where: "$id = ?", whereArgs: [deleteId]);
}
}
使用:
Future<List<ProductEntity>> getAllProduct() async {
ProductDbProvider productDbProvider=ProductDbProvider();
List<ProductEntity> data = List();
//開始查詢
List<Map<String, dynamic>> queryResult = await productDbProvider.queryAll();
//將數(shù)據(jù)轉(zhuǎn)化為實(shí)體類
for (Map<String, dynamic> map in queryResult) {
//這里直接使用fromJson即可
data.add(ProductEntity().fromJson(map));
}
return data;
}
2.3 文件操作(系統(tǒng)有區(qū)別)
Android和iOS的應(yīng)用存儲(chǔ)目錄不同,PathProvider 插件提供了一種兼容的方式來訪問設(shè)備文件系統(tǒng)上的常用位置。該類當(dāng)前支持訪問2個(gè)文件系統(tǒng)位置:
臨時(shí)目錄:可以使用 getTemporaryDirectory() 來獲取臨時(shí)目錄; 系統(tǒng)可隨時(shí)清除的臨時(shí)目錄(緩存)。在iOS上,這對(duì)應(yīng)于NSTemporaryDirectory() 返回的值。在Android上,這是getCacheDir()返回的值。
內(nèi)部存儲(chǔ)目錄:可以使用getApplicationDocumentsDirectory()來獲取應(yīng)用程序的文檔目錄,該目錄用于存儲(chǔ)只有自己可以訪問的文件。只有當(dāng)應(yīng)用程序被卸載時(shí),系統(tǒng)才會(huì)清除該目錄。在iOS上,這對(duì)應(yīng)于NSDocumentDirectory。在Android上,這是AppData目錄。
外部存儲(chǔ)目錄:可以使用getExternalStorageDirectory()來獲取外部存儲(chǔ)目錄,如SD卡;由于iOS不支持外部目錄,所以在iOS下調(diào)用該方法會(huì)拋出UnsupportedError異常,而在Android下結(jié)果是android SDK中g(shù)etExternalStorageDirectory的返回值。
以下例子,在文件counter.txt中保存一個(gè)按鈕的點(diǎn)擊次數(shù),下次進(jìn)入界面時(shí)讀取
// _getLocalFile函數(shù),獲取本地文件目錄
Future<File> _getLoaclFile() async{
//獲取應(yīng)用目錄// 獲取本地文檔目錄
String dir=(await getApplicationDocumentsDirectory()).path;
return new File('$dir/counter.txt');
}
Future<int> _readCounter() async{
try{
/*
* 獲取本地文件目錄
* await等待操作完成
*/
File file =await _getLoaclFile();
//讀取點(diǎn)擊次數(shù)(以字符串)
// 使用給定的編碼將整個(gè)文件內(nèi)容讀取為字符串
String contents=await file.readAsString();
return int.parse(contents);//返回文件中的點(diǎn)擊數(shù)
} on FileSystemException{
// 發(fā)生異常時(shí)返回默認(rèn)值
return 0;
}
}
// _incrementCounter函數(shù),點(diǎn)擊增加按鈕時(shí)的回調(diào)
Future<Null> _incrementCounter() async{
setState(() {
_counter++;
});
//將點(diǎn)擊次數(shù)以字符串類型寫到文件中
await (await _getLoaclFile()).writeAsString('$_counter');
}

四 待解決...
- Flutter 項(xiàng)目不同編譯環(huán)境的搭建 dev/qa/release
- Flutter 在IOS上運(yùn)行(打包和發(fā)布)?
- 可以預(yù)見的會(huì)出現(xiàn)系統(tǒng)差異問題