1

I have a convenience StatelessWidget that returns the appropriate widget for one of three display size breakpoints:

/// Return the most appropriate widget for the current display size.
/// 
/// If a widget for current display size is not available, chose the closest smaller variant.
/// A [mobile] size widget is required.
class SizeAppropriate extends StatelessWidget {
  // ignore: prefer_const_constructors_in_immutables
  SizeAppropriate(
    this.context,
    {
      required this.mobile,
      this.tablet,
      this.desktop,

      Key? key
    }
  ) : super(key: key);

  final BuildContext context;
  late final Widget mobile;
  late final Widget? tablet;
  late final Widget? desktop;

  @override
  Widget build(BuildContext context) {
    switch (getDisplaySize(context)) {
      case DisplaySize.mobile:
        return mobile;
      case DisplaySize.tablet:
        return tablet ?? mobile;
      case DisplaySize.desktop:
        return desktop ?? tablet ?? mobile;
    }
  }
}

I then use it like this:

SizeAppropriate(
    context,
    mobile: const Text('mobile'),
    desktop: const Text('desktop'),
)

Is the keyword late working here as intended, building only the correct variant, or am I hogging the performance, because all variants are constructed (or am I even creating an anti-pattern)?

Should I use builder functions instead?

Edit:

This answer makes me think I'm correct. Although the last two sentences make me think I'm not correct.

When you do this, the initializer becomes lazy. Instead of running it as soon as the instance is constructed, it is deferred and run lazily the first time the field is accessed. In other words, it works exactly like an initializer on a top-level variable or static field. This can be handy when the initialization expression is costly and may not be needed.

When I do log('mobile') and log('desktop') in the respective widgets being constructed, I only get one type of message in the console.

Edit 2 – minimum working example:

This is my main.dart in a newly generated project (VS Code command >Flutter: New Project – Application). It might be worth noting, that I am testing this on Linux.

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: SizeAppropriate(
          context,
          mobile: const Mobile(),
          tablet: const Tablet(),
          desktop: const Desktop(),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    log('mobile');
    return const Text('mobile');
  }
}

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

  @override
  Widget build(BuildContext context) {
    log('tablet');
    return const Text('tablet');
  }
}

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

  @override
  Widget build(BuildContext context) {
    log('desktop');
    return const Text('desktop');
  }
}

enum DisplaySize {
  desktop,
  tablet,
  mobile,
}

DisplaySize getDisplaySize(BuildContext context) {
  Size size = MediaQuery.of(context).size;
  if (size.width < 768) {
    return DisplaySize.mobile;
  }
  else if (size.width < 1200) {
    return DisplaySize.tablet;
  }
  else {
    return DisplaySize.desktop;
  }
}

class SizeAppropriate extends StatelessWidget {
  // ignore: prefer_const_constructors_in_immutables
  SizeAppropriate(
    this.context,
    {
      required this.mobile,
      this.tablet,
      this.desktop,

      Key? key
    }
  ) : super(key: key);

  final BuildContext context;
  late final Widget mobile;
  late final Widget? tablet;
  late final Widget? desktop;

  @override
  Widget build(BuildContext context) {
    switch (getDisplaySize(context)) {
      case DisplaySize.mobile:
        return mobile;
      case DisplaySize.tablet:
        return tablet ?? mobile;
      case DisplaySize.desktop:
        return desktop ?? tablet ?? mobile;
    }
  }
}
IsawU
  • 430
  • 3
  • 12
  • You cannot check if a `late` variable has been initialized. If you want `desktop ?? tablet ?? mobile` to work, just make `desktop` and `tablet` nullable and *not* `late`; they will be `null` by default. – jamesdlin Jul 12 '22 at 16:45
  • The `SizeAppropriate` widget in the code above correctly presents different size variants as window is resized, so checking must definitely work at runtime. – IsawU Jul 13 '22 at 08:02
  • It presents the correct widget variant regardless of which variants are or are not supplied in the constructor. There must be something else at play. – IsawU Jul 13 '22 at 19:58
  • My mistake, I misread your code. Your `SizeAppropriate` constructor does always initialize the `tablet` and `desktop` members, but since it does, marking those members as `late` will have no effect. – jamesdlin Jul 13 '22 at 20:06

1 Answers1

1

late does not do what you want. It's only for the null-safety feature and when you do or don't get warnings about it. Those two texts get built every time regardless of environment, because they need to be there when they are passed to your widget.

If you to only build the appropriate widgets for each size when the size is known, you indeed need indeed pass two builders, one of which you call if appropriate.


For two constant Text widgets, that would be too much overhead, but I am assuming you want "heavier" widget trees for both options in the end.

nvoigt
  • 75,013
  • 26
  • 93
  • 142
  • I just remembered a test I did a couple of weeks ago. I logged `log('mobile')` and `log('desktop')` in the respective widgets being constructed, and only one type of message is shown in the debug console. Is there an explanation for this? – IsawU Jul 12 '22 at 11:53
  • Where did you put them? Can you post an [mcve]? – nvoigt Jul 12 '22 at 12:37
  • I added a minimal example. – IsawU Jul 12 '22 at 13:18
  • 1
    Yes, their `build` method will not be called because they are not in the widget tree, but if you put the log into the constructor of each, you will see they will be constructed. – nvoigt Jul 12 '22 at 13:22