用Flutter给Readhub写一个App

前言

学习Flutter也有一段时间了,这个项目是当时学习过程作的一个练手项目(2019年11月10日)边作边学大概用了半个月的时间完成,至于为啥到如今才写这篇文章。这是一个伤心💔的故事,还不是由于穷买不起Mac😂,没有作iOS的适配检测。感受没有作过iOS适配检测的Flutter项目是不完整的,故这个项目一直就拖在那里了。时隔8个月终于能够作iOS适配检测了(固然仍是不是本身的Mac,公司给配的,为公司点赞👍),故将这个项目的历程作一个阶段性的总结。android

项目简介

名称:Freadhub :即Flutter版本的readhub。readhub官网git

logo:字母F(Flutter)+字母R(readhub) 结合蓝色背景-灵(chao)感(xi)来自阿里巴巴Flutter开源项目FlutterGo的logogithub

Freadhub

slogan:Freadhub-作轻便的聚合资讯web

slogan

iOS: 没有😼(什么没有iOS下载地址,那你检测个什么iOS适配😂)浏览器

Android下载地址:蒲公英下载-安装密码1缓存

蒲公英下载-安装密码1

主要功能

Freadhub 主要囊括如下功能: 热门话题科技动态开发者区块链四大模块 相关聚合资讯快捷查看 方便快捷的浅色/深色模式切换 丰富的彩虹颜色主题/每日主题切换 长按社会化分享预览图效果模式 方便快捷的意见反馈入口bash

浅色主题 深色主题
资讯详情 更多操做
选择主题 社交分享

主要功能实现

该项目主要使用dio库进行网络请求,provider进行状态管理;比较精巧及轻便自己功能比较少但实用。这里挑选几个主要功能实现来简要概述下。网络

七色彩虹主题切换及深色模式-Android虚拟导航栏颜色

深色模式:Flutter提供了方便的浅色/深色主题自动切换相关入口:MaterialApp下的themedarkTheme入口用于设置ThemeData用于控制几乎全部的Widget颜色及主题;Flutter会根据系统(iOS 13及以上版本、Android 10及以上版本原生支持深色模式)当前主题设置自动调用对应theme或darkTheme设置的ThemeData属性进行Widget主题切换操做。该部分可参考 Flutter适配深色模式(DarkMode)app

主动切换颜色主题及深色主题:Freadhub提供了自动切换深色/浅色主题(若是系统设置了深色模式优先)及 七色彩虹系颜色主题设置(icon及部分文字颜色及tab下划线)可选择固定的颜色也可选择天天顺序变化。主要流程则是建立一个ThemeViewModel类继承ChangeNotifier(以便在选择Color值后调用notifyListeners进行widget刷新)->选择对应的主题颜色值 ->调用ThemeViewModelnotifyListeners方法通知widget刷新async

一、入口widget设置MaterialApp下的themedarkTheme对应的ThemeData来自ThemeViewModel方法返回。主要代码以下:

@override
  Widget build(BuildContext context) {
    return BasisProviderWidget2<ThemeViewModel, LocaleViewModel>(
      model1: ThemeViewModel(),
      model2: LocaleViewModel(),
      builder: (context, theme, locale, child) => MaterialApp(
        ///全局主题配置
        theme: theme.themeData(),
        ///全局配置深色主题
        darkTheme: theme.themeData(platformDarkMode: true),
        ///国际化语言
        locale: locale.locale,
        localizationsDelegates: [
          S.delegate,
          ///下拉刷新库国际化配置
          RefreshLocalizations.delegate,
          ///不配置该项会在EditField点击弹出复制粘贴工具时抛异常 The getter 'cutButtonLabel' was called on null.
          GlobalCupertinoLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate
        ],
        supportedLocales: S.delegate.supportedLocales,
        ///启动页显示slogan
        home: SplashPage(),
      ),
    );
复制代码

二、ThemeViewModel获取ThemeData方法主要代码

///根据主题 明暗 和 颜色 生成对应的主题[dark]系统的Dark Mode
themeData({bool platformDarkMode: false}) {
    var isDark = platformDarkMode || _userDarkMode;
    var themeColor = _themeColor;
    _accentColor = isDark ? themeColor[600] : _themeColor;
    Brightness brightness = isDark ? Brightness.dark : Brightness.light;
    var themeData = ThemeData(
      ///主题浅色或深色
      brightness: brightness,
      primaryColorBrightness: brightness,
      accentColorBrightness: brightness,
      primarySwatch: themeColor,
      ///强调色
      accentColor: accentColor,
      primaryColor: accentColor,
    );
    themeData = themeData.copyWith(
        ///appBar主题
      appBarTheme: themeData.appBarTheme.copyWith(
        ///根据主题设置Appbar样式背景
        color: isDark ? colorBlackTheme : Colors.white,
        ///去掉海拔高度
        elevation: 0,
        ///文本样式
        textTheme: TextTheme(
          ///title Text样式
          subtitle1: TextStyle(
            color: isDark ? Colors.white : accentColor,
            fontSize:17,
            fontWeight: FontWeight.w500,
            ///字体
            fontFamily: fontValueList[_fontIndex],
          ),
          ///action Text样式
          bodyText2: TextStyle(
            color: isDark ? Colors.white : accentColor,
            fontSize:13,
            fontWeight: FontWeight.w500,
            ///字体
            fontFamily: fontValueList[_fontIndex],
          ),
        ),
        ///icon样式
        iconTheme: IconThemeData(
          color: isDark ? Colors.white : accentColor,
        ),
      ),
      ///全局icon
      iconTheme: themeData.iconTheme.copyWith(
        color: accentColor,
      ),
      ///长按提示文本样式
      tooltipTheme: themeData.tooltipTheme.copyWith(
          textStyle: TextStyle(
              fontSize: 13,
              color:
                  (darkMode ? Colors.black : Colors.white).withOpacity(0.9))),
      ///TabBar样式设置
      tabBarTheme: themeData.tabBarTheme.copyWith(
        ///标签内边距
        labelPadding: EdgeInsets.symmetric(horizontal: 8),
        ///选中label样式
        labelStyle: TextStyle(
          fontWeight: FontWeight.w600,
          fontSize: 14,
        ),
        ///未选中label样式
        unselectedLabelStyle: TextStyle(
          fontWeight: FontWeight.normal,
          fontSize: 13,
        ),
      ),
      ///floatingActionButton样式
      floatingActionButtonTheme: themeData.floatingActionButtonTheme.copyWith(
        ///背景色
        backgroundColor: themeAccentColor,
        ///水波纹颜色
        splashColor: themeColor.withAlpha(50),
      ),
    );
    setSystemBarTheme();
    return themeData;
  }
复制代码

三、选择Color主题,通知主widget更新

/// 切换指定色彩;没有传[brightness]就不改变brightness,color同理
  void switchTheme(
      {bool userDarkMode, int themeIndex, MaterialColor color}) async {
    if (themeIndex != null && themeIndex != _themeIndex) {
      SpUtil.putInt(SP_KEY_THEME_COLOR_INDEX, themeIndex);
    }
    _userDarkMode = userDarkMode ?? _userDarkMode;
    _themeIndex = themeIndex ?? _themeIndex;
    _themeColor = color ?? getThemeColor();
    ///存入缓存
    SpUtil.putBool(SP_KEY_THEME_DARK_MODE, _userDarkMode);
    notifyListeners();
  }
复制代码

四、Android 虚拟导航栏颜色控制(就是Android模拟器常常出现的那个底部黑色导航栏)

虚拟导航栏

比较遗憾的是:

一、目前市面上绝大多数的应用都是没有作虚拟导航栏适配的(可能跟目前市面上的手机大多都是全面屏没人关心这个吧)

二、Flutter ThemeData里没有专门设置虚拟导航栏颜色的属性,讲道理ThemeData里面的 brightness属性理应同时控制状态栏文字颜色及icon颜色以及底部的虚拟导航栏背景及icon颜色的。可是Flutter官方不讲道理

好在Flutter提供了其它方式来更改系统栏的颜色,那就是 SystemChrome.setSystemUIOverlayStyle 该方法接收一个 SystemUiOverlayStyle样式用于控制系统UI(即状态栏及导航栏)其中systemNavigationBarColor用于控制导航栏背景色 systemNavigationBarIconBrightness用于控制导航栏icon深/浅色。在调用切换主题时同时调用 SystemChrome.setSystemUIOverlayStyle便可

///设置系统Bar主题
  static Future setSystemBarTheme() async {
    bool statusEnable =
        Platform.isAndroid ? await PlatformUtil.isStatusColorChange() : true;
    bool navigationEnable = Platform.isAndroid
        ? await PlatformUtil.isNavigationColorChange()
        : true;
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      ///状态栏背景色
      statusBarColor: darkMode || statusEnable ? Colors.transparent : null,
      ///状态栏icon 亮度(浅色/深色)
      statusBarIconBrightness: darkMode ? Brightness.light : Brightness.dark,
      ///导航栏颜色
      systemNavigationBarColor: darkMode
          ? colorBlackTheme
          : navigationEnable ? Colors.transparent : null,
      ///导航栏icon(浅色/深色)
      systemNavigationBarIconBrightness:
          darkMode ? Brightness.light : Brightness.dark,
    ));
  }
复制代码

导航栏颜色

是否是顿时感受好很多啊(好开森😄)

可是若是咱们从系统切换深色主题会怎样呢?

深色模式异常

尼玛咋深色模式下导航栏仍是浅色的,好突兀。 这个就是前面说到的由于Flutter ThemeData里没有专门设置虚拟导航栏颜色的属性,因此当系统切换深色模式导航栏没有任何变化 这咋整?

这里笔者尝试了很多,如先后台监听-这个只适合去设置页里面控制再返回,像这种从通知栏操做的App未进行先后台切换,不能作相应的操做。最终找到当系统主题切换会回调 StatefulWidgetdidUpdateWidget方法,那不是能够在里面作相应操做?说试就试

@override
  void didUpdateWidget(HomePage oldWidget) {
    super.didUpdateWidget(oldWidget);
    LogUtil.e('home_page_didUpdateWidget');
    ///更新UI--在深色暗色模式切换时候也会触发因ThemeData无NavigationBar相关主题配置故采用该方法迂回处理
    if (_lastSetSystemUiAt == null ||
        DateTime.now().difference(_lastSetSystemUiAt) >
            Duration(milliseconds: 1000)) {
      ///两次点击间隔超过阈值则从新计时
      _lastSetSystemUiAt = DateTime.now();
      LogUtil.e('设置系统栏颜色');
      ThemeViewModel.setSystemBarTheme();
    }
  }
复制代码

深色模式正常

哈哈,终于正常了!这里笔者采用了一个迂回笨的方式,若是有更好的方式请不吝赐教,感谢🙏

字体大小控制

Freadhub还提供了资讯列表标题及简介字体大小控制的功能,主要经过 Text widget的textScaleFactor属性控制,全局设置articleTextScaleFactor值修改后再刷新Widget便可

字体大小控制

长按分享资讯功能

该功能涉及 截屏访问存储权限保存图片分享图片等四个主体功能

一、截屏

Flutter提供专门用于截屏的widget RepaintBoundary只需将须要截屏的widget用RepaintBoundary包裹而后GlobalKey能够拿到RenderRepaintBoundary的引用并将其转化成图片数据Uint8List便可进行后续的保存图片及分享功能,须要注意的是将RenderRepaintBoundary转化成Image资源时需设置好pixelRatio通常获取设备像素比否则最终图片显示可能不清楚或者太大。这块网上例子不少。

RenderRepaintBoundary boundary = globalKey.currentContext.findRenderObject();
    ///弹框宽度与屏幕宽度比值避免截图出来比预览更大
    ///分辨率经过获取设备的devicePixelRatio以达到清晰度良好
    var image = await boundary.toImage(pixelRatio: (MediaQuery.of(context).devicePixelRatio));
    ///转二进制
    ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
    ///图片数据
    pngBytes = byteData.buffer.asUint8List();
复制代码

二、访问存储权限

Freadhub使用的是三方库permission_handler来完成Android端Permission.storage、iOS端Permission.photos权限申请,这个库作得比较不错,若是拒绝了还提供了跳转设置权限页方法。

/// 申请权限
  static Future<bool> checkPermission(Permission permission) async {
    PermissionStatus status = await permission.status;
    if (status != PermissionStatus.granted) {
      status = await permission.request();
      return status == PermissionStatus.granted;
    } else {
      return true;
    }
  }

  ///文件读写android storage iOS photos
  static Future<bool> checkStoragePermission() async {
    return checkPermission(
        Platform.isAndroid ? Permission.storage : Permission.photos);
  }
复制代码

Android端的还好,毕竟作Android出身的,iOS的适配仍是出了个小插曲(其实也怪本身的英语太烂没注意😂而本身想固然的以为了)。这里说一下避坑 permission-handler github官网 iOS的一、2步能够忽略(官网的一、2步意思是不须要的权限可将其移除;当时作的时候没有细看就自觉得的以为是须要啥权限就添加进去将注释去掉,多是惯性思惟😼),直接在Info.plist添加使用照片描述便可

<key>NSPhotoLibraryUsageDescription</key>
<string>保存图片到设备,须要调用你的照片功能</string>
复制代码

三、保存图片

Freadhub使用三方库 path_provider用于获取本地文件路径并经过File对象将步骤一获取到的图片数据Uint8List保存到对应文件夹便可; 特殊说明:去年作的版本是用的一个三方库将图片保存到手机图库而后进行分享操做,最近作iOS适配检测时发现iOS保存图库没有返回File路径这样就没法进行图片File的分享。另外这种保存图库功能是调用一次则进行一次保存操做会形成同一图片屡次保存状况,形成内存的浪费。故现使用File保存经过使用同一文件名以便同一图片屡次保存的问题。 可是这形成另外一个问题:iOS应用建立的私有文件不能被其它应用使用😂;故:iOS只提供分享按钮,用户可在弹出框经过选择Save Image进行保存图库而后其它应用可以使用。

///保存图片到系统图库
    File saveFile = File(await getImagePath(imageName));
    bool exist = saveFile.existsSync() && saveFile.lengthSync() > 0;
    if (!exist) {
      if (!saveFile.existsSync()) {
        await saveFile.create();
      }
      File file = await saveFile.writeAsBytes(pngBytes);
      exist = file.existsSync();
    }
    if (exist) {
      fileImage = saveFile.absolute.path;
      saveImage(context, globalKey, imageName, share: share);
      return;
    }
复制代码

四、分享图片

Flutter官方提供的share只提供简单的文字分享,故Freadhub使用三方库flutter_share_plugin

FlutterShare.shareFileWithText(
            filePath: fileImage, textContent: S.of(context).saveImageShareTip);
复制代码
Android真机 iPad真机

Android 应用升级

Android端App放在蒲公英上的,故也作了一个蒲公英的检测应用版本升级的功能。 有新版本则跳转浏览器由用户本身选择下载安装。

Android应用更新

使用到的三方库

能作成这个Freadhub小应用离不开这些三方库,在此感谢这些三方库的做者🙏

# 国际化支持
  flutter_localizations:
    sdk: flutter
  # 状态管理State
  provider: ^3.2.0
  # 吐司toast
  oktoast: ^2.2.0
  # 设备信息
  device_info: ^0.4.2+4
  # 应用包信息
  package_info: ^0.4.1

  # WebView
  webview_flutter: ^0.3.22+1
  # 网络请求相关dio
  dio: ^3.0.9
  # 加载网络图片
  cached_network_image: ^2.2.0+1
  synchronized: ^2.1.0+1
  # 下拉刷新
  pull_to_refresh: ^1.6.0
  # 本地缓存sp
  shared_preferences: ^0.5.7+3
  # 用于作骨架屏-闪光效果
  shimmer: ^1.1.1
  # 跳转系统浏览器/打电话等
  url_launcher: ^5.4.11
  # 二维码-生成
  qr_flutter: ^3.2.0
  # 工具类
  flustars: ^0.3.2
  # 动态权限申请
  permission_handler: ^5.0.1
  # 文件路径
  path_provider: ^1.6.11
  # 分享文字及文件-注意保存文件位置
  flutter_share_plugin: ^0.1.3+3
复制代码

功能很少,用到的三方库还真很多😄

存在的坑

一、Android 相同的配置release打包处来的versionCode和debug不一致并且是直接加1000的那种(好比当前versionCode为4 release打包出来为1004)-网上没有搜到其余人有相似疑问也没有找到缘由。

二、iOS的下拉刷新功能无论模拟器仍是真机测试总会很容易出现动画及应用页面卡顿的状况

三、iOS运行真机过一下子整个Android Studio会彻底无响应须要杀进程才能够关掉

以上就是Freadhub开发过程遇到的目前还未有解决的疑惑坑点,若是有朋友知道但愿解下小弟的疑惑,感谢🙏

工具推荐

作这个Freadhub过程仍是用了俩不错的小工具,在这里给你们推荐下。

一、Image Asset Icon Resizer Lite 一款方便的logo、启动页生成工具提供一种尺寸的图片可快速生成其它尺寸。支持Android 、iOS等

Image Asset Icon Resizer Lite

二、GIPHY CAPTURE 一款很好用的gif录制软件,文章使用到的gif均是经过该软件录制而成

GIPHY CAPTURE

关于我

掘金: AriesHoo

简书: AriesHoo

GitHub: AriesHoo

Email: AriesHoo@126.com

相关文章
相关标签/搜索