• 面对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,
          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,  20),
            activeIcon: ImageHelper.icon(selectedIcon,  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;
        }
      }
    }
  • 相关阅读:
    使用grep搜索多个字符串
    Linux中如何启用root用户
    Docker Image 的发布和 Container 端口映射
    IIS负载均衡
    IIS负载均衡ARR前端请求到本地服务器和后端处理服务器
    IIS http重定向https,强制用户使用https访问的配置方法-iis设置
    IIS中应用Application Request Route 配置负载均衡
    IIS配置HTTPSIIS配置HTTPS
    asp.net用户登入验证
    高频交易建模
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/13825959.html
Copyright © 2020-2023  润新知