📱

flutter_screenutilを使ってみた

2024/03/12に公開

📕Overview

https://pub.dev/packages/flutter_screenutil
画面とフォント サイズを調整するための Flutter プラグイン。さまざまな画面サイズで UI に適切なレイアウトを表示させます。

注: このプラグインはまだ開発中であるため、一部の API はまだ利用できない可能性があります。

Properties

Property Type Default Value(初期値) Description
designSize Size Size(360,690) デザインドラフトのデバイス画面のサイズ (dp)
builder Function null プロパティ内のライブラリを使用するウィジェットを返します (例: MaterialApp のテーマ)
child Widget null 依存関係/プロパティがライブラリを使用しないビルダーの一部
rebuildFactor Function default 新旧の画面メトリクスを取得し、変更時に再構築するかどうかを返す関数。
splitScreenMode bool false 分割画面のサポート
minTextAdapt bool false 幅と高さの最小値に従ってテキストを調整するかどうか
context BuildContext null 物理デバイス データが提供されていない場合は、MediaQuery.of(context) によって取得します。
fontSizeResolver Function default フォント サイズをどのように調整するかを指定する関数。デフォルトでは、フォント サイズは画面の幅に合わせて調整されます。
reponsiveWidgets Iterable null ツリーの再構築に含める必要があるウィジェット名のリスト/セット。( 「flutter_screenutil がウィジェットにビルドが必要であることをマークする方法」を参照)

注: ビルダー、子、または両方を指定する必要があります。

リストを再構築
バージョン 5.9.0 以降、ScreenUtilInit はウィジェット ツリー全体を再構築せず、代わりに次の場合にのみウィジェットにビルドが必要であることをマークします。

ウィジェットは Flutter ウィジェットではありません (ウィジェットはFlutter Docsで利用可能です)
_ウィジェットがアンダースコア ( )で始まっていません
ウィジェットがSUミックスインを宣言していません
reponsiveWidgetsウィジェット名が含まれていません
ライブラリを使用するウィジェットがあり、これらのオプションを満たさない場合は、SUmixin を追加するか、応答ウィジェット リストにウィジェット名を追加できます。

システムの「フォント サイズ」アクセシビリティ オプションに従って、フィット サイズとフォント サイズを初期化し、拡大/縮小するように設定します#
ご使用前にデザイン案のサイズ、デザイン案の幅と高さを設定してください。

使い方は、READMEに書いてあるように、MaterialAppで使わなくも良いです。私はページごとに設定しました。ScreenUtilInitクラスは、内部実装を見てみると、StatefulWidgetでした。

ScreenUtilInit classの役割は...
フィットサイズを設定する(UIデザインを探し、デバイスの画面の寸法を見て、dp単位で記入する)

builderパラメーターの役割は...
ScreenUtilInitコンテキストの外でライブラリを使用する必要がある場合のみ、ビルダーを使用する。
使用するときは、()の中に、第1引数は、ctxとか_で良いみたいで、第2引数には、childを指定します。

内部実装のコードもご紹介しておきます。ご興味ある人はみてください。

内部実装
import 'dart:async';
import 'dart:collection';

import 'package:flutter/widgets.dart';
import './_flutter_widgets.dart';

import 'screenutil_mixin.dart';
import 'screen_util.dart';

typedef RebuildFactor = bool Function(MediaQueryData old, MediaQueryData data);

typedef ScreenUtilInitBuilder = Widget Function(
  BuildContext context,
  Widget? child,
);

abstract class RebuildFactors {
  static bool size(MediaQueryData old, MediaQueryData data) {
    return old.size != data.size;
  }

  static bool orientation(MediaQueryData old, MediaQueryData data) {
    return old.orientation != data.orientation;
  }

  static bool sizeAndViewInsets(MediaQueryData old, MediaQueryData data) {
    return old.viewInsets != data.viewInsets;
  }

  static bool change(MediaQueryData old, MediaQueryData data) {
    return old != data;
  }

  static bool always(MediaQueryData _, MediaQueryData __) {
    return true;
  }

  static bool none(MediaQueryData _, MediaQueryData __) {
    return false;
  }
}

abstract class FontSizeResolvers {
  static double width(num fontSize, ScreenUtil instance) {
    return instance.setWidth(fontSize);
  }

  static double height(num fontSize, ScreenUtil instance) {
    return instance.setHeight(fontSize);
  }

  static double radius(num fontSize, ScreenUtil instance) {
    return instance.radius(fontSize);
  }

  static double diameter(num fontSize, ScreenUtil instance) {
    return instance.diameter(fontSize);
  }

  static double diagonal(num fontSize, ScreenUtil instance) {
    return instance.diagonal(fontSize);
  }
}

class ScreenUtilInit extends StatefulWidget {
  /// A helper widget that initializes [ScreenUtil]
  const ScreenUtilInit({
    Key? key,
    this.builder,
    this.child,
    this.rebuildFactor = RebuildFactors.size,
    this.designSize = ScreenUtil.defaultSize,
    this.splitScreenMode = false,
    this.minTextAdapt = false,
    this.useInheritedMediaQuery = false,
    this.ensureScreenSize,
    this.responsiveWidgets,
    this.fontSizeResolver = FontSizeResolvers.width,
  }) : super(key: key);

  final ScreenUtilInitBuilder? builder;
  final Widget? child;
  final bool splitScreenMode;
  final bool minTextAdapt;
  final bool useInheritedMediaQuery;
  final bool? ensureScreenSize;
  final RebuildFactor rebuildFactor;
  final FontSizeResolver fontSizeResolver;

  /// The [Size] of the device in the design draft, in dp
  final Size designSize;
  final Iterable<String>? responsiveWidgets;

  
  State<ScreenUtilInit> createState() => _ScreenUtilInitState();
}

class _ScreenUtilInitState extends State<ScreenUtilInit>
    with WidgetsBindingObserver {
  final _canMarkedToBuild = HashSet<String>();
  MediaQueryData? _mediaQueryData;
  final _binding = WidgetsBinding.instance;
  final _screenSizeCompleter = Completer<void>();

  
  void initState() {
    if (widget.responsiveWidgets != null) {
      _canMarkedToBuild.addAll(widget.responsiveWidgets!);
    }
    _validateSize().then(_screenSizeCompleter.complete);

    super.initState();
    _binding.addObserver(this);
  }

  
  void didChangeMetrics() {
    super.didChangeMetrics();
    _revalidate();
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    _revalidate();
  }

  MediaQueryData? _newData() {
    MediaQueryData? mq = MediaQuery.maybeOf(context);
    if (mq == null) mq = MediaQueryData.fromView(View.of(context));

    return mq;
  }

  Future<void> _validateSize() async {
    if (widget.ensureScreenSize ?? false) return ScreenUtil.ensureScreenSize();
  }

  void _markNeedsBuildIfAllowed(Element el) {
    final widgetName = el.widget.runtimeType.toString();
    final allowed = widget is SU ||
        _canMarkedToBuild.contains(widgetName) ||
        !(widgetName.startsWith('_') || flutterWidgets.contains(widgetName));

    if (allowed) el.markNeedsBuild();
  }

  void _updateTree(Element el) {
    _markNeedsBuildIfAllowed(el);
    el.visitChildren(_updateTree);
  }

  void _revalidate([void Function()? callback]) {
    final oldData = _mediaQueryData;
    final newData = _newData();

    if (newData == null) return;

    if (oldData == null || widget.rebuildFactor(oldData, newData)) {
      setState(() {
        _mediaQueryData = newData;
        _updateTree(context as Element);
        callback?.call();
      });
    }
  }

  
  Widget build(BuildContext context) {
    final mq = _mediaQueryData;

    if (mq == null) return const SizedBox.shrink();

    return FutureBuilder<void>(
      future: _screenSizeCompleter.future,
      builder: (c, snapshot) {
        ScreenUtil.configure(
          data: mq,
          designSize: widget.designSize,
          splitScreenMode: widget.splitScreenMode,
          minTextAdapt: widget.minTextAdapt,
          fontSizeResolver: widget.fontSizeResolver,
        );

        if (snapshot.connectionState == ConnectionState.done) {
          return widget.builder?.call(context, widget.child) ?? widget.child!;
        }

        return const SizedBox.shrink();
      },
    );
  }

  
  void dispose() {
    _binding.removeObserver(this);
    super.dispose();
  }
}

こんな感じで書く

class ScreenUtilView extends StatelessWidget {
  const ScreenUtilView({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Screen Util Example'),
      ),
      body: ScreenUtilInit(
        designSize: const Size(360, 690),
        minTextAdapt: true,
        splitScreenMode: true,
        builder: (_, child) => // CenterとかColumn表示したいWidgetを書く...

🧷summary

flutter_screenutilは、異なる端末でも良い感じで画面サイズに対応したレイアウトを表示してくれるそうです。とはいえ、サイズの指定を考慮しないとOverflowはおきますよ笑

🤔どう違うのか?

同じ端末で実験してみるか。まずはノーマルなやつ。

Normal
import 'package:flutter/material.dart';

class NormalView extends StatelessWidget {
  const NormalView({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('通常のUI'),
      ),
      body: Center(
        child: Column(
          children: [
            Stack(
              children: [
                Container(
                  color: Colors.red,
                  width: 300,
                  height: 300,
                ),
                Container(
                  color: Colors.blue,
                  width: 200,
                  height: 200,
                ),
                Positioned(
                  left: 50,
                  top: 50,
                  child: Container(
                    color: Colors.green,
                    width: 100,
                    height: 100,
                  ),
                ),
              ],
            ),
            const Text(
              '16sp, will not change with the system.',
              style: TextStyle(
                color: Colors.black,
                fontSize: 16,
              ),
            ),
            const Text(
              '16sp,if data is not set in MediaQuery,my font size will change with the system.',
              style: TextStyle(
                color: Colors.black,
                fontSize: 16,
              ),
            ),
            const Text(
              'Nice design system, I will change with the system.',
              style: TextStyle(
                color: Colors.black,
                fontSize: 16,
              ),
            ),
            const SizedBox(height: 5),
            ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.blue[300],
                ),
                onPressed: () {},
                child: const Text(
                  "Click me",
                  style: TextStyle(fontSize: 16),
                )),
            const SizedBox(height: 5),
            ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green[300],
                ),
                onPressed: () {},
                child: const Text(
                  "Click me",
                  style: TextStyle(fontSize: 16),
                )),
            const SizedBox(height: 5),
            ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red[300],
                ),
                onPressed: () {},
                child: const Text(
                  "Click me",
                  style: TextStyle(fontSize: 16),
                )),
          ],
        ),
      ),
    );
  }
}

Pixcel_3a

iPhone15 Pro

iPhoneSE

flutter_screenutilを使ったもの

flutter_screenutil
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:screen_util_example/normal_view.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ScreenUtilView(),
      // home: const NormalView(),
    );
  }
}

class ScreenUtilView extends StatelessWidget {
  const ScreenUtilView({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Screen Util Example'),
      ),
      body: ScreenUtilInit(
        designSize: const Size(360, 690),
        minTextAdapt: true,
        splitScreenMode: true,
        builder: (_, child) => Center(
          child: Column(
            children: [
              Stack(
                children: [
                  Container(
                    color: Colors.red,
                    width: 300.w,
                    height: 300.h,
                  ),
                  Container(
                    color: Colors.blue,
                    width: 200.w,
                    height: 200.h,
                  ),
                  Positioned(
                    left: 50.w,
                    top: 50.h,
                    child: Container(
                      color: Colors.green,
                      width: 100.w,
                      height: 100.h,
                    ),
                  ),
                ],
              ),
              Text(
                '16sp, will not change with the system.',
                style: TextStyle(
                  color: Colors.black,
                  fontSize: 16.sp,
                ),
              ),
              Text(
                '16sp,if data is not set in MediaQuery,my font size will change with the system.',
                style: TextStyle(
                  color: Colors.black,
                  fontSize: 16.sp,
                ),
              ),
              Text(
                'Nice design system, I will change with the system.', 
                style: TextStyle(
                  color: Colors.black,
                  fontSize: 16.sp,
                ),
              ),
              SizedBox(height: 5.h),
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.blue[300],
                ),
                onPressed: () {

              }, child: Text("Click me", style: TextStyle(fontSize: 16.sp),)),
              SizedBox(height: 5.h),
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.green[300],
                ),
                onPressed: () {

              }, child: Text("Click me", style: TextStyle(fontSize: 16.sp),)),
              SizedBox(height: 5.h),
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red[300],
                ),
                onPressed: () {

              }, child: Text("Click me", style: TextStyle(fontSize: 16.sp),)),
            ],
          ),
        ),
      ),
    );
  }
}

Pixcel_3a

iPhone15 Pro

iPhoneSE


比較してみたけど、わかりづらいですよね😅
Stack Widgetで重ねている図形のサイズが違うぐらいしかぱっと見わからなかったです。iPadで試してみたら、UIに表示される状態が違ったので、参考になりそうです。

通常のUI

flutter_screenutil

🧑‍🎓thoughts

flutter_screenutilを使ってみた感想ですが、スマートフォンとタブレット複数の端末の画面サイズに対応するユースケースが求めれるAdaptive designでは効果を発揮しそうです。
最近開発してるアプリで導入するのをおすすめされたので、今回はキャッチアップのために学習してみました。
昔からあるみたいですけど、過去の記事を見ると、builderの()中に引数がなかったような???
第1引数と第2引数が必要みたいですね。

Discussion