RSS Archive About

Time Flies with Riverpod

Have you ever had that list item that shows relative time to the current moment, something like “5 minutes ago” or “two days ago”?

A relative timestamp showing "Sent 5 minutes ago"

It’s usually easy to implement: simply use an existing package like timeago that takes an event’s DateTime and current DateTime and returns a relative timestamp.

However, it’s also easy to make these relative timestamps outdated. Using DateTime.now() to get the current time is fine at the moment when DateTime.now() is called but it's not fine when DateTime.now() was last called 10 minutes ago and the timestamp still shows “just now”.

The provider

A colleague of mine introduced this small Riverpod provider to one of the projects years ago and I've been using it ever since:

/// Returns current time and updates it every minute.
final nowProvider = StateProvider<DateTime>((ref) {
  final timer = Timer.periodic(const Duration(seconds: 1), (_) {
    final now = DateTime.now();
    if (ref.controller.state.minute != now.minute) {
      ref.controller.state = now;
    }
  });

  ref.onDispose(timer.cancel);

  return DateTime.now();
});
Riverpod 3 code snippet
final nowProvider = NotifierProvider<NowNotifier, DateTime>(NowNotifier.new);

class NowNotifier extends Notifier<DateTime> {
  Timer? timer;

  @override
  DateTime build() {
    timer?.cancel();
    timer = Timer.periodic(const Duration(seconds: 1), (_) {
      final now = DateTime.now();
      if (state.minute != now.minute) {
        state = now;
      }
    });

    ref.onDispose(() => timer?.cancel());

    return DateTime.now();
  }
}

Watch provider using simple ref.watch:

@override
Widget build(BuildContext context, WidgetRef ref) {
  // This is current time, updated automatically
  final now = ref.watch(nowProvider);

nowProvider immediately returns a value upon subscription and starts a timer that runs a DateTime check every second.

Every time it runs, it checks if the minute value has changed from the last emitted value. It’s too much to update the UI every second, and this provider avoids that. Instead, it only emits a new value if a minute value changes (e.g. 16:05 changes to 16:06). It delivers an up-to-date timestamp every minute with a maximum delay of 1 second.

Tests

This provider can be overridden in widget tests allowing the time to be fixed and eliminating problems with timer disposal:

await tester.pumpWidget(
  UncontrolledProviderScope(
    container: ProviderContainer(
      overrides: [
        // Your other provider overrides here
        nowProvider.overrideWith((ref) => DateTime(2025, 10, 13)),
      ],
    ),
    child: const MyFancyWidget(),
  ),
);
Riverpod 3 code snippet

...
nowProvider.overrideWith(() => FakeNowNotifier(DateTime(2025, 10, 13))),
...

// Slightly more code for Riverpod 3 to create a FakeNowNotifier
class FakeNowNotifier extends NowNotifier {
  final DateTime dateTime;

  FakeNowNotifier(this.dateTime);

  @override
  DateTime build() => dateTime;
}

Not just relative timestamps

You can use this provider everywhere for centralized time access and get precise time control in widget tests for free.

Like any other provider, watching this provider makes UI reactive. If, for example, your app shows that a restaurant is open at the moment by comparing current time to restaurant's working hours, this provider will trigger a UI update for current status text and color:

final now = ref.watch(nowProvider);
final isRestaurantOpen = myRestaurant.isOpen(now);
final highlightColor = isRestaurantOpen ? Colors.green : Colors.red;
final statusText = isRestaurantOpen ? 'Open' : 'Closed';

List tile showing that restaurant is open for 45 more minutes

Automating app store graphics creation for Flutter apps

Creating app store graphics could take up a lot of your time and energy. Flutter widget tests can help you to generate these images. It might not be a typical way to use widget tests but if it works it’s not stupid, right?

Tree app store screenshot examples taken with the provided solution

Why do it with widget tests?

  • To save time. You don't have to manually click through the app to reach the right screen and get it to the right state. Perfect screenshots can be taken every time for any app state needed. Automation helps saving and respecting other people’s time: designers don't have to spend hours editing hundreds of screenshots just to add a device frame and localized text.

  • Flexibility. It works with any configuration: different screen sizes, locales, dark\light themes, text directionalities. Let computer do the boring work of setting configuration and taking screenshots.

  • Works for any configuration. No need to own various devices, no need to launch gazillions of emulators to make a screenshot of the right size. All screenshots can be taken on one platform.

  • Automation. You can create new screenshots after every new release, without having to manually repeat the process. This ensures that your screenshots are always up-to-date.

What do I do to make it work?

There are several steps to make it work. I’m going to use my simple coffee ratio calculator app called Ratio M8 as an example how to create graphics for Apple's App Store. Code samples in this article are simplified for better readability. App is not open source but screenshot generator's code is available on Github.

Step 1. Make app testable

This step depends highly on the way your app is built. You need to make your app testable, make it possible to show the app state for which you want to take a screenshot.

In my case it was easy because the app is very simple and uses Riverpod that allows overriding providers like this:

// Return custom state from shared preferences
// Ratio is 1:16.5, Beans: 24 grams, the rest is calculated
final model = MockPreferencesModel(16.5, 24);

await tester.pumpWidget(
    ProviderScope(
        overrides: [
            preferenceModelProvider.overrideWithValue(model),
        ],
        child: const App(),
    ),
)

Sometimes it’s enough to simply pump a widget you want to take a screenshot of. Sometimes you need to have a widget test that goes through the app and clicks various items. And sometimes you need to take care of your platform-specific dependencies.

Step 2. Show actual texts

As you probably know, Flutter widget tests use a different font from what you use in the app. If you create a screenshot from what’s on the screen without this step it would look like this:

App screenshot with all letters replaced by black rectangles

The trick is to make the app in your tests look nicer by loading actual fonts before running tests:

final fira = rootBundle.load('assets/fonts/fira_code_regular.ttf');
final loader = FontLoader('Fira')..addFont(fira);
await loader.load();

Text widgets that use this font family will now be rendered correctly.

Optional: add device frame and add text

Before taking a screenshot, it’s possible to wrap app into a device frame, add text or other content and apply effects and transformations.

In this example I simply wrap the app widget into an iPhone 13 frame using device_frame package and add a caption:

ColoredBox(
    color: Colors.white,
    child: Column(
        children: const [
            Text('This is some caption'),
            DeviceFrame(
                device: Devices.ios.iPhone13,
                screen: App(),
            ),
        ],
    ),
),

Step 3. Take a screenshot

Last touch before taking a screenshot is to add RepaintBoundary widget around the test app:

RepaintBoundary(
    key: rootKey,
    child: screenshotContent,
),

Finally a screenshot can be taken and saved to a file:

final boundary = tester.renderObject(find.byKey(rootKey));
final image = await boundary.toImage();

final file = File('my_image.png');
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
await file.writeAsBytes(byteData!.buffer.asUint8List());

Run the test and voilà! Screenshot including device frame and everything inside RepaintBoundary is saved as my_image.png.

Notes

  • Navigation bar, status bar and other platform UI elements are not captured and have to be added as an image on top of the app screenshot if needed. This could be a part of the screenshot generation code.

  • This solution most likely doesn't work with Flutter’s platform views.

  • As any other test and automation, some initial work has to be done to make it work.

  • Similarly to other tests this code needs to be maintained.

Conclusion

It may seem easier to use a real device or emulator to create screenshots rather than spending hours writing code. It might be true if you take screenshots once when app is initially released. In a long run with constantly updating configurations and app changes, it’s worth investing into automation like this.

App store images don’t have to be frame-and-text-boring as shown in the example above. You can create more engaging images. For example, a screenshot embedded into an isometric device frame using Flutter’s Transform widget:

Example of a fancier screenshot: app screenshot is embedded into an isometric device frame

Flutter app with a touch of Material You colors, please

Android 12 brings the third iteration of Material Design called Material You - along with other changes apps and home screen widgets may now change their look according to wallpaper colors or selected system palette.

There are no design guidelines for using these new colors although Google is updating their apps - Gmail, Keep, Phone, Calendar and others have already got their Material You makeover. It feels a bit like a secret - there are no guidelines but apps are getting updated. Unfortunately, Flutter has no support for the new coloring system yet.

As a developer I want to support and utilize the latest available specifications and APIs. Let's get the new colors in Flutter.

Getting colors using a platform channel

The idea is simple - call a "native" platform code to get a list of colors. The colors we're interested in are listed in Dmitry Chertenko's article and in the official documentation. There are several new system color attributes available:

system_accent1_0
system_accent1_10
system_accent1_50
...

There are three groups of accent colors and two groups of neutral colors:

Each color group has 13 shades and looks like a gradient - it starts with a lighter color and ends with a darker one. Switching between light and dark system modes does not change the palette's colors.

Note: make sure to set targetSdk to 31 in app's build.gradle file. Otherwise the new R.color.* attributes are not accessible.

Making a call via platform channel is trivial so here's the code.

The map of colors in Flutter's MaterialColor is slightly different from what Android offers - Flutter expects to have a primary color and its 10 shades, Android provides 13 shades. I simply ignored some of the shades on the Flutter side although it doesn't feel right - some of the ignored colors could be useful (see palette usage below).

Guessing Material You colors usage

As there are no guidelines on how to use the colors. I've used a modern nanotech tool (a simple color picker) to find matches between my current Material You palette and real apps. This may help in understanding how colors should be used.

Calculator

Calculator uses colors from all three accent color groups. I couldn't find a color matching digit button's background - perhaps it's a color with opacity and it blends in with window background which makes it's hard to get the actual color.

Gmail

Search and navigation bars use changed version of palette's colors. My guess is they use a transparent color and a solid color background container (white for light theme and black for dark theme) to make the final color opaque. Let's see what Material You guidelines say about it once they're published.

Gboard

Pretty much every color is from the palette:

There's no clear logic which color to use in each case. Looks like the way to achieve colorful Material You looking apps is to use shades 100-300 of accent colors for main controls and lighter\darker colors for the rest of the items.

Using Material You colors in Flutter

Using colors in Flutter is simple: get a color and set it as a primary swatch:

FutureBuilder(
  future: getMaterialYouColor(),
  builder: (context, AsyncSnapshot<MaterialYouPalette?> snapshot) {
    final primarySwatch = snapshot.data?.accent1 ?? Colors.blue;
	return MaterialApp(
	  theme: ThemeData(
	    primarySwatch: primarySwatch,
...

The biggest challenge comes with widgets customization. Setting a primary swatch is simple but overriding themes for all needed widgets could be hard - it's a lot to do, to test and to maintain. And then there's dark theme. And fallback theme for other platforms where Material You colors are not supported. Let's hope Flutter will have a better and simpler solution for that.

In this article I'm looking for a way to bring Material You colors to Flutter. Can it be done? Yes. Is using these colors to style widgets a pleasant experience? Doesn't feel so.