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

更新日期: 2019-12-27阅读: 2.2k标签: Flutter
作者:星星y,链接:https://www.jianshu.com/p/e6d66f77733b

哎,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(() {});
  }


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

链接: https://www.fly63.com/article/detial/7074

移动跨平台框架Flutter介绍和学习线路

Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。我们兼容滚动行为、排版、图标等方面的差异。

初识flutter

关注flutter已经好久,因为没有发正式版,所以一直也不想过早的躺浑水,但是最近无意中看到几篇文章,再加上美团和咸鱼等app也一直在做灰度测试,所以上周开始看了一下官方文档,地址:https://flutter.io/docs/get-started/install,然后在此做一下总结。

深入理解Flutter多线程

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。

Flutter1.5 开始,将成为全平台 UI 框架!

Flutter 1.5 的发布,同期也宣布发布 Flutter for Web 的 Preview 版本,正式开启了 Flutter 的全平台 UI 框架之路。早在年初发布的 Flutter 2019 Roadmap 中,就有提到,会在今年支持移动设备之外的平台,对 Web 的支持,算是完成了一个新的里程碑吧。

Flutter支持Web开发了!

Flutter作为一个可移植的UI框架,已经支持现代Web应用开发了!我们很开心已经发布了SDK预览版,这样你可以在Web浏览器里直接运行你的Flutter UI代码。

Flutter 混合开发 (交互通信)

Flutter 与原生之间的通信依赖灵活的消息传递方式:1,Flutter 部分通过平台通道将消息发送到其应用程序的所在的宿主环境(原生应用)。2,宿主环境通过监听平台通道,接收消息。

Flutter 局部路由实现

Flutter是借鉴React的开发思想实现的,在子组件的插槽上,React有this.props.children,Vue有<slot></slot>。当然Flutter也有类似的Widget,那就是Navigator,不过是以router的形式实现(像<router-view></router-view>)。

Flutter Kotlin 到底该如何选择?

这两个技术在当下如何选择,我之前在公众号上的回复是:如果你已经处于一个比较满意的公司,并考虑长期发展,公司并未使用这两个技术,你可以专心钻研公司当下使用的,或者未来将要使用的,这些才能助你在公司步步高升。

Flutter 与 iOS 原生 WebView 对比

本文对比的是 UIWebView、WKWebView、flutter_webview_plugin(在 iOS 中使用的是 WKWebView)的加载速度,内存使用情况。测试网页打开的速度,只需要获取 WebView 在开始加载网页和网页加载完成时的时间戳

Flutter For Web

用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!