FlutterのParentDataWidgetエラー、気が付きにくい問題

## わりにやっかいなエラー
`Incorrect use of ParentDataWidget.`
このエラーはFlutterを触ったとことがある人なら誰しもが遭遇しますが、なにもその対応がやっかいというわけではありません。
以下の公式ドキュメント、「よくあるエラー」のページに従って、適切なParentDataWidgetを設定するだけです。（Positionの親WidgetはStack、Expandedの場合は、Row,ColumnやFlexといったように）

https://docs.flutter.dev/testing/common-errors#incorrect-use-of-parentdata-widget

では、一体なにがやっかいなのか。

それはデバッグビルドだと「画面が描画」されてしまうことです。そしてさらに悪いことにリリースビルドでは**画面描画に失敗**します。

もちろんデバッグ時にも、コンソールエラーは吐くので把握自体は可能です。

ただし実際問題、開発中はしばしばコンソールエラーを見逃しがちですし、ホットリロードではこのエラーは検出されず、あくまで初回描画のときのみエラーを出力するので、うっかりしちゃいます。

## なぜデバッグモードでは赤い画面にならず、描画されてしまうのか？

まずこのエラーを出す一例としては、[Flutterのテストコード](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/parent_data_test.dart#L486-L519)から以下のようなものです

```dart
  testWidgets('Parent data invalid ancestor', (WidgetTester tester) async {
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Row(
        children: <Widget>[
        //ExpandedのParentWidgetがStackなので発生
          Stack(
            textDirection: TextDirection.ltr,
            children: <Widget>[
              Expanded(
                child: Container(),
              ),
            ],
          ),
        ],
      ),
    ));

    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(
      exception.toString(),
      startsWith(
        'Incorrect use of ParentDataWidget.\n'
        'The ParentDataWidget Expanded(flex: 1) wants to apply ParentData of type '
        'FlexParentData to a RenderObject, which has been set up to accept ParentData of '
        'incompatible type StackParentData.\n'
        'Usually, this means that the Expanded widget has the wrong ancestor RenderObjectWidget. '
        'Typically, Expanded widgets are placed directly inside Flex widgets.\n'
        'The offending Expanded is currently placed inside a Stack widget.\n'
        'The ownership chain for the RenderObject that received the incompatible parent data was:\n'
        '  LimitedBox ← Container ← Expanded ← Stack ← Row ← Directionality ← ', // Omitted end of debugCreator chain because it's irrelevant for test.
      ),
    );
  });
```

上のテストコードで、`expect(exception, isFlutterError);`とチェックされていることからわかるように、このエラーは`FlutterError.onError`で捕捉可能です。

ではおなじみの赤い画面（＝ErrorWidget）が表示されないのはなぜかというと、**そうならないように実装しているからに他なりません**。

以上。

なのですが、それでは説明になっていない気もするので[framework.dartのここ](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L6686-L6713)を確認します。

```dart
  void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {
    bool applyParentData = true;
    assert(() {
      try {
        if (!parentDataWidget.debugIsValidRenderObject(renderObject)) {
          applyParentData = false;
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('Incorrect use of ParentDataWidget.'),
            ...parentDataWidget._debugDescribeIncorrectParentDataType(
              parentData: renderObject.parentData,
              parentDataCreator: _ancestorRenderObjectElement?.widget as RenderObjectWidget?,
              ownershipChain: ErrorDescription(debugGetCreatorChain(10)),
            ),
          ]);
        }
      } on FlutterError catch (e) {
        // We catch the exception directly to avoid activating the ErrorWidget,
        // while still allowing debuggers to break on exception. Since the tree
        // is in a broken state, adding the ErrorWidget would likely cause more
        // exceptions, which is not good for the debugging experience.
        _reportException(ErrorSummary('while applying parent data.'), e, e.stackTrace);
      }
      return true;
    }());
    if (applyParentData) {
      parentDataWidget.applyParentData(renderObject);
    }
  }
  ```

コメントを翻訳すると、
>ErrorWidgetの起動を避けるため、例外を直接キャッチし、デバッガが例外でブレークできるようにします。
ツリーが壊れている状態なので、ErrorWidgetを追加すると、より多くの例外が発生する可能性があります。

とある通り、このエラー発生時はWidgetTreeで不整合が起きている状態なので、ErrorWidgetをWidgetTreeに差し込む処理をすると、その際にエラーが発生して無限ループに陥ることを示唆しています。
だからErrorWidgetの表示をしていない、というよりは「そうすることができない」、ということです。

またデバッグモードでは描画され、リリースモードだと描画できない理由は、デバッグ時のみ動く`assert`メソッドの中で`debugIsValidRenderObject`のチェックが実行されているため、リリースモードではチェックが動作せず、問答無用で`applyParentData`が実行されるためです。

この点は後述するPRにあるように、Flutterにコントリビュートすることで解決が期待されますが、（しばしば問題となるように）その難しさから放置されているのが現状です。

## 検知する方法をさがす

### exit(1)
前述の通り、FlutterError.onErrorで補足自体は可能なので、

```dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    final isParentDataWidetError = details.summary.value
        .toString()
        .contains('Incorrect use of ParentDataWidget.');
    if (kDebugMode && isParentDataWidetError) {
      exit(1);
    }
  };
  runApp(const MyApp());
}
 ```
とすれば、デバッグ時のみアプリが即落ちしますがそれではあんまりです。

理想としては、「よくあるエラー」の同僚であるPixelOverFlowみたいに出て欲しいところですが、やはりこのエラーに対しては何がしかのWidgetを代わりにTreeに挿入すること自体が御法度です。

結局のところ、Flutterフレームワーク自体を修正しにいくしかない（＝少なくともデバッグモードでもリリースモードと同じく描画に失敗する、という方向）と思います。

コントリビューションについてはその方法に関する素晴らしい記事も最近みかけたことですし、次の担当日までにやろうかな…

## Flutterの悪いところ

というわけで、現状は「即落ちコード」でもいいかと考えています。

冒頭でも言った通り、`Incorrect use of ParentDataWidget.`は基本的なエラーであり、どちらかというと集中力の問題です。

アプリが即落ちしたらコーヒーでも飲んで一呼吸いれるくらい、そんなに悪くないと思いませんか？

Flutterの悪いところは、ホットリロードのせいで、iOSビルド中の古き良きコーヒーブレイクが失われたことかもしれませんから。

## おなじ悩みを抱えるひとたち
最後に、ぼくが考えることは他の誰かも考えてるわけで、同じ主旨のIssueと（惜しくもCloseしてますが）男気のあるPRを紹介します。

https://github.com/flutter/flutter/issues/108186

https://github.com/flutter/flutter/pull/155091