Flutter 掉坑錯誤


典型錯誤一:無法掌握的Future

典型錯誤信息:NoSuchMethodError: The method 'markNeedsBuild' was called on null.

這個錯誤常出現在異步任務(Future)處理,比如某個頁面請求一個網絡API數據,根據數據刷新 Widget State。

異步任務結束在頁面被pop之后,但沒有檢查State 是否還是 mounted,繼續調用 setState 就會出現這個錯誤。

示例代碼

一段很常見的獲取網絡數據的代碼,調用 requestApi(),等待Future從中獲取response,進而setState刷新 Widget:

class AWidgetState extends State<AWidget> {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
setState((){
this.data = response.data;
})
}
}

原因分析

response 的獲取為async-await異步任務,完全有可能在AWidgetState被 dispose之后才等到返回,那時候和該State 綁定的 Element 已經不在了。故而在setState時需要容錯。

解決辦法: setState之前檢查是否 mounted

class AWidgetState extends State {
// ...
var data;
void loadData() async {
var response = await requestApi(...);
if (mounted) {
setState((){
this.data = response.data;
})
}
}
}

這個mounted檢查很重要,其實只要涉及到異步還有各種回調(callback),都不要忘了檢查該值。

比如,在 FrameCallback里執行一個動畫(AnimationController):

@override
void initState(){
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _animationController.forward();
});
}

AnimationController有可能隨着 State 一起 dispose了,但是FrameCallback仍然會被執行,進而導致異常。

又比如,在動畫監聽的回調里搞點事:

@override
void initState(){
_animationController.animation.addListener(_handleAnimationTick);
}


void _handleAnimationTick() {
if (mounted) updateWidget(...);
}

同樣的在_handleAnimationTick被回調前,State 也有可能已經被dispose了。

如果你還不理解為什么,請仔細回味一下Event loop 還有復習一下 Dart 的線程模型。

典型錯誤二:Navigator.of(context) 是個 null

典型錯誤信息:NoSuchMethodError: The method 'pop' was called on null.

常在 showDialog 后處理 dialog 的 pop() 出現。

示例代碼

在某個方法里獲取網絡數據,為了更好的提示用戶,會先彈一個 loading 窗,之后再根據數據執行別的操作...

// show loading dialog on request data
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
return Center(
child: CircularIndicator(),
);
},
);
var data = (await requestApi(...)).data;
// got it, pop dialog
Navigator.of(context).pop();

原因分析:

出錯的原因在於—— Android 原生的返回鍵:雖然代碼指定了barrierDismissible: false,用戶不可以點半透明區域關閉彈窗,但當用戶點擊返回鍵時,Flutter 引擎代碼會調用 NavigationChannel.popRoute(),最終這個 loading dialog 甚至包括頁面也被關掉,進而導致Navigator.of(context)返回的是null,因為該context已經被unmount,從一個已經凋零的樹葉上是找不到它的根的,於是錯誤出現。

另外,代碼里的Navigator.of(context) 所用的context也不是很正確,它其實是屬於showDialog調用者的而非 dialog 所有,理論上應該用builder里傳過來的context,沿着錯誤的樹干雖然也能找到根,但實際上不是那么回事,特別是當你的APP里有Navigator嵌套時更應該注意。

解決辦法

首先,確保 Navigator.of(context) 的 context 是 dialog 的context;其次,檢查 null,以應對被手動關閉的情況。

showDialog 時傳入 GlobalKey,通過 GlobalKey去獲取正確的context

GlobalKey key = GlobalKey();


showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) {
return KeyedSubtree(
key: key,
child: Center(
child: CircularIndicator(),
)
);
},
);
var data = (await requestApi(...)).data;


if (key.currentContext != null) {
Navigator.of(key.currentContext)?.pop();
}

key.currentContext 為null意為着該 dialog 已經被dispose,亦即已經從 WidgetTree 中unmount

其實,類似的XXX.of(context)方法在 Flutter 代碼里很常見,比如 MediaQuery.of(context)Theme.of(context)DefaultTextStyle.of(context)DefaultAssetBundle.of(context)等等,都要注意傳入的context是來自正確節點的,否則會有驚喜在等你。

寫 Flutter 代碼時,腦海里一定要對context的樹干脈絡有清晰的認知,如果你還不是很理解context,可以看看 《深入理解BuildContext》 - Vadaski。

典型錯誤三:ScrollController 里薛定諤的 position

在獲取ScrollControllerpositionoffset,或者調用jumpTo()等方法時,常出現StateError錯誤。

錯誤信息:StateError Bad state: Too many elementsStateError Bad state: No element

示例代碼

在某個按鈕點擊后,通過ScrollController 控制ListView滾動到開頭:

final ScrollController _primaryScrollController = ScrollController();
// 回到開頭
void _handleTap() {
if(_primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

原因分析

先看ScrollController的源碼:

class ScrollController extends ChangeNotifier {
//...
@protected
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[];

double get offset => position.pixels;

ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
return _positions.single;
}
//...
}

很明顯,ScrollController 的 offest 是從 position 中獲得,而position 則是來自變量 _positions

StateError錯誤,就是_positions.single 這一行拋出:

abstract class Iterable<E> {
//...
E get single {
Iterator<E> it = iterator;
if (!it.moveNext()) throw IterableElementError.noElement();
E result = it.current;
if (it.moveNext()) throw IterableElementError.tooMany();
return result;
}
//...
}

那么問題來了,這個_positions 為什么忽而一滴不剩,忽而卻嫌它給的太多了呢?ˊ_>ˋ

還是要回到 ScrollController 的源碼里找找。

class ScrollController extends ChangeNotifier {
// ...
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}


void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}
}
  1. 為什么沒有數據(No element): ScrollController還沒有 attach 一個 position。原因有兩個:一個可能是還沒被 mount 到樹上(沒有被Scrollable使用到);另外一個就是已經被 detach了。

  1. 為什么多了(Too many elements): ScrollController還沒來得及 detach舊的 position,就又attach了一個新的。原因多半是因為ScrollController的用法不對,同一時間被多個 Scrollable關注到了。

解決辦法

針對 No element 錯誤,只需判斷一下 _positions是不是空的就行了,即hasClients

final ScrollController _primaryScrollController = ScrollController();
// 回到開頭
void _handleTap() {
if(_primaryScrollController.hasClients && _primaryScrollController.offset > 0) _primaryScrollController.jumpTo(0.0)
}

針對 Too many elements 錯誤,確保ScrollController只會被一個 Scrollable綁定,別讓它劈腿了,且被正確 dispose()

class WidgetState extends State {
final ScrollController _primaryScrollController = ScrollController();


@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _primaryScrollController,
itemCount: _itemCount,
itemBuilder: _buildItem,
)
}


int get _itemCount => ...;
Widget _buildItem(context, index) => ...;


@override
void dispose() {
super.dispose();
_primaryScrollController.dispose();
}
}

典型錯誤四:四處碰壁 null

Dart 這個語言可靜可動,類型系統也獨樹一幟。萬物都可以賦值null,就導致寫慣了 Java 代碼的同志們常常因為bool int double這種看起來是"primitive"的類型被null附體而頭暈。

典型錯誤信息:

  • Failed assertion: boolean expression must not be null

  • NoSuchMethodError: The method '>' was called on null.

  • NoSuchMethodError: The method '+' was called on null.

  • NoSuchMethodError: The method '*' was called on null.

示例代碼

這種錯誤,較常發生在使用服務端返回的數據model時。

class StyleItem {
final String name;
final int id;
final bool hasNew;


StyleItem.fromJson(Map<String, dynamic> json):
this.name = json['name'],
this.id = json['id'],
this.hasNew = json['has_new'];
}


StyleItem item = StyleItem.fromJson(jsonDecode(...));


Widget build(StyleItem item) {
if (item.hasNew && item.id > 0) {
return Text(item.name);
}
return SizedBox.shrink();
}

原因分析

StyleItem.fromJson() 對數據沒有容錯處理,應當認為 map 里的value都有可能是 null

解決辦法:容錯

class StyleItem {
final String name;
final int id;
final bool hasNew;


StyleItem.fromJson(Map<String, dynamic> json):
this.name = json['name'],
this.id = json['id'] ?? 0,
this.hasNew = json['has_new'] ?? false;
}

一定要習慣 Dart 的類型系統,什么都有可能是null,比如下面一段代碼,你細品有幾處可能報錯:

class Test {
double fraction(Rect boundsA, Rect boundsB) {
double areaA = boundsA.width * boundsA.height;
double areaB = boundsB.width * boundsB.height;
return areaA / areaB;
}

void requestData(params, void onDone(data)) {
_requestApi(params).then((response) => onDone(response.data));
}

Future<dynamic> _requestApi(params) => ...;
}

小提示,onDone()也可以是null >﹏<。

在和原生用 MethodChannel傳數據時更要特別注意,小心駛得萬年船。

典型錯誤五:泛型里的 dynamic 一點也不 dynamic

典型錯誤信息:

  • type 'List<dynamic>' is not a subtype of type 'List<int>'

  • type '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, String>'

常發生在給某個List、Map 變量賦值時。

示例代碼

這種錯誤,也較常發生在使用服務端返回的數據model時。

class Model {
final List<int> ids;
final Map<String, String> ext;


Model.fromJson(Map<String, dynamic> json):
this.ids = json['ids'],
this.ext= json['ext'];
}


var json = jsonDecode("""{"ids": [1,2,3], "ext": {"key": "value"}}""");
Model m = Model.fromJson(json);

原因分析

jsonDecode()這個方法轉換出來的map的泛型是Map<String, dynamic>,意為 value 可能是任何類型(dynamic),當 value 是容器類型時,它其實是List<dynamic>或者Map<dynamic, dynamic>等等。

而 Dart 的類型系統中,雖然dynamic可以代表所有類型,在賦值時,如果數據類型事實上匹配(運行時類型相等)是可以被自動轉換,但泛型里 dynamic 是不可以自動轉換的。可以認為 List<dynamic> 和 List<int>是兩種運行時類型。

解決辦法:使用 List.from, Map.from

class Model {
final List<int> ids;
final Map<String, String> ext;


Model.fromJson(Map<String, dynamic> json):
this.ids = List.from(json['ids'] ?? const []),
this.ext= Map.from(json['ext'] ?? const {});
}

總結

綜上所述,這些典型錯誤,都不是什么疑難雜症,而是不理解或者不熟悉 Flutter 和 Dart 語言所導致的,關鍵是要學會容錯處理。

但容錯辦法又來自於一次次經驗教訓,誰也不能憑空就認識到要做什么樣的錯誤處理,所以相信在經過一段時間到處踩坑的洗禮后,初學者也可以快速成長,將來各個都是精通。


學習來源:https://yrom.net/blog/2020/03/13/The-most-often-errors-in-Flutter/


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM