HackWeek Flutter 开发实用总结

in FEcoding with 0 comment

上上周结束了Hack Week项目,拖了很久都没有做总结....总算趁这个周末在家躺尸,把在4天极限coding中学到的Flutter开发做个小小的总结。

一. 首先是好用的库推荐:

Flutter库:color_thief_flutter

Future getColorFromUrl(String url, [int quality]) async
getColorFromUrl(
    "xxxxx"// Image Url
).then((color) {
    setState(() {
      mainColor = color;
    });
});

ScreenUtil适配

flutter库: flutter_ScreenUtil

非常方便的一套屏幕适配方案!只要在初始化并设置适配尺寸及字体大小是否根据系统的“字体大小”辅助选项来进行缩放,就可以只传入设计稿的px值,直接进行尺寸的适配了,注意需要在MaterialApp的home中的页面设置,才可以保证在每次使用前都是指好了适配尺寸。

二 布局

下面是使用频率可能比较高的复杂滑动布局和键盘遮挡问题。

嵌套同方向滑动+嵌套不同方向+吸顶效果

请输入图片描述

1 实现难点:使用customScroller的时候,用SliverAppBar的话适用于背景图头部,使用SliverPersistentHeader封装的话会出现无法控制顶部在向上滑动时首先被收起,向下滑动显示第一行列表后顶部拉出显示,而是会出现嵌套的纵向方向上的滑动冲突,必须在顶部滑动才会收起顶部,在内容区滑动是日记列表的滑动

请输入图片描述

2 解决方案:根据滑动事件是谁发起的、CustomScrollView与ListView的状态、滑动的方向、滑动的距离、滑动的速度等进行协调它们怎么响应。

参考实现:Flutter 实现类似美团外卖店铺页面滑动效果

简单来说,我们需要修改 ScrollerPosition, ScrollerController。修改ScrollerPosition是为了把手指滑动距离或手指离开屏幕前滑动速度传递给协调器协调处理。修改ScrollerController是为了保证滑动控制器在创建ScrollerPosition创建的是我们修改过后的ScrollerPosition

Coding 如下:

// 协调器:协调主页面和子部件的滑动数据,实现用户手机离开页面时的监听函数
// 控制器:继承自ScrollController,为滚动小部件创建一个控制器
// 滚动位置信息:把手指滑动距离或手指离开屏幕前滑动速度传递给协调器协调处理
// 主页面结构:
_calenderCoordinator = CalenderScrollCoordinator();
Listener(
    onPointerUp: _calenderCoordinator.onPointerUp,
    child: CustomScrollView(
      controller: _pageScrollController,
      physics: ClampingScrollPhysics(),
      slivers: <Widget>[
        // 需要SliverAppBar才可以收起下面的SliverPersistentHeader
        SliverAppBar(
            pinned: true,
            backgroundColor: Color(0xFFF7F7F7),
            bottom: PreferredSize(
              preferredSize: Size.fromHeight(-25),
              child: SizedBox(
                height: 0,
              ),
            ),
            flexibleSpace: // 叠加的背景,
         ),
      SliverPersistentHeader(
        pinned: false,
        floating: true,
        delegate: _SliverAppBarDelegate(
          maxHeight: 100,
          minHeight: 0,
          child: _Userinfo(viewModel),//用户信息
        )),
      SliverPersistentHeader(
          pinned: true,
          floating: false,
          delegate: _SliverAppBarDelegate(
              maxHeight: _tabBarHeight,
              minHeight: _tabBarHeight,
              child: Column(
                children: [
                  Row(
                    children: [
                    // 2020
                    ],
                  ),
                  Container(
                    padding: EdgeInsets.symmetric(
                        horizontal: viewModel.marginLeft / 5),
                    child: TabBar(
                        labelColor: Colors.black,
                        // 和下面的tabview使用同一个控制器
                        controller: _tabController,
                        isScrollable: true, //是否可滚动
                        indicatorColor: Colors.grey, //指示器颜色
                        indicator: CircleTabIndicator(
                            color: Color(0xFF333333),
                            radius: 4),
                        tabs: monthTabs
                            .map((item) => Container(
                                  height: fix(28),
                                  child: Text(
                                    '${item.text}月',
                                    style: TextStyle(
                                        fontSize: fix(14),
                                        color: item.tab ==
                                                _tabController
                                                    .index
                                            ? Color(0xFF333333)
                                            : Color(
                                                0xFF999999)),
                                  ),
                                ))
                            .toList()),
                  ),
                ],
              )),
        ),
        SliverFillRemaining(
            child: Container(
              padding: EdgeInsets.symmetric(
              horizontal: viewModel.marginLeft / 2),
          child: TabBarView(
              controller: _tabController,
              children: monthTabs
                  .map((item) => LogList(
                      calenderCoordinator: _calenderCoordinator,
                      logs: yearData[item.tab],
                      viewModel: viewModel,
                      key: Key("${item.tab}")))
                  .toList()),
        ))
      ]
    )
)

键盘遮挡

当弹起键盘的时候,系统会缩小Scaffold的高度并重建,导致布局异常。解决方法是增加布局变化的监听,当输入框得到聚焦时把输入框往上移。

// 关键代码
void handlePostFrame(Duration duration) {
    var pageHeight = ScreenUtil.screenHeight/ScreenUtil.pixelRatio;
    if (userFocusNode.hasFocus || pwdFocusNode.hasFocus) {
      setState(() {
        marginBottom = pageHeight * 0.3;
      });
    } else if (!userFocusNode.hasFocus && !pwdFocusNode.hasFocus) {
      setState(() {
        marginBottom = pageHeight * 0.2;
      });
    }
  }

之前做其他项目的时候也有遇到过这个问题,点此看完整代码

三. 实用工具

JSON解析

dart本身提供了json.encode和json.decode来进行dart和json的转换,但是由于json.decode的转换是dynamic的,这也意味着需要等到运行时才知道数据的类型,使用这种方式会失去dart作为一门静态语言的特性,因此像很多其他语言一样,dart同样也有json model化,把json转换为对应的dart model,这样,调用代码现在可以具有类型安全、自动补全字段以及编译时异常。
解析JSON需要在model类中提供fromJson方法,使用字典数据为对象初始化赋值.

factory SongList.fromJson(Map<String, dynamic> json) =>
      _$SongListFromJson(json);
SongList _$SongListFromJson(Map<String, dynamic> json) {
  return SongList(
    (json['songlist'] as List)
        ?.map((e) =>
            e == null ? null : SongInfo.fromJson(e as Map<String, dynamic>))
        ?.toList(),
  );
}
SongInfo _$SongInfoFromJson(Map<String, dynamic> json) {
  return SongInfo(
    json['songMid'] as String,
    json['songName'] as String,
    json['actorName'] as String,
    json['lyrics'] as String,
    json['mediaMid'] as String,
    json['albumMid'] as String,
    json['url'] as String,
    json['lyricsArr'] as List<dynamic>,
  );
}

在比赛结束后才发现有个插件可以直接把json数据转化为dart model类,真的是懒人神器了,首先安装下依赖
再pubspec.yaml添加:

dependencies:
    json_annotation: (latest version)
dev_dependencies:
    build_runner: (latest version)
    json_serializable: (latest version)

再安装下插件Json to Dart Model
复制或者选中json,按command+shift+p选择convert json命令,就会生成对应的类了,超级方便!生成的代码如下所示:

请输入图片描述

Storage

使用mmkv封装成全局的单例,可以用来存储简单的键值对,MMKV是自动初始化的。相当于web开发中的LocalStorage

airing使用单例模式封装的代码如下:

import 'dart:async';
import 'dart:convert';

import 'package:mmkv_flutter/mmkv_flutter.dart';
//import './logger.dart';

/// 通用操作接口
abstract class StorageOperator {
  Future<void> set(String key, dynamic value);

  Future<String> get(String key);

  Future<void> setBool(String key, bool value);

  Future<bool> getBool(String key);

  Future<void> setString(String key, String value);

  Future<String> getString(String key);

  Future<void> setInt(String key, int value);

  Future<int> getInt(String key);

  Future<void> setDouble(String key, double value);

  Future<double> getDouble(String key);

  Future<void> setLong(String key, int value);

  Future<int> getLong(String key);

  Future<void> clear();
}

class Storage implements StorageOperator {
  static final Storage _instance = Storage._internal();

  Storage._internal();

  static Storage get instance => _instance;

  static MmkvFlutter _mmkv;

  Future<void> _setupMmkvIfNeeded() async {
    if (_mmkv != null) {
      return;
    }
    _mmkv = await MmkvFlutter.getInstance();
  }

  Future<MmkvFlutter> get mmkv async {
    if (_mmkv != null) return _mmkv;

    _mmkv = await MmkvFlutter.getInstance();

    return _mmkv;
  }

  Future<void> set(String key, dynamic value) async {
    await _setupMmkvIfNeeded();

    if (value is String) {
      await _mmkv.setString(key, value);
    } else {
      try {
        await _mmkv.setString(key, json.encode(value));
      } catch (e) {}
    }
  }

  Future<String> get(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getString(key);
  }

  Future<void> setBool(String key, bool value) async {
    await _setupMmkvIfNeeded();

    await _mmkv.setBool(key, value);
  }

  getBool(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getBool(key);
  }

  Future<void> setString(String key, String value) async {
    await _setupMmkvIfNeeded();

    await _mmkv.setString(key, value);
  }

  getString(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getString(key);
  }

  Future<void> setDouble(String key, double value) async {
    await _setupMmkvIfNeeded();

    await _mmkv.setDouble(key, value);
  }

  getDouble(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getDouble(key);
  }


  Future<void> setInt(String key, int value) async {
    await _setupMmkvIfNeeded();

    await _mmkv.setInt(key, value);
  }

  getInt(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getInt(key);
  }

  Future<void> setLong(String key, int value) async {
    await _setupMmkvIfNeeded();

    await _mmkv.setLong(key, value);
  }

  getLong(String key) async {
    await _setupMmkvIfNeeded();

    return await _mmkv.getLong(key);
  }

  Future<void> clear() async {
    await _setupMmkvIfNeeded();

    await _mmkv.clear();
  }
}

封装后使用如下:
引用封装后的文件,要使用异步函数

Storage.instance.set("key", value);
Storage.instance.get("key").then((res){} )

ok, 大概是这些内容啦!

Responses