首頁>技術>

前言

哎,Flutter真香啊

早在一年前想學習下flutter,但當時對於它佈局中地獄式的巢狀有點望而生畏,心想為什麼巢狀這麼複雜,就沒有xml佈局方式嗎,用jsx方式也行啊,為什麼要用dart而不用javascript,走開,勞資不學了。然而,隨著今年google io大會flutter新版本釋出,大勢宣揚。我又開始從頭學習flutter了:

瀏覽https://dart.dev/瀏覽https://book.flutterchina.club/本想看下視訊實戰的,後面發現效率太低(有點囉嗦),放棄了。最終還是決定通過閱讀flutter專案原始碼學習,事實上還是這種效率最高。

剛好公司有新app開發,這次決定用flutter開發了,邊開發邊學習,既完成了工作又完成了學習(ps:現在公司ios和前端也在學了)。

用完flutter的感受是,一旦接受這種巢狀佈局後,發現佈局也沒那麼難,hot reload牛皮,async真好用,dart語言真方便,嗯,香啊。

第三方庫dio: 網路sqflite: 資料庫pull_to_refresh: 下拉重新整理,上拉載入json_serializable: json序列化,自動生成model工廠方法shared_preferences: 本地儲存fluttertoast: 吐司訊息圖片資源

為適配各個解析度的圖片資源,通常需要1,2,3倍的圖。在flutter專案根目錄下建立assets/images目錄,在pubspec.yaml檔案中加入圖片配置

flutter: # ... assets: - assets/images/

然後通過sketch切出1/2/3倍圖片,這裡可通過編輯預設,在詞首加入2.0x/和3.0x/,這樣匯出的格式便符合flutter圖片資源所需了。

這裡再建一個image_helper.dart的工具類,用於產生Image

class ImageHelper { static String png(String name) { return "assets/images/$name.png"; } static Widget icon(String name, {double width, double height, BoxFit boxFit}) { return Image.asset( png(name), width: width, height: height, fit: boxFit, ); }}主介面Tab導航

在app主介面,tab底部導航是最常用的。通常基於Scaffold的bottomNavigationBar配和PageView使用。通過PageController控制PageView介面切換,同時使用BottomNavigationBar的currentIndex控制tab選中狀態。為了能使監聽返回鍵,使用WillPopScope實現點兩次返回鍵退出app。

List pages = <Widget>[HomePage(), MinePage()];class _TabNavigatorState extends State<TabNavigator> { DateTime _lastPressed; int _tabIndex = 0; var _controller = PageController(initialPage: 0); BottomNavigationBarItem buildTab( String name, String normalIcon, String selectedIcon) { return BottomNavigationBarItem( icon: ImageHelper.icon(normalIcon, width: 20), activeIcon: ImageHelper.icon(selectedIcon, width: 20), title: Text(name)); } @override Widget build(BuildContext context) { return Scaffold( bottomNavigationBar: BottomNavigationBar( currentIndex: _tabIndex, backgroundColor: Colors.white, onTap: (index) { setState(() { _controller.jumpToPage(index); _tabIndex = index; }); }, selectedItemColor: Color(0xff333333), unselectedItemColor: Color(0xff999999), selectedFontSize: 11, unselectedFontSize: 11, type: BottomNavigationBarType.fixed, items: [ buildTab("Home", "ic_home", "ic_home_s"), buildTab("Mine", "ic_mine", "ic_mine_s") ]), body: WillPopScope( child: PageView.builder( itemBuilder: (ctx, index) => pages[index], controller: _controller, physics: NeverScrollableScrollPhysics(),//禁止PageView左右滑動 ), onWillPop: () async { if (_lastPressed == null || DateTime.now().difference(_lastPressed) > Duration(seconds: 1)) { _lastPressed = DateTime.now(); Fluttertoast.showToast(msg: "Press again to exit"); return false; } else { return true; } }), ); }}網路層封裝

網路框架使用的是dio,不管是哪種平臺,網路請求最終要轉成實體model用於ui展示。這裡先將dio做一個封裝,便於使用。

通用攔截器

網路請求中通常需要新增自定義攔截器來預處理網路請求,往往需要將登入資訊(如user_id等)放在公共引數中,例如:

import 'package:dio/dio.dart';import 'dart:async';import 'package:shared_preferences/shared_preferences.dart';class CommonInterceptor extends Interceptor { @override Future onRequest(RequestOptions options) async { options.queryParameters = options.queryParameters ?? {}; options.queryParameters["app_id"] = "1001"; var pref = await SharedPreferences.getInstance(); options.queryParameters["user_id"] = pref.get(Constants.keyLoginUserId); options.queryParameters["device_id"] = pref.get(Constants.keyDeviceId); return super.onRequest(options); }}Dio封裝

然後使用dio封裝get和post請求,預處理響應response的code。假設我們的響應格式是這樣的:

{ code:0, msg:"獲取資料成功", result:[] //或者{}}import 'package:dio/dio.dart';import 'common_interceptor.dart';/* * 網路管理 */class HttpManager { static HttpManager _instance; static HttpManager getInstance() { if (_instance == null) { _instance = HttpManager(); } return _instance; } Dio dio = Dio(); HttpManager() { dio.options.baseUrl = "https://api.xxx.com/"; dio.options.connectTimeout = 10000; dio.options.receiveTimeout = 5000; dio.interceptors.add(CommonInterceptor()); dio.interceptors.add(LogInterceptor(responseBody: true)); } static Future<Map<String, dynamic>> get(String path, Map<String, dynamic> map) async { var response = await getInstance().dio.get(path, queryParameters: map); return processResponse(response); } /* 表單形式 */ static Future<Map<String, dynamic>> post(String path, Map<String, dynamic> map) async { var response = await getInstance().dio.post(path, data: map, options: Options( contentType: "application/x-www-form-urlencoded", headers: {"Content-Type": "application/x-www-form-urlencoded"})); return processResponse(response); } static Future<Map<String, dynamic>> processResponse(Response response) async { if (response.statusCode == 200) { var data = response.data; int code = data["code"]; String msg = data["msg"]; if (code == 0) {//請求響應成功 return data; } throw Exception(msg); } throw Exception("server error"); }}map轉model

使用dio可以將最終的請求響應response轉成Map<String, dynamic>物件,我們還需要將map轉成相應的model。假如我們有一個獲取文章列表的介面響應如下:

{ code:0, msg:"獲取資料成功", result:[ { article_id:1, article_title:"標題", article_link:"https://xxx.xxx" } ]}

就需要一個Article的model。由於Flutter下是禁用反射的,我們只能手動初始化每個成員變數。不過我們可以通過json_serializable將手動初始化的工作交給它。首先在pubspec.yaml引入它:

dependencies: json_annotation: ^2.0.0dev_dependencies: json_serializable: ^2.0.0

我們建立一個article.dart的model類:

import 'package:json_annotation/json_annotation.dart';part 'article.g.dart';//FieldRename.snake 表示json欄位下劃線分割型別如:article_id@JsonSerializable(fieldRename: FieldRename.snake, checked: true)class Article { final int articleId; final String articleTitle; final String articleLikn;}

注意這裡引用到了一個article.g.dart沒有產生的檔案,我們通過pub run build_runner build命令就會生成這個檔案

// GENERATED CODE - DO NOT MODIFY BY HANDpart of 'article.dart';// **************************************************************************// JsonSerializableGenerator// **************************************************************************Article _$ArticleFromJson(Map<String, dynamic> json) { return $checkedNew('Article', json, () { final val = Article(); $checkedConvert(json, 'article_id', (v) => val.articleId = v as int); $checkedConvert( json, 'article_title', (v) => val.articleTitle = v as String); $checkedConvert(json, 'article_link', (v) => val.articleLink = v as String); return val; }, fieldKeyMap: const { 'articleId': 'article_id', 'articleTitle': 'article_title', 'articleLink': 'article_link' });}Map<String, dynamic> _$ArticleToJson(Article instance) => <String, dynamic>{ 'article_id': instance.articleId, 'article_title': instance.articleTitle, 'article_link': instance.articleLink };

然後在article.dart裡添加工廠方法

class Article{ ... factory Article.fromJson(Map<String, dynamic> json) => _$ArticleFromJson(json);}具體請求封裝

建立好model類後,就可以建一個具體的api請求類ApiRepository,通過async庫,可以將網路請求最終封裝成一個Future物件,實際呼叫時,我們可以將非同步回撥形式的請求轉成同步的形式,這有點和kotlin的協程類似:

import 'dart:async';import '../http/http_manager.dart';import '../model/article.dart';class ApiRepository { static Future<List<Article>> articleList() async { var data = await HttpManager.get("articleList", {"page": 1}); return data["result"].map((Map<String, dynamic> json) { return Article.fromJson(json); }); }}實際呼叫

封裝好網路請求後,就可以在具體的元件中使用了。假設有一個_ArticlePageState:

import 'package:flutter/material.dart';import '../model/article.dart';import '../repository/api_repository.dart';class ArticlePage extends StatefulWidget { @override State<StatefulWidget> createState() { return _ArticlePageState(); }}class _ArticlePageState extends State<ArticlePage> { List<Article> _list = []; @override void initState() { super.initState(); _loadData(); } void _loadData() async {//如果需要展示進度條,就必須try/catch捕獲請求異常。 showLoading(); try { var list = await ApiRepository.articleList(); setState(() { _list = list; }); } catch (e) {} hideLoading(); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: ListView.builder( itemCount: _list.length, itemBuilder: (ctx, index) { return Text(_list[index].articleTitle); })), ); }}資料庫

資料庫操作通過sqflite,簡單封裝處理事例了文章Article的插入操作。

import 'package:sqflite/sqflite.dart';import 'package:path/path.dart';import 'dart:async';import '../model/article.dart';class DBManager { static const int _VSERION = 1; static const String _DB_NAME = "database.db"; static Database _db; static const String TABLE_NAME = "t_article"; static const String createTableSql = ''' create table $TABLE_NAME( article_id int, article_title text, article_link text, user_id int, primary key(article_id,user_id) ); '''; static init() async { String dbPath = await getDatabasesPath(); String path = join(dbPath, _DB_NAME); _db = await openDatabase(path, version: _VSERION, onCreate: _onCreate); } static _onCreate(Database db, int newVersion) async { await db.execute(createTableSql); } static Future<int> insertArticle(Article item, int userId) async { var map = item.toMap(); map["user_id"] = userId; return _db.insert("$TABLE_NAME", map); }}Android層相容通訊處理

為了相容底層,需要通過MethodChannel進行Flutter和Native(Android/iOS)通訊

flutter呼叫Android層方法

這裡舉例flutter端開啟系統相簿意圖,並取得最終的相簿路徑回撥給flutter端。我們在Android中的MainActivity中onCreate方法處理通訊邏輯

eventChannel = MethodChannel(flutterView, "event") eventChannel?.setMethodCallHandler { methodCall, result -> when (methodCall.method) {\\ "openPicture" -> PictureUtil.openPicture(this) { result.success(it) } } }

因為是通過result.success將結果回撥給Flutter端,所以封裝了開啟相簿的工具類。

object PictureUtil { fun openPicture(activity: Activity, callback: (String?) -> Unit) { val f = getFragment(activity) f.callback = callback val intentToPickPic = Intent(Intent.ACTION_PICK, null) intentToPickPic.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*") f.startActivityForResult(intentToPickPic, 200) } private fun getFragment(activity: Activity): PictureFragment { var fragment = activity.fragmentManager.findFragmentByTag("picture") if (fragment is PictureFragment) { } else { fragment = PictureFragment() activity.fragmentManager.apply { beginTransaction().add(fragment, "picture").commitAllowingStateLoss() executePendingTransactions() } } return fragment }}

然後在PictureFragment中加入callback,並且處理onActivityResult邏輯

class PictureFragment : Fragment() { var callback: ((String?) -> Unit)? = null override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 200) { if (data != null) { callback?.invoke(FileUtil.getFilePathByUri(activity, data!!.data)) } } }}

這裡FileUtil.getFilePathByUri是通過data獲取相簿路徑邏輯就不貼程式碼了,網上很多可以搜尋一下。然後在flutter端使用

void _openPicture() async { var result = await MethodChannel("event").invokeMethod("openPicture"); images.add(result as String); setState(() {}); }Android端呼叫Flutter程式碼

將剛剛MainActivity中的eventChannel宣告成類變數,就可以在其他地方使用它了。比如推送通知,如果需要呼叫Flutter端的埋點介面方法。

class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith(this) eventChannel = MethodChannel(flutterView, "event") eventChannel?.setMethodCallHandler { methodCall, result -> ... } } checkNotify(intent) initPush() } companion object { var eventChannel: MethodChannel? = null }}

在Firebase訊息通知中呼叫Flutter方法

class FirebaseMsgService : FirebaseMessagingService() { override fun onMessageReceived(msg: RemoteMessage?) { super.onMessageReceived(msg) "onMessageReceived:$msg".logE() if (msg != null){ showNotify(msg) MainActivity.eventChannel?.invokeMethod("saveEvent", 1) } }}

然後在Flutter層我們添加回調

class NativeEvent { static const platform = const MethodChannel("event"); static void init() { platform.setMethodCallHandler(platformCallHandler); } static Future<dynamic> platformCallHandler(MethodCall call) async { switch (call.method) { case "saveEvent": print("saveEvent....."); await ApiRepository.saveEventTracking(call.arguments); return ""; break; } }}

感謝大家能耐著性子看完囉裡囉嗦的文章

在這裡我也分享一份私貨,自己收錄整理的Android學習PDF+架構視訊+面試文件+原始碼筆記,還有高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習

如果你有需要的話,可以點贊+評論+轉發,關注我,然後私信我【進階】我發給你

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 大膽推測 2020年web前端發展趨勢