面對Flutter,我終於邁出了第一步


哎,Flutter真香啊

早在一年前想學習下flutter,但當時對於它布局中地獄式的嵌套有點望而生畏,心想為什么嵌套這么復雜,就沒有xml布局方式嗎,用jsx方式也行啊,為什么要用dart而不用JavaScript,走開,勞資不學了。

然而,隨着今年google io大會flutter新版本發布,大勢宣揚。我又開始從頭學習flutter了:

瀏覽 https://dart.dev/ 
瀏覽 https://book.flutterchina.club/

本想看下視頻實戰的,后面發現效率太低(有點啰嗦),放棄了。最終還是決定通過閱讀flutter項目源碼學習,事實上還是這種效率最高。

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

用完flutter的感受是,一旦接受這種嵌套布局后,發現布局也沒那么難,hot reload牛皮,async真好用,dart語言真方便,嗯,香啊。

下面就此次app開發記錄相關要點(菜鳥階段,歡迎指正)

 

第三方庫

 

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.0 dev_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 HAND part 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(() {}); }

電腦刺綉綉花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com

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; } } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM