14

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzIxNzU1Nzk3OQ%3D%3D&%3Bmid=2247489542&%3Bidx=1&%3Bsn=426ce0b2746303439c1475638cfb5af4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

code小生 一个专注大前端领域的技术平台 公众号回复 Android 加入安卓技术群

作者:星星y

链接:https://www.jianshu.com/p/e6d66f77733b

声明:本文已获 星星y 授权发表,转发等请联系原作者授权

哎,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图片资源所需了。

RVzE3my.jpg!web image.png

这里再建一个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(() {});
  }

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

相关阅读

1 重磅! Flutter视图局部更新

2 使用Flutter一年后,这是我得到的经验

3 Flutter 与原生交互总结

4 Flutter 应用性能优化最佳实践

5 Flutter 体验记

B3Q7faa.jpg!web

如果你想要跟大家分享你的文章

欢迎投稿


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK