PR

[Flutter]Flutter Widget of the Week「#15 InheritedModel」、「#16 ClipRRect」、「#17 Hero」

Flutter

はじめに

前回はFlutter Widget of the Weekの「#13FadeInImage」、「#14 StreamBuilder」を紹介しました。

今回はその続きで「#15 InheritedModel」、「#16 ClipRRect」、「#17 Hero」の3つです。

の記事はこちら

Flutter Widget of the Week

環境

  • Flutter 2.8.1

記事にした時点でのバージョンです。GitHubに公開しているのは常に最新の安定版(stable)を使う予定です。

#15 InheritedModel

InheritedModelとは

InheritedWidgetと組み合わせて利用します。

InheritedWidgetの変更を購読している子孫のウィジェットの中で、一部のウィジェットだけに変更を伝えたい場合に利用します。

そうすることによりウィジェット全体をビルドすることなく効率的に画面をビルドすることができます。

サンプルコード

import 'package:flutter/material.dart';

class SamplePage015 extends StatelessWidget {
  const SamplePage015({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('InheritedModel'),
      ),
      body: _SamplePage015Stateful(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            _SamplePage015ColorOneWidget(),
            _SamplePage015ColorTwoWidget(),
            _SamplePage015ColorChangerWidget(),
          ],
        ),
      ),
    );
  }
}

class _SamplePage015Stateful extends StatefulWidget {
  const _SamplePage015Stateful({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

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

class _SamplePage015StatefulState extends State<_SamplePage015Stateful> {
  Color colorOne = Colors.red;
  Color colorTwo = Colors.blue;

  static _SamplePage015StatefulState? of(BuildContext context) {
    return context.findAncestorStateOfType<_SamplePage015StatefulState>();
  }

  void changeColorOne() {
    setState(() {
      colorOne = (colorOne == Colors.red) ? Colors.green : Colors.red;
    });
  }

  void changeColorTwo() {
    setState(() {
      colorTwo = (colorTwo == Colors.blue) ? Colors.yellow : Colors.blue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return _SamplePage015InheritedModel(
      colorOne,
      colorTwo,
      widget.child,
    );
  }
}

class _SamplePage015InheritedModel extends InheritedModel<String> {
  const _SamplePage015InheritedModel(
    this.colorOne,
    this.colorTwo,
    Widget child,
  ) : super(child: child);

  final Color colorOne;
  final Color colorTwo;

  // ignore: unused_element
  static _SamplePage015InheritedModel? of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<_SamplePage015InheritedModel>();
  }

  @override
  bool updateShouldNotify(_SamplePage015InheritedModel oldWidget) {
    return colorOne != oldWidget.colorOne || colorTwo != oldWidget.colorTwo;
  }

  @override
  bool updateShouldNotifyDependent(
    _SamplePage015InheritedModel oldWidget,
    Set<String> dependencies,
  ) {
    if (dependencies.contains('one') && colorOne != oldWidget.colorOne) {
      return true;
    }
    if (dependencies.contains('two') && colorTwo != oldWidget.colorTwo) {
      return true;
    }
    return false;
  }
}

class _SamplePage015ColorOneWidget extends StatelessWidget {
  const _SamplePage015ColorOneWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ignore: avoid_print
    print('_SamplePage015ColorOneWidget#build');
    final inherited = InheritedModel.inheritFrom<_SamplePage015InheritedModel>(
      context,
      aspect: 'one',
    );
    return Container(
      color: inherited?.colorOne,
      height: 100,
      width: 100,
    );
  }
}

class _SamplePage015ColorTwoWidget extends StatelessWidget {
  const _SamplePage015ColorTwoWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ignore: avoid_print
    print('_SamplePage015ColorTwoWidget#build');
    final inherited = InheritedModel.inheritFrom<_SamplePage015InheritedModel>(
      context,
      aspect: 'two',
    );
    return Container(
      color: inherited?.colorTwo,
      height: 100,
      width: 100,
    );
  }
}

class _SamplePage015ColorChangerWidget extends StatelessWidget {
  const _SamplePage015ColorChangerWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ignore: avoid_print
    print('_SamplePage015ColorChangerWidget#build');
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: () =>
              _SamplePage015StatefulState.of(context)?.changeColorOne(),
          child: const Text('Change colorOne'),
        ),
        ElevatedButton(
          onPressed: () =>
              _SamplePage015StatefulState.of(context)?.changeColorTwo(),
          child: const Text('Change colorTwo'),
        ),
      ],
    );
  }
}

結果

初期画面

「Change colorOne」を押下すると上の色が変わります。

その時にログを見ると

flutter: _SamplePage015ColorOneWidget#build

不要なビルドが発生していないのがわかります。

次に「Change colorTwo」を押下すると下の色が変わります。

その時にログを見ると

flutter: _SamplePage015ColorTwoWidget#build

こちらも不要なビルドが発生していないのがわかります。

動画

公式リファレンス

InheritedModel class - widgets library - Dart API
API docs for the InheritedModel class from the widgets library, for the Dart programming language.

#16 ClipRRect

ClipRRectとは

画像やコンテンツの角をきれいに丸くしてくれるウィジェットです。

ClipOvalClipPathなど類似のウィジェットもあるようです。

サンプルコード

import 'package:flutter/material.dart';

class SamplePage016 extends StatelessWidget {
  const SamplePage016({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ClipRRect'),
      ),
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('ClipRRect'),
              ClipRRect(
                borderRadius: BorderRadius.circular(15),
                child: _SamplePage016ChildWidget(),
              ),
              const SizedBox(height: 20),
              const Text('ClipOval'),
              ClipOval(
                child: _SamplePage016ChildWidget(),
              ),
              const SizedBox(height: 20),
              const Text('ClipPath'),
              ClipPath(
                clipper: _SamplePage016CustomClipperPath(),
                child: _SamplePage016ChildWidget(),
              )
            ],
          ),
        ),
      ),
    );
  }
}

class _SamplePage016ChildWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 128,
      color: Colors.blue,
      child: const Center(
        child: FlutterLogo(
          size: 100,
        ),
      ),
    );
  }
}

class _SamplePage016CustomClipperPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path()
      ..moveTo(size.width / 2, 0)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return false;
  }
}

結果

動画

公式リファレンス

ClipRRect class - widgets library - Dart API
API docs for the ClipRRect class from the widgets library, for the Dart programming language.

#17 Hero

Heroとは

違う画面で同一の画像を利用している際に利用するときにオシャレに画面遷移することができます。

言葉で説明するよりも見たほうが早いと思います。

サンプルコード

import 'package:flutter/material.dart';

class SamplePage017 extends StatelessWidget {
  const SamplePage017({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hero'),
      ),
      body: SafeArea(
        child: ListView(
          children: [
            ListTile(
              leading: Hero(
                tag: 'sample017',
                child: ClipOval(
                  child: Image.asset('assets/sample.png'),
                ),
              ),
              title: const Text('Hero Sample'),
              trailing: const Icon(Icons.navigate_next),
              onTap: () {
                Navigator.push<void>(
                  context,
                  MaterialPageRoute(
                    builder: (context) {
                      return _SamplePage017DetailPage();
                    },
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _SamplePage017DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Hero'),
      ),
      body: SafeArea(
        child: Column(
          children: [
            Center(
              child: Hero(
                tag: 'sample017',
                child: Image.asset('assets/sample.png'),
              ),
            ),
            const SizedBox(height: 20),
            const Text('Hero Sample')
          ],
        ),
      ),
    );
  }
}

結果

ListTileを選択すると

画面遷移しながらシームレスに画像が拡大したようなアニメーションをしてくれます。

動画

公式リファレンス

Hero class - widgets library - Dart API
API docs for the Hero class from the widgets library, for the Dart programming language.

さいごに

InheritedModelはイマイチうまくできなかったので、使うことはなさそうです。

ClipRRect系の自分でPathとかを考えるのは時間がかかりそうですね。

Heroは今後使ってみたいと思いました。

おすすめ参考書

リンク

GitHub - nobushiueshi/flutter_widget_of_the_week
Contribute to nobushiueshi/flutter_widget_of_the_week development by creating an account on GitHub.

コメント

タイトルとURLをコピーしました