Flutter開發中的一些Tips(二)

接着上篇 Flutter開發中的一些Tips,今天再分享一些我遇到的問題,這篇較上一篇,細節方面更多,但願「引覺得戒」,畢竟細節決定成敗。本篇的全部例子,都在我開源的flutter_deer中。但願Star、Fork支持,有問題能夠Issue。附上連接: https://github.com/simplezhli...

Logo

1. setState() called after dispose()

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

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

固然flutter在錯誤信息以後還有給出問題緣由及解決方法:android

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(),但此時頁面已關閉時,就會發生此錯誤。這個錯誤通常並不會程序崩潰,只是會形成內存的泄露。git

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

  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在源碼中是什麼json

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的概念是比較重要,須要掌握它,錯誤使用通常雖不會崩潰,可是會使得代碼無效。app

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

2.監聽Dialog的關閉

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

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

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...

    相關文章
    相關標籤/搜索