24 Sep 201810 min read

Flutter — my thoughts and impressions — PART V [FINAL]

TL;DR: If you are not interested in the technical stuff how I implemented a few things just jump to the last part where I wrote the cons and pros of Flutter in my opinion. Also if you want to see how it works on the real device you can find there a link to the app (Google Play/Android) as well.

Redesign 🖌

The time for a redesign of the application has come. I have designed it in Gravit Designer and since I am not really experienced with UI/UX it is still, not the best app design you will ever see 🌝

Below you can see the final design of the app (for now at least). It’s not that great but I think it’s good enough to finish my series which were purely supposed to give me a content for learning Google’s Flutter.

Watermaniac screenshots

The base of the app is made of a Stack that contains the top image and a container that contains main content of the page. Refactoring the app to use iOS’s style widgets was the simplest thing ever — just replaced Material widgets with Cupertino ones, adjusted parameters a little bit and pretty much all worked. I really love that in Flutter — juggling views (widgets) is really easy and do not have to worry about setting up constraints (referring to iOS) each time I move something in the view’s hierarchy. I really consider writing an layout library in Swift for iOS that will work similarly to Flutter’s widgets — or maybe is there already one?

The History, Notifications, and Settings views are quite simple. Created a simple widget which is a Container with a shadow and contains a child shown inside it.

class ContainerWrapper extends StatelessWidget {
  final Widget child;
  final double widthScale;

  ContainerWrapper({@required this.child, this.widthScale = 0.8});

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    return SizedBox(
      width: size.width * widthScale,
      child: Container(
        color: Colors.transparent,
        child: Container(
          decoration: BoxDecoration(
              color: const Color(0xe6ffffff),
              boxShadow: [
                BoxShadow(color: const Color(0x28000000), blurRadius: 5.0)
              ],
              borderRadius: BorderRadius.all(Radius.circular(16.0))),
          child: child,
        ),
      ),
    );
  }
}

Nothing really to explain here, it is straightforward 👌. This widget is used as a parent for most widgets you can see on the screenshots.

Let’s focus on the Home screen though since it is more interesting. You can notice three main sectors here — a short history on the drop, drink progress in the middle and menu on the bottom of the screen.

The short history widget is a simple one — it is just a text wrapped by ShadowText. Each time a drink is added it redraws itself, reads history entries from the database and shows the last 3 ones.

Drink Menu

The menu used for adding a drink to the daily progress consists a main (toggle) button that after tapping shows options for water amount to add — those buttons are just objects of FloatingActionButton widget. The toggle button is a GestureDedector with an image as a child since the icon was created in Gravit Designer.

Tapping on the toggle button calls the “toggle menu action” that shows or hides the extra buttons (water amounts). To make it look smoother I added a toggle animation as well.

@override
  initState() {
    final Curve curve = Curves.easeOut;

    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 500));
    _translateButton = Tween<double>(
      begin: _fabHeight,
      end: -8.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Interval(
        0.0,
        0.75,
        curve: curve,
      ),
    ));
    super.initState();
  }

toggleMenu() {
    if (!isOpened) {
      _animationController.forward();
    } else {
      _animationController.reverse();
    }

    setState(() {
      isOpened = !isOpened;
    });
  }

It creates an animation controller with a duration of half a second and adds Tween animation which value is used to determine the position of those amount buttons.

Also added little bubbles around the toggle button that moves slowly from the center to the outside and disappears. It is done using CustomPainter and AnimationController.

class Bubbles extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _BubblesState();
  }
}

class _BubblesState extends State<Bubbles> with TickerProviderStateMixin {
  AnimationController animationController;
  final bubbles = <Bubble>[];

  @override
  void initState() {
    super.initState();

    List.generate(5, (i) {
      bubbles.add(Bubble());
    });

    animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
          ..addListener(() {
            for (int i = 0; i < bubbles.length; i++) {
              bubbles[i].move();

              if (bubbles[i].remainingLife < 0 || bubbles[i].radius < 0) {
                bubbles[i] = Bubble();
              }
            }
          })
          ..repeat();
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
            animation: animationController,
            builder: (context, child) => CustomPaint(
                  size: Size(64.0, 64.0),
                  painter: _BubblePainter(bubbles),
                ),
          );
  }
}

class _BubblePainter extends CustomPainter {
  final List<Bubble> bubbles;

  _BubblePainter(this.bubbles);

  @override
  void paint(Canvas canvas, Size size) {
    for (var bubble in bubbles) {
      bubble.display(canvas);
    }
  }

  @override
  bool shouldRepaint(_BubblePainter oldDelegate) => true;
}

It generates 5 bubbles (bubble is just a circle with a gradient, it also has a property of its life and radius that is random). Each time the listener is called it reduces bubble’s life and changes its location on the screen.

This is pretty much that about the menu, really simple one. Might improve it in the future though. If anyone would like a tutorial on how I have done it exactly I can write that but there is already plenty of (better) examples out there 🔥

{ad_banner}

Water Drop

A wave effect on the drop is created by stacking two drops together and adding a clipper with a wave path to the one that is on top.

Stack(
  alignment: Alignment.center,
  children: <Widget>[
    Center(child: Image.asset('assets/images/drop.png')),
    Center(
      child: AnimatedBuilder(
        animation: CurvedAnimation(
        parent: animationController,
        curve: Curves.easeInOut),
        builder: (context, child) => ClipPath(
          child: Image.asset('assets/images/drop-blue.png'),
          clipper: WaveClipper(target > 0 ? current / target * 100 : 100.0,
                    animationController.value),
          ),
        ),
    ),
    Center(
      child: Column(
        children: <Widget>[
          ShadowText(
            '${(target > 0 ? current / target * 100 : 100).toStringAsFixed(0)}%',
              shadowColor: Colors.black.withOpacity(0.15),
              offsetX: 3.0,
              offsetY: 3.0,
              blur: 3.0,
              style: TextStyle(
                color: const Color(0x7F4C9BFB),
                fontSize: 40.0,
                fontWeight: FontWeight.bold),
              ),
          ShadowText(
            '$current ml',
              shadowColor: Colors.black.withOpacity(0.15),
              offsetX: 3.0,
              offsetY: 3.0,
              blur: 3.0,
              style: TextStyle(
                color: const Color(0x4B4C9BFB),
                fontSize: 18.0,
                fontWeight: FontWeight.bold),
              )
        ],
      ),
    ),
  ],
)

And the wave animation is — you are right — once again made with AnimationController. As the time of animation passes, I generate clip path with a help of sin function from math library with an x offset that depends on the animation’s value (time).

class WaveClipper extends CustomClipper<Path> {
  final double percentage;
  final double animation;

  WaveClipper(this.percentage, this.animation);

  @override
  Path getClip(Size size) {
    var progress = (percentage > 100.0 ? 100.0 : percentage) / 100.0;
    progress = 1.0 - progress;
    final double wavesHeight = size.height * 0.1;

    var path = new Path();

    if (progress == 1.0) {
      return path;
    } else if (progress == 0.0) {
      path.lineTo(0.0, size.height);
      path.lineTo(size.width, size.height);
      path.lineTo(size.width, 0.0);
      path.lineTo(0.0, 0.0);
      return path;
    }

    List<Offset> wavePoints = [];
    for (int i = -2; i <= size.width.toInt() + 2; i++) {
      var extraHeight = wavesHeight * 0.5;
      extraHeight *= i / (size.width / 2 - size.width);
      var dx = i.toDouble();
      var dy = sin((animation * 360 - i) % 360 * Vector.degrees2Radians) * 5 +
          progress * size.height -
          extraHeight;
      if (!dx.isNaN && !dy.isNaN) {
        wavePoints.add(Offset(dx, dy));
      }
    }

    path.addPolygon(wavePoints, false);

    // finish the line
    path.lineTo(size.width, size.height);
    path.lineTo(0.0, size.height);

    return path;
  }

  @override
  bool shouldReclip(WaveClipper old) =>
      percentage != old.percentage || animation != old.animation;
}

Pros and cons

Since it is the last article of the series, it is a perfect time to sum up the pros and cons of working in Flutter (in my eyes).

Pros

Cons

Overall I think it is worth giving Flutter and Dart a try. I had a lot of fun and it has been a great experience. As an iOS Developer, I have learned a lot about Android and I think it will be easier for me to create an Android app in Kotlin as well — might actually give it a try in my next series.

{ad_banner}

Final words

I am still not sure about the performance of the animations on the Home page — I hope they are not too much for a mobile’s CPU. If there will be any issues with that I will try to improve that. Also, history’s performance is not that great either since I think it rebuilds too much while using Redux architecture so have to improve that one as well.

Anyway, the application is available on the Google Play right now.

I did not get my personal Apple Developer Account yet so have not uploaded it there, will do that once I find out if it is worth spending $100 to release this app to the AppStore — will not do that if there is no one interested into using that 😉

If you got an Android device feel free to give it a try and let me know if you like it or not and what can I improve.

Also if you would like to know how I implemented certain widget in the app leave a comment and if enough people are interested I will write a tutorial about that or maybe even… open source it 😈