用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

相關文章
相關標籤/搜索