Flutter Tidbits from 1Block

4 thoughts/tips from developing a small to medium-sized Flutter app

22-05-2019 - 5 minutes, 2 seconds -

Flutter is a cross-platform UI framework from Google, but you already knew that if you're reading this post. It's such fun to use, I use it for my Android-only apps! (Which use Android-specific features. Sorry iPhone users...)

1Block was recently released, a much larger app in terms of UI than our first app, Minimal Maps. I thought I'd share a few of my favourite Flutter related miscellany in this post.

AnimatedSize - The most useful Flutter widget?

If you want to make your app look and feel great with minimal effort, look no further than AnimatedSize. Check this out:

Simple to use, it's great for looks and user experience, guiding the user's eye to changes in the UI state (this is an important function of animations). Just put this in your build function:

AnimatedSize(
  //make sure to use different keys
  child: someCondition
    ? MyWidget(key: Key("A"))
    : MyOtherWidget(key: Key("B")),
  duration: Duration(milliseconds: 200),
  vsync: this,
)

Easy! Well... except for the vsync: this part. You probably aren't using StatefulWidgets if you're using a state management solution that tries to keep everything as StatelessWidgets, which means you can't add the required SingleTickerProviderStateMixin.

To keep my code clean, I created a tiny widget to provide the ticker, with the original name, EasyAnimatedSize:

import 'package:flutter/widgets.dart';

class EasyAnimatedSize extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final AlignmentGeometry alignment;
  final Curve curve;
  EasyAnimatedSize({
    this.child,
    this.duration = Duration(milliseconds: 200),
    this.alignment = Alignment.center,
    this.curve = Curves.ease,
  });
  @override
  _EasyAnimatedSizeState createState() => _EasyAnimatedSizeState();
}

class _EasyAnimatedSizeState extends State<EasyAnimatedSize> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return AnimatedSize(
      vsync: this,
      duration: widget.duration,
      child: widget.child,
      curve: widget.curve,
      alignment: widget.alignment,
    );
  }
}

Now use it as so:

EasyAnimatedSize(
  child: someCondition
    ? MyWidget(key: Key("A"))
    : MyOtherWidget(key: Key("B")),
)

Much better.

palette_generator - pull colours from images

A common criticism of Flutter (compared to older frameworks such as React Native) is that the ecosystem is not so mature, particularly for plugins and packages. While this may be largely true, you might be surprised with what you can find. I certainly was when I found palette_generator, which is maintained by Google! It generates a colour palette based on image data. That meant I could make the slices of my usage pie chart coloured according to the app icons.

Pie chart with slices matching app colours

It's very simple to use, just pass in a AssetImage, MemoryImage or NetworkImage and you'll get a Future, the result from which you can access different colours.

PaletteGenerator palette = await PaletteGenerator.fromImage(
  NetworkImage('https://cdn-images-1.medium.com/max/1200/1*5-aoK8IBmXve5whBQM90GA.png')
);
// note: this is just an example - you should cache the PaletteGenerator somehow
setState(() {
  //make sure to have a fallback, since darkVibrantColor can be null (but not dominantColor)
  _myWidgetColor = palette.darkVibrantColor ?? palette.dominantColor;
});

Exponential Slider

When you want to set a 'Quick Block' in 1Block, you are presented with this slider:

If you look carefully, you'll see that the max value is 12:00, but the halfway point is around 4:00. In the end, it probably wasn't necessary here, but it can be useful to have an exponential scale like this for more fine control at the low end, while still allowing for a larger max value.

To implement this, you can use this code:

//don't forget this with your imports
import 'dart:math'; 

//we'll figure this out below. This value gives a range of 0-720 minutes
const exponentMultiplier = 419.0; 

//in your build function
return Slider(
  value: log((myValue + exponentMultiplier)/exponentMultiplier),
  onChanged: (newValue) =>
    setState(() => myValue = exponantMultiplier*pow(e, newValue) - exponentMultiplier),
);

Huh? Time for an ultra-condensed maths lesson. Skip the next paragraph if you just want to calculate your own exponentMultiplier.

If y is the value that our slider shows, and x is what Flutter thinks it's showing (if it were a normal slider), we want to use y = e^x to get some nice exponential scaling. But that's not very flexible - the minimum value at x=0 is always 1 and we can't control how much it scales. So, we add in two constants, A and B, like this: y = A*e^x - B. Now we just need to figure out what their values are. To do this, we come up with two equations, one for our desired minimum value at x=0, and one for our desired maximum value at x=1.

So if I want my slider to go from 0 to 720 (12 hours * 60 minutes), my two equations are 0 = A*e^(0) - B and 720 = A*e^(1) - B. Now just plug that in to WolframAlpha, click approximate value and you'll get the solution. Both are equal to 419.02! They'll always be the same if your minimum value is at x=0 and your maximum is at x=1. Use this as the exponentMultiplier in the above code.

If you're clever, you can do all kinds of scaling this way. In a future 1Block update, I'll probably change the equation to use 10^x instead of e^x, for a bigger effect.

Onboarding with an overlay

I'm not a UX designer. I tried to make the 1Block interface as intuitive as possible, but when I showed some friends how to create a block profile, they found it a bit confusing - too much information at once. I needed a simple way to guide the user through the first time around, and came up with onboard_overlay:

I'll release it on pub when I get around to adding some tests, but it's possible to use now from the Github repo.

Keep in mind it's not always an ideal solution, nor relevant for every app. Even in 1Block, I think I have a few too many steps, and I'll likely streamline it in a future update.

That's it!

Leave a comment if you'd like to hear any other experiences, questions, etc! Next time: The stupid (and not so stupid) mistakes I made developing 1Block!