Flutter開發中的一些Tips(三)

唯鹿發表於2019-09-09

距離flutter_deer開源快3個月了,目前為止收穫了1600+的Star,感謝大家的對此專案的認可支援。不過雖然表面看上去光鮮亮麗,但我知道還是有很多不規範不合理的用法及寫法,為了不對初學者造成誤導作用,所以這期間我幾乎每天都在完善優化它(現在應該還不錯吧)。

今天繼續分享一些在Flutter開發中需要注意的點,希望對你有所幫助。本篇的所有例子,都在我開源的flutter_deer中。希望Star、Fork支援,有問題建議可以Issue。附上鍊接:github.com/simplezhli/…

本系列前兩篇:

Flutter開發中的一些Tips

Flutter開發中的一些Tips(二)

在這裡插入圖片描述

1.多語言配置(國際化)

預設情況下,Flutter是沒有進行多語言配置。所以無論我們的手機系統環境是否是中文,一些Widget的文字都是英文顯示。比如常見的輸入框(TextField)的操作選單、日期選擇(showDatePicker)上的年月日。

在這裡插入圖片描述

既然沒有配置,那我我們新增上即可。

  1. 在 pubspec.yaml 中新增依賴:
  flutter_localizations:
    sdk: flutter
複製程式碼
  1. MaterialApp 中配置 localizationsDelegatessupportedLocales兩個屬性。
import 'package:flutter_localizations/flutter_localizations.dart';

class MyApp extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Deer',
      home: SplashPage(),
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('zh', 'CH'),
        const Locale('en', 'US')
      ]
    );
  }
}
複製程式碼

這裡我只配置了中英文兩個語言的支援。有其他語言的需要可以自行新增。

  1. 如果是iOS平臺,還需要在Info.plist檔案中做如下配置:
    在這裡插入圖片描述

其中:Localized resources can be mixed 為 YES 表示允許應用程式獲取框架庫內語言。

如果你完成了上述的配置,那麼在系統為中文環境時,就會自動將英文替換為中文。

在這裡插入圖片描述

其實翻看flutter_localizations的原始碼,你會發現它內建了多國語言的翻譯。如果你覺得文字不恰當,也可以繼承對應的Localizations去修改。

在這裡插入圖片描述
本問題詳細的程式碼見:點選檢視

2.文字字號

預設文字配置是ThemeData通過Typography(platform: platform).black方法獲取的。

在這裡插入圖片描述

比如我們常用的Text文字配置就是TextTheme中的body1Typography中按照Material的規範將語言分為三大類別

  1. 英語類字型(englishLike) 西歐、中歐、東歐和非洲大部分地區的語言通常用拉丁字母書寫。越南語是一個明顯的例外,雖然它使用了拉丁文書寫系統的本地化形式,但它的重音符號可能比西歐語言中的要高得多。希臘語和西里爾語的書寫系統與拉丁文非常相似。

  2. 高字型(tall) 語言指令碼,需要額外的行高來容納較大的象形文字,包括南亞、東南亞和中東語言,如阿拉伯語、印地語、泰語和越南語。

  3. 密集字型(dense) 需要額外的行高才能容納更大的象形文字的語言指令碼,包括中文、日語和韓文。

那麼針對這三種型別的語言,預設的文字大小也會有調整。比如下圖中englishLikedense的對比(點選檢視原始碼):

在這裡插入圖片描述
可以看到預設情況下,中文、日文、韓文會比英文類的文字大一個字號。比如常用的Text文字配置body1在切換為中文環境後,文字的預設大小變為了15.0,然而在英文環境下是14.0

這個問題也是我是在做完多語言配置後發現的一個問題,因為文字變大導致個別頁面造成了文字溢位元件。所以儘量指定文字大小以避免不必要的這類問題。

3.預先快取圖片

在Flutter中,載入本地圖片會存在一個載入過程。比如點選圖示做圖示的切換時,那麼首次會發生閃動的情況。尤其是做類似引導頁這類需求是,通過左右滑動切換圖片時會發生比較明顯的白屏一閃而過。

在這裡插入圖片描述

解決方法很簡單,就是使用 precacheImage,它將影象預存到影象快取中。如果影象稍後被ImageBoxDecationFadeInImage使用,它會被載入得更快。

precacheImage(AssetImage("assets/logo"), context);
複製程式碼

本問題詳細的程式碼見:點選檢視

4. 螢幕方向

新建的Flutter專案預設並沒有限制螢幕的橫豎屏,所以如果你的專案並沒有適配橫豎屏,需要限制某一方向。我以限制豎屏為例:

Flutter方法:


void main(){
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]).then((_){
    runApp(MyApp());
  });
}
複製程式碼

原生方法:

Android在android -> app -> src-> main -> AndroidManifest.xml中的activity標籤新增 screenOrientation屬性。

    <activity
        ...
        android:screenOrientation="portrait">
    </activity>
複製程式碼

iOS在Runner ->Info.plist中刪除UISupportedInterfaceOrientations中的 UIInterfaceOrientationLandscapeLeft與·UIInterfaceOrientationLandscapeRight。最終如下:

	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
	</array>
複製程式碼

上面的方法都是針對整個應用的。如果你希望部分頁面可以橫屏或者豎屏,只能使用Flutter的方法在對應的頁面去指定方向。

不過Flutter這個方法在iOS端有點問題,它並不能強制螢幕旋轉(也就是螢幕當前為豎屏,你指定頁面橫屏顯示,它並不會生效)。所以有這方面需求的同學可以使用flutter_orientation這個外掛。

5.拆分widget

在書寫Flutter的頁面時,難免會巢狀的層級很深或者存在許多重複使用widget。所以一般我們都會將一些widget抽離出來。抽離的方法有兩種,一種是直接抽成方法(函式)返回。一種是抽出一個自定義的widget來使用。我下面舉例說明一下:

class _TestPageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Test"),
      ),
      body: Column(
        children: <Widget>[
          const Text("Android", style: const TextStyle(fontSize: 12.0, fontStyle: FontStyle.italic, color: Colors.blue),),
          const Text("iOS", style: const TextStyle(fontSize: 12.0, fontStyle: FontStyle.italic, color: Colors.blue),),
          const Text("Flutter", style: const TextStyle(fontSize: 12.0, fontStyle: FontStyle.italic, color: Colors.blue),),
        ],
      ),
    );
  }
}
複製程式碼

上面程式碼中存在著三個樣式一致,只是文字不同的Text。我們將它抽離出來(當然你也可以直接抽離Column)。

使用第一種方式:

  Widget _buildText(String text){
    return Text(text, style: const TextStyle(fontSize: 12.0, fontStyle: FontStyle.italic, color: Colors.blue),);
  }
複製程式碼

使用方式:

  Column(
    children: <Widget>[
      _buildText("Android"),
      _buildText("iOS"),
      _buildText("Flutter"),
    ],
  )
複製程式碼

第二種方式:

class _MyText extends StatelessWidget {
  
  const _MyText(this.text, {Key key}) : super(key: key);
  
  final String text;
  
  @override
  Widget build(BuildContext context) {
    return Text(text, style: const TextStyle(fontSize: 12.0, fontStyle: FontStyle.italic, color: Colors.blue),);
  }
}
複製程式碼

使用方式:

  Column(
    children: <Widget>[
      const _MyText("Android"),
      const _MyText("iOS"),
      const _MyText("Flutter"),
    ],
  )
複製程式碼

看起來是第一種很方便,但不知道你有沒有發現第一種方式無法新增 const關鍵字。其實問題就出在了這裡。在我之前的部落格中就有提到儘量使用const關鍵字 來定義常量。那麼這是為什麼呢?

我們來再看一個小例子:

在這裡插入圖片描述

我直接呼叫上面抽出的widget,每次點選按鈕setState重新整理頁面,輸出widget的hashCode。

在這裡插入圖片描述
可以看到使用了const 關鍵字修飾的widget,並不會重複建立。看來養成隨手新增const的好習慣會在無形中提升應用的效能,比如常用的Color、TextStyle、分隔塊我們整合起來,直接呼叫就是很不錯的做法。
在這裡插入圖片描述

這個問題是由Provider的作者Rémi Rousselet提出的,What is the difference between functions and classes to create widgets?

作者得出的結論是:永遠不要使用方法返回的形式建立可重用的widget,始終將它們封裝到StatelessWidget中。 注意這個結論中的可重用

在上面的例子中,我們的文字是固定的(可重用),這導致我們可以直接在widget上新增 const。實際中,我們的展示的資料都是請求的並不是固定的,我們即使抽離出StatelessWidget,也無法直接新增 const來使用。所以如果你的widget並無法重用,使用上述兩種方法的哪一種效果都是一樣的。

上述的例子中,即使Text無法使用const標記,但是Text中的TextStyle確可以重用。這一切取決於你的widget拆分的顆粒度是否足夠合理,來儘可能的避免這種效能上的浪費。

當然我更推薦StatelessWidget的方式。正如作者說的,它具備以下優點:

  • 允許效能優化(const 建構函式,更精細粒度的重建)
  • 有熱過載
  • 整合到widget檢查器中(debugFillProperties
  • 可以定義Key(關於Key的作用可以看這裡的解答
  • 可以方便的使用context
  • 規範所有widget以相同的方式使用(始終使用建構函式)
  • 可以確保在兩個不同佈局之間切換時,正確的配置資訊(函式可能重用一些以前的狀態)

如果你之前已經寫了大量的方法建立返回widget的程式碼,可以使用Rémi Rousselet的functional_widget來改善這個問題。

6.其他

  • 上面有說道抽離出StatelessWidget。其實為了避免因重新整理區域性widget呼叫setState而導致整個頁面重新整理造成的效能損耗,我們可以將區域性重新整理的地方抽離為StatefulWidget。我曾經寫過一個頁面經過這樣的優化,耗時由25ms降到了6ms,因此控制重新整理範圍是很必要的。

  • 一般情況下不建議使用 Offstage來做隱藏功能,雖說它可以隱藏指定的widget。但是它還是會建立出對應的widget,只是放在了看不見的“後臺”。我之前就將一個CupertinoActivityIndicator()這樣隱藏了起來,結果在PerformanceOverlay 中就看到頁面不斷在繪製。。。所以如果你需要隱藏widget,可以使用 isGone ? const SizedBox() : CupertinoActivityIndicator()這類三元運算子的方式處理。

  • 可以將資料解析放在 isolate 中處理,避免某些效能不好的裝置在解析資料時造成的卡頓。詳細例子可以檢視文件


這篇斷斷續續寫了半個月,終於完成了!我可以安心的去參加GDD了。碼字不易,希望點贊支援!最後再次奉上Deer的Github地址,順手也可以支援一波!

相關文章