A few years ago when we started using Flutter more and more in our projects, we were confronted with many pain points for which we didn’t find an already existing viable solution. So, we created this flutter_requery from scratch. This is a Dart-first solution without any scope or context-dependency, the boilerplate is down to a minimum and it has its own caching mechanism.

The requery

One of the first major pain points we experienced when using Flutter was API calls. Since we had a strong JavaScript background, where one can easily use Axios to reduce the boilerplate, the decision was made to use Dio for handling HTTP requests and json_serializable for data serialization.

The last piece of the puzzle was how to integrate this HTTP layer into the Flutter widget tree. The requirements were quite simple and clear:

  1. Call API and store response in cache;
  2. Invalidate response in cache and re-run the API function;
  3. Optimistically set response in cache.

For starters, we turned to the dev community to find potential options. Bloc seemed like a good way to go. Well maintained and documented, has the ability to support complicated workflows, and has many different widgets you can use to support your use case. It had no out-of-the-box caching mechanism, but we could always build one. Provider was another viable option. It’s very popular among the community, especially for simpler logic with little or no business logic. Also, our integration contained less code than Bloc which means we could add new endpoints faster.

After a thorough review, we decided that both options were a no-go. 

Our reasoning behind this decision was the following:

  1. No cache – we still had to build a caching mechanism. The ability to store data was just a basic requirement, we also needed to come up with a way to delete the data from cache, re-run the API call, and set the new data. Having all of this in mind, we began to wonder was it really indispensable to use the restricted flexibility of the underlying package (Bloc, Provider). 
  2. Context-dependent – in essence, both options were wrappers around InheritedWidget, making it impossible to use them outside the widget tree. We aimed to build a Dart-first solution and provide a way for developers to manage their state in Flutter. 
  3. Scope-dependent – Bloc and Provider instantiated in ScreenA were not available for re-use on ScreenB. This limited us in handling cache invalidation. Even though this could be avoided in case we opted for the Dart-first idea
  4. Boilerplate – finally, it was just too much boilerplate. Every API call was another Provider or Bloc class. We needed a way to be fast and efficient. 

An alternative was to write our own, custom solution from scratch. After a few back and forth bug fixing sessions and one big rewrite, this is how CoreLine’s flutter_requery was born. For a better understanding, let’s start with an example:

For the purpose of staying on point, this example will use global variables instead of the data from the server. Also, HTTP calls will be mocked and latency will be mimicked with the Future. delayed function. Firstly, we need to define a function for retrieving data from the server (_getData)  and a function to modify that data (_addString).

final List<String> data = ["Hello"];
Future<List<String>> _getData() async {
    await Future.delayed(Duration(seconds: 1));
    return data;
  }
  Future<void> _addString() async {
    await Future.delayed(Duration(seconds: 1));
    data.add("World");
  }

Next, we need to connect the _getData function with the widget tree which is performed by the Query widget. Its first positional parameter is the cacheKey: the value under which our data will be stored in the cache. The cache key can be either a string or number and it accepts arrays. The next parameter, the future function will be run by the Query widget and the builder exposes the function response to the widget tree. The response contains 3 properties:

  1. data – data returned by the future, initially its value is null;
  2. loading – this is the status of the Query widget. Initially true but will be set to false once the future is resolved;
  3. error – error object, if the future throws an exception it will be stored in this object.
Query<List<String>>(
            'strKey',
            future: _getData,
            builder: (context, response) {
              if (response.error != null) {
                return Text('Error');
              }
              if (response.loading) {
                return CircularProgressIndicator();
              }
              return ListView(
                children: response.data.map((str) => Text(str)).toList(),
              );
            },
          ),

Now we are able to fetch and see our data on the screen. If you go back to our initial requirements, you will see that this covers only the first point because the ability to manipulate the cache programmatically is missing. So, we resolved that with the queryCache object.

Once we call the _addString method, new data will be stored (in the object, not in cache) but we still need to update the cache and rebuild the widget. This is done by calling the queryCache.invalidateQueries method. It takes a cache key, which must have the same value as before in the Query widget. 

void _onPress() async {
    await _addString();
    queryCache.invalidateQueries('strKey');
  }

Alternatively, if latency is an issue you can use queryCache.setOptimistic method. The idea is to build new data manually and store it in the cache. 

Pro tip: make sure to remove the await keyword before the _addString since this will block further code execution.  

void _onPress() async {
    _addString();
    queryCache.setOptimistic('strKey', [...data, 'World']);
  }

And with this last step, we can conclude our brief example. You can find the full example here.  

Conclusion

All in all, we are happy with how this turned out and ecstatic we can finally share our solution with the community. If you have any suggestions, feel free to open a pull request or discussion on Github.

So, give the flutter_requery a try and let us know what you think!