Flutter開發中的一些Tips(二)

接着上篇 Flutter開發中的一些Tips,今天再分享一些我遇到的問題,這篇較上一篇,細節方面更多,希望“引以爲戒”,畢竟細節決定成敗。本篇的所有例子,都在我開源的flutter_deer中。希望Star、Fork支持,有問題可以Issue。附上鍊接:https://github.com/simplezhli...

Logo

1. setState() called after dispose()

這個是我偶然在控制檯發現的,完整的錯誤信息如下:

Unhandled Exception: setState() called after dispose(): _AboutState#9c33a(lifecycle state: defunct, not mounted)

當然flutter在錯誤信息之後還有給出問題原因及解決方法:

This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

大致的意思是,widget已經在dispose方法時銷燬了,但在這之後卻調用了setState方法,那麼會發生此錯誤。比如定時器或動畫回調調用setState(),但此時頁面已關閉時,就會發生此錯誤。這個錯誤一般並不會程序崩潰,只是會造成內存的泄露。

那麼解決的辦法分爲兩部分:

  1. 及時停止或者銷燬監聽,例如一個定時器:
  Timer _countdownTimer;

  @override
  void dispose() {
    _countdownTimer?.cancel();
    _countdownTimer = null;
    super.dispose();
  }
  1. 爲了保險我們還要在調用setState()前判斷當前頁面是否存在:
  _countdownTimer = Timer.periodic(Duration(seconds: 2), (timer) {
    if (mounted){
      setState(() {
        
      });
    }
  });    

我們可以看看 mounted在源碼中是什麼

  BuildContext get context => _element;
  StatefulElement _element;

  /// Whether this [State] object is currently in a tree.
  ///
  /// After creating a [State] object and before calling [initState], the
  /// framework "mounts" the [State] object by associating it with a
  /// [BuildContext]. The [State] object remains mounted until the framework
  /// calls [dispose], after which time the framework will never ask the [State]
  /// object to [build] again.
  ///
  /// It is an error to call [setState] unless [mounted] is true.
  bool get mounted => _element != null;

BuildContextElement的抽象類,你可以認爲mounted 就是 context 是否存在。那麼同樣在回調中用到 context時,也需要判斷一下mounted。比如我們要彈出一個 Dialog 時,或者在請求接口成功時退出當前頁面。BuildContext的概念是比較重要,需要掌握它,錯誤使用一般雖不會崩潰,但是會使得代碼無效。

本問題詳細的代碼見:點擊查看

2.監聽Dialog的關閉

問題描述:我在每次的接口請求前都會彈出一個Dialog 做loading提示,當接口請求成功或者失敗時關閉它。可是如果在請求中,我們點擊了返回鍵人爲的關閉了它,那麼當真正請求成功或者失敗關閉它時,由於我們調用了Navigator.pop(context) 導致我們錯誤的關閉了當前頁面。

那麼解決問題的突破口就是知道何時Dialog的關閉,那麼就可以使用 WillPopScope 攔截到返回鍵的輸入,同時記錄到Dialog的關閉。


  bool _isShowDialog = false;

  void closeDialog() {
    if (mounted && _isShowDialog){
      _isShowDialog = false;
      Navigator.pop(context);
    }
  }
  
  void showDialog() {
    /// 避免重複彈出
    if (mounted && !_isShowDialog){
      _isShowDialog = true;
      showDialog(
        context: context,
        barrierDismissible: false,
        builder:(_) {
          return WillPopScope(
            onWillPop: () async {
              // 攔截到返回鍵,證明dialog被手動關閉
              _isShowDialog = false;
              return Future.value(true);
            },
            child: ProgressDialog(hintText: "正在加載..."),
          );
        }
      );
    }
  }

本問題詳細的代碼見:點擊查看

3.addPostFrameCallback

addPostFrameCallback回調方法在Widget渲染完成時觸發,所以一般我們在獲取頁面中的Widget大小、位置時使用到。

前面第二點我有說到我會在接口請求前彈出loading。如果我將請求方法放在了initState方法中,異常如下:

inheritFromWidgetOfExactType(_InheritedTheme) or inheritFromElement() was called before initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.

原因:彈出一個DIalog的showDialog方法會調用Theme.of(context, shadowThemeOnly: true),而這個方法會通過inheritFromWidgetOfExactType來跨組件獲取Theme對象。

在這裏插入圖片描述

inheritFromWidgetOfExactType方法調用inheritFromElement
在這裏插入圖片描述

但是在_StateLifecyclecreateddefunct 時是無法跨組件拿到數據的,也就是initState()時和dispose()後。所以錯誤信息提示我們在 didChangeDependencies 調用。

然而放在didChangeDependencies後,新的異常:

setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.

提示我們必須在頁面build時,纔可以去創建這個新的組件(這裏就是Dialog)。

所以解決方法就是使用addPostFrameCallback回調方法,等待頁面build完成後在請求數據:

  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((_){
      /// 接口請求
    });
  }
 

導致這類問題的場景很多,但是大體解決思路就是上述的辦法。

本問題詳細的代碼見:點擊查看

4.刪除emoji

不多嗶嗶,直接看圖:

問題

簡單說就是刪除一個emoji表情,一般需要點擊刪除兩次。碰到個別的emoji,需要刪除11次!!其實這問題,也別吐槽Flutter,基本emoji在各個平臺上都或多或少有點問題。

原因就是:
表情長度
這個問題我發現在Flutter 的1.5.4+hotfix.2版本,解決方法可以參考:https://github.com/flutter/en... 雖然只適用於長度爲2位的emoji。

幸運的是在最新的穩定版1.7.8+hotfix.3中修復了這個問題。不幸的是我發現了其他的問題,比如在我小米MIX 2s上刪除文字時,有時會程序崩潰,其他一些機型正常。異常如下圖:

崩潰信息

我也在Flutter上發現了同樣的問題Issue,具體情況可以關注這個Issue :https://github.com/flutter/fl... ,據Flutter團隊的人員的回覆,這個問題修復後不太可能進入1.7的穩定版本。。

Issue

所以建議大家謹慎升級,尤其是用於生產環境。那麼這個問題暫時只能擱置下來了,等待更穩定的版本。。。

5.鍵盤

1.是否彈起

MediaQuery.of(context).viewInsets.bottom > 0

viewInsets.bottom就是鍵盤的頂部距離底部的高度,也就是彈起的鍵盤高度。如果你想實時過去鍵盤的彈出狀態,配合使用didChangeMetrics。完整如下:

import 'package:flutter/material.dart';

typedef KeyboardShowCallback = void Function(bool isKeyboardShowing);

class KeyboardDetector extends StatefulWidget {

  KeyboardShowCallback keyboardShowCallback;

  Widget content;

  KeyboardDetector({this.keyboardShowCallback, @required this.content});

  @override
  _KeyboardDetectorState createState() => _KeyboardDetectorState();
}

class _KeyboardDetectorState extends State<KeyboardDetector>
    with WidgetsBindingObserver {
  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      print(MediaQuery.of(context).viewInsets.bottom);
      setState(() {
        widget.keyboardShowCallback
            ?.call(MediaQuery.of(context).viewInsets.bottom > 0);
      });
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.content;
  }
}

代碼來自項目GSYFlutterDemo:https://github.com/CarGuo/GSYFlutterDemo

2.彈出鍵盤

if (MediaQuery.of(context).viewInsets.bottom == 0){
  final focusScope = FocusScope.of(context);
  focusScope.requestFocus(FocusNode());
  Future.delayed(Duration.zero, () => focusScope.requestFocus(_focusNode));
}

其中_focusNode是對應的TextFieldfocusNode屬性。

3.關閉鍵盤

FocusScope.of(context).requestFocus(FocusNode());

這裏提一下關閉,一般來說即使鍵盤彈出,點擊返回頁面關閉,鍵盤就會自動收起。但是順序是:

頁面關閉 --> 鍵盤關閉

這樣會導致鍵盤短暫的出現在你的上一頁面,也就會出現短暫的部件溢出(關於溢出可見上篇)。

所以這時你就需要在頁面關閉前手動調用關閉鍵盤的代碼了。按道理是要放到deactivate或者dispose中處理的,可誰讓context已經爲null了,所以,老辦法,攔截返回鍵:

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        // 攔截返回鍵
        FocusScope.of(context).requestFocus(FocusNode());
        return Future.value(true);
      },
      child: Container()
    );
  }   

本問題詳細的代碼見:點擊查看

6.Android 9.0適配

話說現在新建的Flutter項目,Android的 targetSdkVersion 默認都是28。所以不可避免的就是Android 9.0的適配甚至6,7,8的適配,那我碰到的一個問題是接入的高德2D地圖在9.0的機子上顯示不出來。

問題的主要原因是Android 9.0 要求默認使用加密連接,簡單地說就是不允許使用http請求,要求使用https。高德的2D地圖sdk懷疑是使用了http請求,所以會加載不出。

解決方法兩個:

    1. targetSdkVersion 改爲28以下(長遠看來不推薦)
    1. android -> app - > src -> main -> res 目錄下新建xml,添加network_security_config.xml文件:
    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <base-config cleartextTrafficPermitted="true" />
    </network-security-config>

    AndroidManifest.xml 中的application添加:

    android:networkSecurityConfig="@xml/network_security_config"

    這個問題只是Android適配中的一小部分,相應的iOS中也有適配問題。比如常用的權限適配等。

    不得不說做Flutter的開發需要對原生開發有一定了解。尤其是之前在寫Flutter的地圖插件時感受深刻,那麼我本來就是做Android開發的,所以Android端的部分很快就完成了。iOS部分就很吃力,首先OC的語法就不會,其次說實話寫完了心裏也沒底,還是需要向iOS的同事請教確保一下。所以跨平臺方案的出現並不會對原生開發造成衝擊,反而是對原生開發提出了更高的要求。

    本問題詳細的代碼見:點擊查看

    7.其他

    1. Flutter開發中的json解析確實很麻煩,當然有許多的插件來解決我們的問題。我個人推薦使用FlutterJsonBeanFactory。關於它的一系列使用可以參看:https://www.jianshu.com/nb/33...
    2. UI層面的功能最好還是使用Flutter來解決。比如Toast功能,很多人都會選擇fluttertoast這個插件,而我推薦oktoast這類使用Flutter的解決方案 。因爲fluttertoast是調用了Android原生的Toast,首先在各個系統上的樣式就不統一,同時部分系統機型上受限通知權限,會導致Toast無法彈出。

    篇幅有限,那麼先分享以上幾條Tips,如果本篇對你有所幫助,可以點贊支持!其實收藏起來不是以後遇到問題時查找更方便嗎🤔。

    最後再次奉上Github地址:https://github.com/simplezhli...

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章