Flutterで、あるWidgetのレイアウト後の実際のサイズを取得する

FlutterでのUIビルディングは、最終的に表示されるUIの構造（とプロパティ）を、Widgetのツリーで定義します。アプリを作る開発者は、そのUIの各要素が、どのようにレイアウトされレンダリングされたかの詳細を知る必要は、ほとんどありません。

ただし、まれにではありますが、あるWidgetが、実際のレイアウト時にどのようなサイズになっているかを知りたいケースがある……かもしれません。

アプリ開発者が通常扱う Widget とは別の、より低レイヤの Render でレイアウトが行われており、ここまで踏み込むとサイズがわかります。

そこで以下に、あるWidgetのレイアウト・サイズを、コールバックとして受け取るSizeListenableContainerというクラスを実装してみました。

※コードはMITライセンスとします

```dart
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

typedef SizeChangedCallback = void Function(Size size);

class SizeListenableContainer extends SingleChildRenderObjectWidget {
  const SizeListenableContainer({
    Key key,
    Widget child,
    @required this.onSizeChanged,
  })  : assert(onSizeChanged != null),
        super(key: key, child: child);

  final SizeChangedCallback onSizeChanged;

  @override
  _SizeListenableRenderObject createRenderObject(BuildContext context) {
    return _SizeListenableRenderObject(onSizeChanged: onSizeChanged);
  }
}

class _SizeListenableRenderObject extends RenderProxyBox {
  _SizeListenableRenderObject({
    RenderBox child,
    @required this.onSizeChanged,
  })  : assert(onSizeChanged != null),
        super(child);

  final SizeChangedCallback onSizeChanged;

  Size _oldSize;

  @override
  void performLayout() {
    super.performLayout();

    final Size size = this.size;
    if (size != _oldSize) {
      _oldSize = size;
      _callback(size);
    }
  }
  
  void _callback(Size size) {
    SchedulerBinding.instance.addPostFrameCallback((Duration _) {
      onSizeChanged(size);
    });
  }
}
```

これを使う側の例は以下の通り。

```dart
class HomePage extends StatefulWidget {
  @override
  HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
  String _text = "あ";
  Size _size;

  @override
  Widget build(BuildContext context) {
    final String sizeText = _size?.toString() ?? "null";
    return Scaffold(
      appBar: AppBar(
        title: Text("Widget Size Listening"),
      ),
      body: Container(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: <Widget>[
            SizeListenableContainer(
              child: Container(
                color: Colors.blue,
                child: Text(
                  _text,
                  style: const TextStyle(color: Colors.white),
                ),
              ),
              onSizeChanged: (Size size) {
                setState(() {
                  _size = size;
                });
              },
            ),
            Text("上のWidgetのSizeは(${sizeText})です"),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          setState(() {
            _text += "あ";
          });
        },
      ),
    );
  }
}

```

以下のように、Textを含むContainerのサイズを取得できています。

<img width="505" alt="スクリーンショット 2019-07-30 15.05.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21341/0ec78fe1-415f-50d4-64e1-d0190b03f8a8.png">

onSizeChangedでsetStateするのはよいですが、それによってSizeListenableContainer.childのサイズが変わるとリビルド・ループになるので、使い方は気をつけてください。

----

なお、サイズを知りたいというより、レイアウトをコントロールしたいというのが目的あれば [CustomSingleChildLayout](https://api.flutter.dev/flutter/widgets/CustomSingleChildLayout-class.html) や [CustomMultiChildLayout](https://api.flutter.dev/flutter/widgets/CustomMultiChildLayout-class.html) を使います。