前言
哎,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開發面試專題資料,高階進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習
如果你有需要的話,可以點贊+評論+轉發,關注我,然後私信我【進階】我發給你