玖叶教程网

前端编程开发入门

真香!面对Flutter,我终于迈出了第一步

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

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

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言