Flutter知識(shí)點(diǎn)整理

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è)目錄:


image.png

打開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

image.png

這里可以看到,在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: () {},
          ),
        ],
      ),
    );
  }
}
image.png

在數(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');
  }
image.png

四 待解決...

  1. Flutter 項(xiàng)目不同編譯環(huán)境的搭建 dev/qa/release
  2. Flutter 在IOS上運(yùn)行(打包和發(fā)布)?
  3. 可以預(yù)見的會(huì)出現(xiàn)系統(tǒng)差異問題
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容