I'm using an M1 MacBook Air for my stuff. It's an old but still good and capable machine that has been working flawlessly for years. There's one tiny thing: I made the mistake of buying the 256 GB version.
I'm constantly running out of space because of build files, iOS and Android caches and other temporary things. This issue happens so often that DaisyDisk lives in my Dock rent-free.
Telegram is happy to tell me that I'm running out of space every time I open it:
I think it starts noticing the space issue when there's less than 4 GB available… but it's none of its business! It's none of a
messenger app's business! I didn't ask it to check my disk space. I didn't ask it to interrupt what I was doing or about to do.
It's clearly not the most important app on my computer, why does it check how much space is left on my device and tell me about it all the time?
What if every other app that needs to cache media started showing pop-ups like that? It's a feature that nobody asked for and it only annoys the user when space issues happen on a constant basis.
Have you ever had that list item that shows relative time to the current moment, something like “5 minutes ago” or “two days 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(constDuration(seconds:1),(_){final now =DateTime.now();if(ref.controller.state.minute != now.minute){
ref.controller.state = now;}});
ref.onDispose(timer.cancel);returnDateTime.now();});
Riverpod 3 code snippet
final nowProvider =NotifierProvider<NowNotifier, DateTime>(NowNotifier.new);classNowNotifierextendsNotifier<DateTime>{Timer? timer;@overrideDateTimebuild(){
timer?.cancel();
timer =Timer.periodic(constDuration(seconds:1),(_){final now =DateTime.now();if(state.minute != now.minute){
state = now;}});
ref.onDispose(()=> timer?.cancel());returnDateTime.now();}}
Watch provider using simple ref.watch:
@overrideWidgetbuild(BuildContext context,WidgetRef ref){// This is current time, updated automaticallyfinal 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:constMyFancyWidget(),),);
Riverpod 3 code snippet
...
nowProvider.overrideWith(()=>FakeNowNotifier(DateTime(2025,10,13))),...// Slightly more code for Riverpod 3 to create a FakeNowNotifierclassFakeNowNotifierextendsNowNotifier{finalDateTime dateTime;FakeNowNotifier(this.dateTime);@overrideDateTimebuild()=> 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';
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?
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 calculatedfinal model =MockPreferencesModel(16.5,24);await tester.pumpWidget(ProviderScope(
overrides:[
preferenceModelProvider.overrideWithValue(model),],
child:constApp(),),)
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:
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:
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: