初探Flutter:我终于迈出了第一步

发表时间: 2019-12-12 11:55

作者:星星y

前言

哎,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语言真方便,嗯,香啊。

下面就此次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封装getpost请求,预处理响应responsecode。假设我们的响应格式是这样的:

{ 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进行FlutterNative(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开发面试专题资料,高级进阶架构资料帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习

如果你有需要的话,可以点赞+评论+转发关注我,然后私信我【进阶】我发给你