Flutter Web API Request Local Cache

I have already made an article about how to develop a local caching for Firestore. Now it’s time to make the same thing, but for everything else. So I am going to show you how you can create local caching for your API calls.

The problem

You can easily find yourself in my position, where I created a web app, but a web app is really different than a mobile app. Mobile apps cannot be resized like you could resize a browser. When you resize the browser – and did not implement a proper state management- every API call that is on the page, will be reran.

As you can see resizing is flooding the network with API calls. The good thing is, you can get rid of these requests pretty easily. First you have to recognize that there’s a problem, but trust me you will see it, it laggs like crazy when you are resizing. Second, the solution is obvious. Just don’t let those request get out to the network. Third is implementation.

Should you cache?

Or rather when should you cache? Simple, you should cache everytime, when the API you are requesting is not a POST, modify something and gives you back some data that you would use. The reason is, if you cache that, you won’t be able to see the modified data, because it will be your previous cached data, that is outdated at this point. But we will implement a way around it.

Implementation

You could implement a local cache by modifying the flutter_service_worker.js, but that is a bit of a hassle. That file is only available once you have built your project, and everytime you build it, it gets overwritten. Secondly it is not Flutter, but js.

I have chosen the approach of creating an interceptor within Flutter. An interceptor is some kind of gateway, where your network packages will be monitored. You can find and create interceptors in the Dio package, but other packages also have similar approaches, like Chopper, which also has them.

You can monitor out going requests, incoming responses and also incoming errors as well. For the local cache, we are just going to need the onRequest and onResponse methods.

import 'package:DOChallenge/core/network/entities/cache_response.dart';
import 'package:dio/dio.dart';

const DEFAULT_CACHE_DURATION 5;

class LocalCacheInterceptor implements Interceptor {
  final int cacheDuration;
  static Map<StringCacheResponse> cache = Map<StringCacheResponse>();

  LocalCacheInterceptor({this.cacheDuration = DEFAULT_CACHE_DURATION});

  @override
  Future onError(DioError err) {
    return Future.value(err);
  }

  @override
  Future onRequest(RequestOptions options) async {
    String key = options.path;
    
    if (options.extra["isForced"] ?? false) {
      return Future.value(options);
    }

    if (cache.containsKey(key)) {
      if (Duration(seconds: cacheDuration) > DateTime.now().difference(cache[key].timestamp)) {
        return Future.value((cache[key].response as Response));
      }
    }

    return Future.value(options);
  }

  @override
  Future onResponse(Response response) {
    
    final key = response.request.path;
    if (cache.containsKey(key)) {
      if (Duration(seconds: cacheDuration) < DateTime.now().difference(cache[key].timestamp)) {
        cache[key] = CacheResponse(timestamp: DateTime.now(), response: response);
      }
    } else {
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: response);
    }
    return Future.value(cache[key].response);
  }
}

We are saving the response in a Map<String, CacheResponse> object, where the key is the path of the request, and the value is the response plus a timestamp.

class CacheResponse {
  final DateTime timestamp;
  final dynamic response;

  CacheResponse({this.response, this.timestamp});
}

And we are checking in the onRequest function, whether the map contains a response, and the ellapsed time is smaller than the cache duration.

Now for the POST problem I have described, you can use the extra option, and you can set the isForced to always get the data from the web, and not from your cache.

Example

You don’t need anything other than your API call, this example was created using the Dio package. But you can use it everywhere, you just need the create the interceptor.

Future<ChallengegetChallenge(int challengeId) async {
  Response response = await db.server.get('/doc/pub/challenge' + challengeId.toString());
  return Challenge.fromJson(response.data);
}

Results

Conclusion

We have created a way of improving our wb application, with local cahcing API requests. You could modify the flutter_service_worker.js file to implement caching, but I think the previous method is way simpler, and more usable. So feel free the copy the code and make stuff with it.

Advertisement

Firestore Local Cache with Flutter Web

Caching is really important when it comes to the web. It is also possible to implement your own local cache for Firestore, with Flutter Web.

Why should you use caching

Caching is especially important for Flutter Web, because of the rerendering of the screen when setting state. When you use setState, everything in the build will run again. There are some state management packages, that will not rerun, and saves the current state, but most of them will do. Everytime it is rerun, your Firestorecalls will run again.

Not to mention, whenever the screen is resized, the rerendering will occur. When this happens, the network will be flooded with unnecessary Firestore calls.

Firebase is accumulating every call you are sending out, and will bill you at the end of the month. If you exceed the free limits, you can get an unexpected amount. When more people use your application, the more the bill will be. So you should take it into consideration, and use the local cache method I am going to show you.

When should you use caching?

The way I am going to show you is only applicable for Flutter Web and also it makes more sense to do than on mobile. In short, you should cache your data everytime you get the chance.

There are some Firestore methods that you can use caching on:

  1. document()
  2. collection()
  3. collection().where()

Getting a document, getting a list of documents, and querying a list of document.

Implementing local cache for Firestore

Every method is following the same principles and stays at the same class, which is LocalCache.

import 'package:firebase/firestore.dart' as fb;
import 'package:firebase/firebase.dart' as db;
import 'package:localcache/cache_response.dart';

const DEFAULT_CACHE_DURATION = 25;

class LocalCache {
  final int cacheDuration;
  static Map<String, CacheResponse> cache = Map<String, CacheResponse>();

  LocalCache({
    this.cacheDuration = DEFAULT_CACHE_DURATION,
  });
}

We have a cacheDuration property and we can change it from the default 25 seconds. That property indicates for how long we are keeping the data. 25 seconds seems too short, but actually, it gets rid of the resizing issue, which really is the biggest problem.

Also we have the cache property, which is a map of request path and responses. The idea is simple. We are storing the path as a key, and the cacheResponse as the value, which looks like this:

class CacheResponse {
  final DateTime timestamp;
  final dynamic response;

  CacheResponse({this.response, this.timestamp});
}

Simple entity class, which contains a timestamp of the last network call and a dynamic response. We are storing the response in that dynamic variable.

Lets start with the easyiest, allthough all the functions are the same with just minor changes.

1 document()

Future<fb.DocumentSnapshotdocumentCacheWrap(String path, {String additionalInfo, bool isForced}) async {
  String key = path + (additionalInfo ?? "");

  if (isForced ?? false) {
    final res = await db.firestore().doc(key).get();
    cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
    return Future.value(cache[key].response);
  }

  if (cache.containsKey(key)) {
    if (Duration(seconds: cacheDuration) > DateTime.now().difference(cache[key].timestamp)) {
      return Future.value(cache[key].response);
    } else {
      final res = await db.firestore().doc(key).get();
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
      return Future.value(cache[key].response);
    }
  } else {
    final res = await db.firestore().doc(key).get();
    cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
    return Future.value(cache[key].response);
  }
}

The documentCacheWrap function has a path parameter and 2 optional parameters, which are the additionalInfo and the isForced. Path is the regular Firestore path that you would give to the firestore document.

With the additionalInfo String you are able to differentiate some similar requests, and isForced is the bool value for when you don’t want to use the cached version of the data, you need the data straight from the web.

The path and additionalInfo is concatenated to form the key, for the cache map. This is a unique identification for your request, then we check every condition like, is the isForced is set, or the cache map already contains the data, if it does we are checking the cacheDuration and if the duration expired, we are getting the value from the web. Same thing when we do not have the key in the map yet.

We are returning a DocumentSnapshot, that we can use the same way we would use like with the default Firestore method.

2 collection()

Future<fb.QuerySnapshotcollectionCacheWrap(String path, {String additionalInfo, bool isForced}) async {
  String key = path + (additionalInfo ?? "");

  if (isForced ?? false) {
    final res = await db.firestore().collection(key).get();
    cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
    return Future.value(cache[key].response);
  }

  if (cache.containsKey(key)) {
    if (Duration(seconds: cacheDuration) > DateTime.now().difference(cache[key].timestamp)) {
      return Future.value(cache[key].response);
    } else {
      final res = await db.firestore().collection(key).get();
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
      return Future.value(cache[key].response);
    }
  } else {
    final res = await db.firestore().collection(key).get();
    cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
    return Future.value(cache[key].response);
  }
}

Not much to say about it, this is almost exactly the same as the document one. But this time we are returning a QuerySnapshot.

3 document().where()

Future<fb.QuerySnapshotqueryCacheWrap(String path, String field, String relation, dynamic value,
      {String additionalInfo, bool isForced}) async {
    String key = path + (additionalInfo ?? "") + field + relation + value.toString();
    if (isForced ?? false) {
      final res = await db.firestore().collection(path).where(field, relation, value).get();
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
      return Future.value(cache[key].response);
    }

    if (cache.containsKey(key)) {
      if (DateTime.now().difference(cache[key].timestamp) < Duration(seconds: cacheDuration)) {
        return Future.value(cache[key].response);
      } else {
        final res = await db.firestore().collection(path).where(field, relation, value).get();
        cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
        return Future.value(cache[key].response);
      }
    } else {
      final res = await db.firestore().collection(path).where(field, relation, value).get();
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: res);
      return Future.value(cache[key].response);
    }
  }

There are additional parameters, that we will use to query the data. The field, relation and value has been added. The same parameters that the collection().where method has. So the real difference is we form the key from the additional parameters too, and we query the data with the where method and again, we are returning a QuerySnapshot.

Example

To use this LocalCache class, I would advise to put it in some kind of Injector class, or create a Singleton Object that contains the LocalCache.

@override
  Widget build(BuildContext context) {
    Future.delayed(Duration.zero, () {
      setState(() {});
    });

    return Scaffold(
      body: FutureBuilder<fb.DocumentSnapshot>(
        future: widget.cache.documentCacheWrap("example/1"),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return Container(
              child: Text(snapshot.data.get("text")),
            );
          }
          return Container(
            child: Text("NO DATA"),
          );
        },
      ),
    );
  }

I tested the methods with this example. But you can use this class the same way you would use the generic firestore functions.

Conclusion

Caching is important. If you want to save on the Firebase expenses, developing a local caching system should be your first priority. It is easy and it will appriciate your time invested.

Next time I am going to implement a local cache for REST APIs.

Is Flutter Worth Learning

For me, the answer is simple. You should learn Flutter in 2021. In fact Flutter should be your number one choice, if you are planning on developing apps for mobile, or even web apps.

What is Dart?

Wait what? Dart? Not Flutter?

Meme of woman confused with mathematic equations - ABC News (Australian  Broadcasting Corporation)

Well both are right. Dart is the programming language and Flutter is the frontend framework, on top of dart.

Dart was developed by Google to be a general purpose programming language, that can run on every machine natively. It was planned to be used for backend servers, battling the likes of Java or NodeJS and generally Dart was planning to replace JavaScript on the web.

Pretty big plans, huh? But did they succeed? Well, partially. They were developing a dart interpreter for Chromium, but they ended up scraping the idea. I tried it out in 2018 for a bit, it was working, but one had to install a new browser for just the sake of dart. They did not replace JavaScript, in fact, dart is now being compiled to JavaScript on the web.

So what about the other goals? Can it be used for the backend?

It cannot be used for that.

What can it be used for then? They created the Angular Framework with Dart. It was going to be another option for the Angular community. They could not only develop web apps with TypeScript of Angular, or AngularJS, but they added a new, a better option, AngularDart.

The Starter App | angulardart.dev

Finally, an established framework with the engine of Dart, and its strongly typed nature. It will be a killer new way of developing web apps. So did it happen?

No.

Flipping Tables / (╯°□°)╯︵ ┻━┻ | Know Your Meme

Okay, not as dramatic. AngularDart is a product, that you can use right now, although, it is not being developed with the speed it would deserve. It is usable, but with the kinds of Angular and React. When you are in a tough competition like this, it is expected to give it your best. But unfortunately, right now AngularDart is not giving its best.

But the product manager of Dart and Flutter has explained in a Reddit comment, what the current status of AngularDart is right now.

The real savior of Dart came out in May 2017. It was Flutter.

Easy Flutter essentials for beginners #1 - Stratton Apps

What can you do with Flutter?

As of November 2020, you can do almost anything with Flutter.

  • Mobile apps
  • Web applications
  • Desktop Applications

It really is a cross platform framework, that was intended for Dart. So they finally did it. And they did it with exceeding expectations. The framework is really great for mobile development and the functionality for it is quite complete and always improving. There are updates every month on the beta channel, and every quarter there is an update on the stable channel.

It is possible to create web apps and desktop applications with Flutter, but as of the writting of this post, it is only possible on the beta channel. It is not 100% right now, but I have been developing for the web with Flutter for 8 month now and I have to say, I am pleased with what the Flutter team has done so far on the web version of Flutter.

There are key features, in which web is very different than mobile. Here is a list about the weakness of Flutter web.

  1. Scrolling
  2. Selectable texts
  3. Overall feel on desktop

Altough the Flutter team is always improving the framework not only for mobile, but for the web as well, there are still room for improvement. But luckily we have pub dev.

Pub dev is a site where open source packages has been created by the community, that helps improve and fade the weaknesses of Flutter. Even I have been creating packages for Flutter, because I believe that this framework really worths the work.

I have 2 packages so far that both are targeting scrolling and I have blogposts for both of them. One is improving the scroll performance when using a mouse wheel, which I pretty much did not like when I tried out Flutter web for the first time. The other is the scrollbar. Flutter did not have a general scrollbar on desktop, so I have created one.

In the future you could also develop some missing features for Flutter and it would be also beneficial for everyone. I am planning on creating a package for the second problem, which is the selectable text problem.

Is Flutter easy?

Yes. Flutter is easy, even for a first time language it is generally a great starting point if you are planning on picking up coding. First it is a no brainer is you are going to develop mobile applications, because the alternatives are not on the same level as Flutter. Of course you could create mobile apps with Java/Kotlin for android and Swift for IOS, but wouldn’t it be easier, if you could develop apps for both platforms with just one language? Flutter promises that.

It is cross platform and runs natively, because the Flutter code is being translated to Java, when you are building an android app, and for Swift if you are building an IOS app.

It is also an Object Oriented Programming language (OOP), which you must know if you are going to establish yourself as a programmer in general. It is strongly typed, which means every variable you create has a type and must have a type. Maybe it is just me, but I prefer the strongly typed languages to the weakly typed ones.

But if you want to create web apps and desktop apps with it, you could start with Flutter if you are really into it, but for the web, I would advise against it as your first language. Maybe a year later, when it finally comes out on the stable channel as well. Until then, there are more established web frameworks that you can look into.

The future of Flutter

Google is dreaming big with Flutter. They are developing an operating system with Dart that will replace Android, that is Fuchsia. That is good for us. It means that Flutter is being used heavily by one of the biggest internet companys. Currently there are no release date set for the project, but they are developing it for about 3 years.

The job offerings for Flutter is projected to be high, when Fuchsia releases. But even before it, it is getting higher each year. If you don’t want to be an employee, because you want to be a freelancer, or an entrepreneur, Flutter is a good choice.

Upwork is littered with Flutter projects, that you can apply right now. The number of jobs for Flutter is lower than Java Swift or the other mobile languages, but the early bird gets the worm. It is a good idea to start learning Flutter and be one of the people who has years of experience with it. It will be a valuable knowledge in the future.

Conclusion

We have concluded that Dart is a really great language for beginners and Flutter tops the Mobile scene with it’s logical structure. It is also really great if you are planning on freelancing.

Create Simple Animations In Flutter

You can create animations with the TweenAnimationBuilder widget in Flutter. A great animation can differentiate among web pages. If you want your page to stand out, you have to make everything in your arsenal to make it happen. Animations are little extras, that you want to know from up to bottom and want to use them with ease.

It should not be a drag to create your own animations. Today I am going to show you the easiest way of making animations in Flutter.

Why develop animations on your own?

Nowadays you cannot prevail on the internet without doing something more than your competitors. It can be anything. A great product, a revolutionary business model, or event the animations if they are stunning. But why develop animations and not search for them on the internet. Two reasons.

The animation that your heart desires may not be found on the internet, or it is, but there are something you would like some other way.

By creating your own animations, you are slowly developing yourself technically, you will have practise, you will be able to create any animations you want. It is highly customizable.

If there come a time when you, or your customer want a difficult animation, you can create it and satisfy your or the customer’s wish. And trust me, customer’s will not look at the backend code, or the frontend code. They are not interested in how the page works (most of them are not), but they are very interested in the look and feel of the final product. If the customer likes what he/she sees, he will be a reoccuring client of yours.

When to use animations?

Well everything you know about UI/UX, you can apply to making animations. Let your imagination fly. But it can be broken down to these fundamental points:

Use animations where and when they are necessary. Everyone remembers those kitschy mouse animations from the early 2000s, when you moved the mouse the some glistering partical would come out from the cursor. Yikes! Back then you could find those in nearly every website, and they were all terrible. Fight me on this!

https://blog.humphd.org/the-technology-of-nostalgia/

The theme of the animation should match with the theme of the website. Is it for kids website? Put a lot in it, play with it, kids will love them. Is it for some official website? Maybe tune it back a bit. Noone wants to see santa waving when doing their tax. Okay, you got me, a waving santa could fit in everywhere.

How animations work?

The animations I am going to show you are all going to work by the same principle. We are going to have an interval, where we are going to be incrementing and decrementing a number. That number could be then used for whatever you’d like.

During the animation, we are going to rebuild the component as many times as necessary, until the number gets from the one end of the interval, to the other. We can make the animation faster or slower, by changing its duration. I am always using milliseconds, because it gives me a good precision of controlling the speed of the animation.

There are other methods of animation, but in my opinion this is the easiest way, that not require any additional mixins for our class.

Why I don’t use Ticker

Creating an animation with SingleTickerProviderStateMixin is just a bit harder than creating an animation without them. It has more boilerplate code, than the code I am going to show you. For you it could be a minor difference, but from a programmer’s standpoint if you can make your code smaller and easier to read, then you should do it that way.

Animating in Flutter

In my opinion currently the best and easiest way of animating in Flutter is the TweenAnimationBuilder widget. It works as described, it moves a number beTWEEN the start and end point of the given interval.
The parameters I am going to be using are:

  • Tween, which is the interval description object, which has begin and end properties.
  • Duration for the speed of the animation.
  • Curve, which describes the movement of the animation. The Curves class has several predefined Curve objects, that you can use, from linear movements to bouncy ones.
  • The onEnd callback function. It is a great method to turn around your animation repeat it, or just do something when the animation completes.
  • And lastly the builder method, which gives back the current number between the interval. Within that method, we are going to be building the animation.

Examples

We are going to make 5 examples:

  • Menu animation
  • Multiline loading animation
  • Single line loading animation
  • Warning sign animation
  • Scrollbar animation

Every animation has common components like the AnimationState enum, which will tell us what state the animation is.

Menu animation

Let’s start with the easiest, just to warm up. Our task is to change the height of the menu, when the cursor is hovering on top of the header. So we need the MouseRegion widget, so we can keep track of the animation state.

Widget _getMouseRegion(Widget child) {
  return MouseRegion(
    onEnter: (s) {
      setState(() {
        animationState = AnimationState.OUT;
      });
    },
    onExit: (s) {
      setState(() {
        animationState = AnimationState.IN;
      });
    },
    child: child,
  );
}

The build method

Widget build(BuildContext context) {
  Tween tween;

  if (animationState == AnimationState.OUT) {
    tween = Tween<double>(begin: MIN_HEIGHT, end: MAX_HEIGHT);
  } else {
    tween = Tween<double>(begin: MAX_HEIGHT, end: MIN_HEIGHT);
  }

  return TweenAnimationBuilder<double>(
    tween: tween,
    duration: Duration(milliseconds: 200),
    curve: Curves.bounceIn,
    builder: (BuildContext cont, double height, Widget w) {
      return _buildBody(height);
    },
  );
}

And the body

Widget _buildBody(double height) {
  return _getMouseRegion(
    Center(
      child: Container(
        height: height,
        width: double.infinity,
        color: Colors.blue,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            Text("Home", style: TextStyle(color: Colors.white, fontSize: 22)),
            SizedBox(width: 20),
            Text("Login", style: TextStyle(color: Colors.white, fontSize: 22)),
            SizedBox(width: 20),
            Text("Contacts", style: TextStyle(color: Colors.white, fontSize: 22)),
            SizedBox(width: 20),
          ],
        ),
      ),
    ),
  );
}

The next will be slightly harder, but still a simple animation.

Multiline loading animation

Because when would you want an animation the most? Of course when there is nothing happening on the screen and you want to show the user that, something IS happening behind the curtains, so just hold your horses and don’t click away.

This animation, unlike the previous, will be repeated infinitely, so we are using he onEnd callback to change the state when the animation ended.

Widget build(BuildContext context) {
  Tween tween;
  switch (animationState) {
    case AnimationState.IN:
      tween = Tween<double>(begin: MAX_WIDTH, end: MIN_WIDTH);
      break;
    case AnimationState.OUT:
      tween = Tween<double>(begin: MIN_WIDTH, end: MAX_WIDTH);
      break;
  }

  return TweenAnimationBuilder<double>(
    tween: tween,
    duration: Duration(milliseconds: 550),
    curve: Curves.linear,
    onEnd: () {
      setState(() {
        if (animationState == AnimationState.IN) {
          animationState = AnimationState.OUT;
        } else {
          animationState = AnimationState.IN;
        }
      });
    },
    builder: (BuildContext cont, double width, Widget w) {
      return _buildBody(width);
    },
  );
}

The line builder is has a bit of math in it, because there are 2 groups of lines, group 0 is the first and third, groupt 1 is the second and fourth line. That is important, because we have to differentiate among the lines. When group 0 has full height, group 1 should have the minimum height, and vica versa.

Widget _getloadingLine(double width, int groupId) {
  double animatedWidth = width;
  if (groupId == 1) {
    animatedWidth = MIN_WIDTH + (MAX_WIDTH - width);
  }

  return Container(
    height: 3,
    width: animatedWidth,
    color: Colors.blue,
  );
}

The body is farily simple.

Widget _buildBody(double width) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      _getloadingLine(width, 0),
      SizedBox(height: 8),
      _getloadingLine(width, 1),
      SizedBox(height: 8),
      _getloadingLine(width, 0),
      SizedBox(height: 8),
      _getloadingLine(width, 1),
    ],
  );
}

Single line loading animation

So I came up with another simple loading indicator, which is just a simple line, that has 2 colors, when one is gaining, the other is disappearing. Simple enough, so how is it?

The build method is the same as before, with one minor change. Up to this point I used linear curves for the animation movement. Now I am going to use another, which in this example, looks better. That is the decelerate curve, which slowly decelerates when reaching the endpoint.

curve: Curves.decelerate,

Then we have to build 2 lines, in a row, with different colors and widths.

Widget _buildBody(double width) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      _getloadingLine(width, Colors.blue[300]),
      _getloadingLine(MAX_WIDTH - width, Colors.greenAccent),
    ],
  );
}

Widget _getloadingLine(double width, Color color) {
  double animatedWidth = width;

  return Container(
    height: 5,
    width: animatedWidth,
    color: color,
  );
}

Warning sign animation

Once again the build method stays the same, but this time, we are going to return the widget straight in the builder method.

builder: (BuildContext cont, double colorNum, Widget w) {
  return Center(child: _getCube(colorNum.floor()));
},

We are going to finish off with a WARNING text and the color attribute, in the following way.

Widget _getCube(int colorNum) {
  return Container(
    decoration: BoxDecoration(
      borderRadius: BorderRadius.all(Radius.circular(30)),
      color: Color.fromARGB(255230, colorNum, colorNum),
    ),
    height: 42,
    width: 130,
    child: Center(
      child: Text(
        "WARNING",
        style: TextStyle(color: Colors.white, fontSize: 22),
      ),
    ),
  );
}

Srollbar animation

In my previous post , I made a generic feel scrollbar for Flutter web. Now I am going to extend the functionality of that scrollbar, with the Menu hide/show animation. I have created a widget, that changes the state of the animation when the cursor interacts with the scrollbar. This widget looks similar to the Menu example state handler.

Widget _getMouseRegion(Widget child) {
  return MouseRegion(
    onEnter: (s) {
      setState(() {
        thumbAnimation = ThumbAnimation.OUT;
      });
    },
    onExit: (s) {
      setState(() {
        thumbAnimation = ThumbAnimation.IN;
      });
    },
    child: child,
  );
}

I ended up using this method on the scrollbar background and on the scrollthumb itself.

return Stack(
  fit: StackFit.loose,
  alignment: Alignment.topRight,
  children: [
    widget.child,
    _getMouseRegion(_getScrollbarBackground(width)),
    _getScrollThumb(width),
  ],
);

Then adjusted the width in both of the functions.

Conclusion

So now you know how to create simple animations in Flutter. My advice to you is to come up with your own ideas regarding the animations, so the internet will be full of unique and interesting animations. Stand out with your creativity.

The whole code is available on my gitlab account.

Flutter Web Scrollbar With Generic Desktop Feel

A generic desktop feel scrollbar is not yet available in the Flutter framework, but with a bit of a work, it can be implemented. That’s what we are going to do today.

This scrollbar should be used only on the desktop version of your web application. It is not common to use a scrollbar on mobile, but if you want to use one, than Flutter has one ready for you, and that is the ScrollBar widget. But it is alien on the web, to use such scrollbar.

Why develop a scrollbar?

Because scrolling is one of the most important things on the web, it should not be taken lightly and should be handled very professionally. If not, a buggy, jerky scroll and scrollbar could ruin the user experience of your web application.
Scrolling alone is not enough, because there are always users, who are using old laptops, without a mouse, without a scrollwheel, their only option is to have a scrollbar to drag onto.

What makes a great scrollbar?

The scrollbar should be very responsive to resizing, it should readjust itself, to fit the window. So here are my requirements for the perfect scrollbar:

  • The height of the scroll thumb is perfectly sized in ratio of the visible height, and the unvisible height.
  • The scrollbar should perfectly move along with the mouse that’s dragging it.
  • The scrolling speed should match up with the amount of the drag movement.
  • It goes without saying, but the direction of the scroll thumb and the scrolling should be opposite
  • It’s not neccesary, but in this example we’re going to make a fade in/ fade out scrollbar.

How does the scrollbar work?

The hardest part of making the perfect, bugfree scrollbar, is coming up with the formulas that are going to be used to calculate all the stuff that I have written in the requirements. The formulas are not that hard to make at first glance, but it took me 1 or 2 hours to satisfy every requirement. It is easy to mess up. I am going to show you the formulas with a very easy example for it to be easier to understand.

The first question should be: Should I have a scrollbar, does it make sense now to have a scrollbar? To answer that question we need to calculate the remainder height of the child widget. So basically we need to subtract the visible height from the child’s full height.
If the result is negative, than we need to have the scrollbar, otherwise, we can just ditch the scrollbar for now.

The next step is to determine the height of the scroll thumb. For that we need the ratio of the visible height and the child’s full height. Take the reciprocal of the ratio, and multiply it by the height of the window. It makes sense believe me. Formula:

ratio = fullHeight / visibleHeight
thumbHeight = (1 / ratio) * visibleHeight.

That’s it, we have everything to make the scrollbar look accurate. But what about the movement? Next step.

You can move the scrollbar 2 ways. Either scroll with the mousewheel, or by dragging it with the mouse. In the future, I will add keyboard support as well for the up and down arrows.

There are problems with each one, and we need some other formulas that we need to get right.

Scrolling with the mouse wheel: We have to move the scroll thumb proportionally to the mouse scrolling. So we need a formula that tells us how the scroll thumb should move properly. The formula will give us the top position of the scroll thumb.
We need to divide the visible height with the full height then multiply by the amount of space the scroll has moved. That’s how we get the top position of the scroll thumb.
The formula:

scrollThumbTop = ((visibleHeight) / fullHeight) * scrollExtentBefore

It will make sense in a minute.

For when the scrollbar is being dragged, the scroll position should be moved the accurate spot. The formula for that is:

(dragDelta * ratio) – offsetTop

The offsetTop is the spot on the scrollThumb, where the user has clicked on, multiplied by the ratio. It could mess up the calculations if we did not normalize the position that the scrollbar was touched.

Okay, we are done figuring out the formulas that we need to use, so let’s put it into action.

Implementing the scrollbar with Flutter

Before starting, I would suggest checking out my previous blogpost, I have written on making the scrolling smoother with Flutter web. This guide uses the SmoothScrollWeb component, that I have made in that post.

So the ScrollBarWeb component will be our wrapper for the code, but that wrapper is going in another wrapper, that is the SmoothScrollWeb component.
It’s important to disable the scrolling on the child widget, by adding the NeverScrollableScrollPhysics to its physics, otherwise it won’t work. We need the complete control over the scrolling, that’s why we are adding the physics.

return Container(
  color: Colors.red,
  child: SmoothScrollWeb(
    controller: controller,
    child: ScrollBar(
      child: _getChild(),
      controller: controller,
      visibleHeight: MediaQuery.of(context).size.height,
    ),
  ),
);

Example child

Widget _getChild() {
  return Container(
    child: SingleChildScrollView(
      physics: NeverScrollableScrollPhysics(),
      controller: controller,
      child: Column(
        children: [
          for (int i = 0; i < 200; i++)
            Container(
              height: 10,
              color: RandomColor.generate(),
            ),
        ],
      ),
    ),
  );
}

The ScrollBar class will have the following properties:

///Same ScrollController as the child widget's.
final ScrollController controller;

///Child widget.
final Widget child;

///The height of the child widget.
final double visibleHeight;

///Lenght of the Thumb fade in out animations in milliseconds.
final int animationLength;

///The color of the scroll thumb
final Color scrollThumbColor;

///The background color of the scrollbar.
final Color scrollbarColor;

///The width of the scrollbar, when it is 'hidden'
final double scrollbarMinWidth;

///The width of the scrollbar, when it is 'showing'
final double scrollbarMaxWidth;

Getting the full height of the child

It is a crucial point of the scroll bar. It would be really great if we could get the full height of the child somehow and not fill in the full height manually, which would be a drag. But how should it be done? Luckily we have a way of finding the height. But it is only available, once the widget has loaded. It is not desireable, but with a little bit of trickery we can make it work in the build method.

if (fullHeight == null) {
  Future.delayed(Duration.zero, () {
    setState(() {
      fullHeight = widget.controller.position.maxScrollExtent + widget.controller.position.viewportDimension;
    });
  });
  return widget.child;
}

We need to build twice. first without the scrollbar, and once we have the scrollController initialized, we can have the scrollController.position.maxScrollExtent, which is the maximum scrollable height, and the viewportDimension, which is basically the visible height. Add these 2 together, and you will get the fullHeight. Next step.

Remainder calculation

So do you need the scrollbar? Calculate it:

final remainder = (fullHeight - widget.visibleHeight);

if (remainder < 0) {
  return widget.child;
}

Ratio and thumbHeight:

ratio = fullHeight / widget.visibleHeight;
thumbHeight = (/ ratio) * widget.visibleHeight;

Then we are going to return a Stack widget, with the child widget on top, then the scrollbar background and lastly the scroll thumb. The order is important, because the later the widget, the higher layer it will be.

return Stack(
  fit: StackFit.loose,
  alignment: Alignment.topRight,
  children: [
    widget.child,
    _getScrollbarBackground(width),
    _getScrollThumb(width),
  ],
);

The scrollbar background will have the length of the visible height, with a width of your choice, but please match the width with the thumb width that you are going to use.

Widget _getScrollbarBackground(double width) {
  return Container(
    width: width,
    height: widget.visibleHeight,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(3),
      color: widget.scrollbarColor,
      border: Border.all(color: widget.scrollbarColor, width: 1),
    ),
  );
}

One of the reasons I am using a Stack and not a Row, is because I am going to animate the scrollbar to appear and dissappear due to mouse movement. In the next post, I am going to show you how to do animations and we will go more in depth with the scrollbar’s width property.

Building the scroll thumb

Lastly there is the scroll thumb builder function _getScrollThumb. We are going to take advantage of flutter’s Positioned widget, which is a great sollution for moving the scrollthumb anytime we scroll, or touch it. The most important property of the Positioned widget is the top property.
The top property positions its child. We need to use the formula that we have put together for calculating the top property.

double calculateTop() {
 return (widget.visibleHeight / fullHeight) * widget.controller.position.extentBefore;
}

That takes care of the scrolling with the mouse wheel part.

But we do need to take care of the dragging, and we are going to use the GestureDetector widget, with the onVerticalDragDown and the onVerticalDragUpdate properties. With the onVerticalDragDown callback normalizing the dragging point, and giving us the offsetTop and the onVerticalDragUpdate handling the scrolling.

Widget _getScrollThumb(double width) {
  return Positioned(
    top: fullHeight != null calculateTop() : 0,
    child: GestureDetector(
      onVerticalDragDown: (s) {
        offsetTop = widget.controller.offset.toDouble() - (s.localPosition.dy * ratio);
      },
      onVerticalDragUpdate: (dragDetails) {
        final newPosition = (dragDetails.localPosition.dy * ratio) + offsetTop;
        widget.controller.jumpTo(newPosition);
      },
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(3),
          color: widget.scrollThumbColor,
          border: Border.all(color:widget.scrollThumbColor, width:1),
        ),
        width: width,
        height: thumbHeight,
      ),
    ),
  );
}

One more finishing touch will be needed for it to work as it’s intended. The scroll thumb will not be moved by the scrollController, unless we refresh the ScrollBar widget, every the scrollController is scrolled. The easiest way is to add a listener to the scrollController.

@override
void initState() {
  widget.controller.addListener(() {
    setState(() {});
  });
  super.initState();
}

That’s it. We are done. Now the scrolling is finally just like the page was written in html. The whole code is available on my gitlab account. Or you can get the package from pubdev.

The results

Conclusion

Although Flutter Web is in beta stages, and lack some basic features on desktop, we can easily create our own component, that will fit on desktop, so our site will blend in as a generic webpage, and not stand out with it’s lack of desktop features.

Smooth Scrolling With Flutter Web

Smooth scrolling with flutter web is not yet implemented in the Flutter framework, but we are going to solve this problem by doing our own smooth scrolling component.

The scrolling on mobile looks great, but scrolling with the mouse scroll on desktop is jerky. The reason is, the scrolling animation just snaps when you are scrolling with the mouse wheel.

Jerky scroll

We can fix it, by adding a wrapper around our scrollable widget, that will animate the scrolling using the ScrollController’s animateTo function.

SmoothScroll(
    child: _getChild(),
    controller: controller,
),

First we need to disable the default physics of the scrollable widget, we would like to use the SmoothScroll on. NeverScrollableScrollPhysics should be used on the desktop version of your page. Unfortunatelly it disables the scrolling on mobiles as well, but you should be able to separate the mobile and desktop views, so you would only use NeverScrollableScrollPhysics, when the desktop version is loaded.

Widget _getChild() {
    return Container(
      child: ListView(
        physics: NeverScrollableScrollPhysics(),
        controller: controller,
        children: [
          ...
        ],
      ),
    );
}

Smooth Scroll Inputs

  • Widget child: This is going to be our scrollable widget.
  • ScrollController controller: This is the same ScrollController that we will attach to the child as well.

In addition, we will need an inner variable to keep track off the scrolling, which will has a double value. It is always a great practise to name some constant values
in order to avoid using magic numbers in the code. In our case these constants will be:

  • NORMAL_SCROLL_ANIMATION_LENGTH_MS: This constant holds the length of the animation in milliseconds.
  • SCROLL_SPEED: This is the ammount the page will go down, or go up, by just one scrolling click.

We can modify the animation length that will result in faster or slower scrolling animation and also modify the scroll speed.
You can play with it and optimize it the way you like it. The default values that worked for me the most are 250ms of animation, and 130 for the scroll speed.

Scroll animation

First we need a way to listent to the mouse scroll event. We can use the Listener widget for that. The Listener widget has an onPointerSignal named parameter, which is a function that catches PointerSignalEvents.

Listener(
  onPointerSignal: (pointerSignal) {
    if (pointerSignal is PointerScrollEvent) {
          
    }
  },
  child: child,
);

We are going to catch PointerScrollEvents, which activates anytime the mouse scroll wheel is moved. We need some additional information in this event.

  • In which direction did the scroll move?
  • Are we going to exceed the limits of the scrollable widget?

The direction of the scroll can be determined via the pointerSignal.scrollDelta.dy, which is the vertical scroll delta of the mouse wheel. Positive when scrolling down, negative, when scrolling up. Now we can add or extract the SCROLL_SPEED from the scroll variable.

if (pointerSignal.scrollDelta.dy > 0) {
    scroll += SCROLL_SPEED;
} else {
    scroll -= SCROLL_SPEED;
}

We need to check the bounds of the scroll extent. We want to keep the scroll at it’s bounds, and we don’t want to overshoot. For better feel, we are going halve the milliseconds neccessary when we are about to exceed the bounds.

if (scroll > controller.position.maxScrollExtent) {
    scroll = controller.position.maxScrollExtent;
    millis = NORMAL_SCROLL_ANIMATION_LENGTH_MS ~/ 2;
} else if (scroll < 0) {
    scroll = 0;
    millis = NORMAL_SCROLL_ANIMATION_LENGTH_MS ~/ 2;
}

At last, we are going to animateTo the calculated position, within the given time, and with a linear curviture. I have tested numerous Curves, but I found that the most generic curve for the desktop, is the linear curve.

Listener( 
    onPointerSignal: (pointerSignal) {
        int millis = NORMAL_SCROLL_ANIMATION_LENGTH_MS;
        if (pointerSignal is PointerScrollEvent) {
            if (pointerSignal.scrollDelta.dy > 0) {
              scroll += SCROLL_SPEED;
            } else {
              scroll -= SCROLL_SPEED;
            }
            if (scroll > controller.position.maxScrollExtent) {
              scroll = controller.position.maxScrollExtent;
              millis = NORMAL_SCROLL_ANIMATION_LENGTH_MS ~/ 2;
            } else if (scroll < 0) {
              scroll = 0;
              millis = NORMAL_SCROLL_ANIMATION_LENGTH_MS ~/ 2;
            }

            controller.animateTo(
              scroll,
              duration: Duration(milliseconds: millis),
              curve: Curves.linear,
            );
        }
    },
    child: child,
);

That’s it, we are done. And here is the result.

Smooth Scrolling

Conclusion

Although Flutter Web is in beta stages, and lack some basic features on desktop, we can easily create our own component, that will fit on desktop, so our site will blend in as a generic webpage, and not stand out with it’s lack of desktop features.

You can clone the example project at this link. Or you can get the package from pubdev.