DEV Community

loading...
Cover image for Cleaner Flutter Vol. 7: Where the data comes from

Cleaner Flutter Vol. 7: Where the data comes from

Marcos Sevilla
Flutter and Dart developer.
Updated on ・6 min read

Welcome to one more volume of Cleaner Flutter. I know we left the last volumes a bit unfinished but we will make an effort to finish all the remaining articles this month.

Without more to mention, let's get down to business.

Where we left

The previous volume began with our data layer, a layer where we implement all the actions described in the domain layer to be able to interact with an external service, or a local service (some database on the device or contact with the hardware where we run).

Data Layer in CleanScope

As the title says, let's talk about where the data comes from. We already have our models where we establish what data we are going to use, now we have to go and bring them.

We tell the classes DataSource that they are going to bring us the "raw" data, convert it into the respective models and pass it to their repository. But there are also other nomenclatures, as in the Bloc library, they are called DataProvider.

You can call it whatever you want, it seems to me that the name DataProvider would be better if Providers of these classes were created.

Since we don't create Providers for DataSources at Riverpod, the name is enough for me.

More of this in the article on business logic.

The source of the data

To define what a DataSource is, I'm going to borrow the concept of DataProvider from bloclibrary.dev...

They provide provide raw data, they should be generic and versatile. They will usually expose simple APIs to perform CRUD operations.

When I use DataSources I separate them into two folders: local and remote. This is because DataSources are any way to bring information, which as I mentioned before can be on the same device or to an external API where we have to use HTTP or some other internet protocol.

DataSources folder structure

A common question is why we don't use repositories to do everything the DataSource does. Mainly because there are many possible actions that are carried out with the data that we want to bring.

A case that happens to me quite often is that I want to save some data in a local database (my preferred option is hive) and I bring the data from a REST API. The save to device action and the API call action are two different functions. In addition to being two actions that are executed in two different data sources.

Given the above, my repository is in charge of establishing this flow with two different data sources and is in charge of handling the data returned by various data sources and subsequently communicating that result to the business logic.

Implementing a DataSource

LocalDataSource

import 'dart:convert';

import 'package:hive/hive.dart' show Box;

abstract class ILocalDataSource {
  SomeModel getSomeModel();
  Future<void> saveSomeModel(SomeModel model);
}

class LocalDataSource implements ILocalDataSource {
  LocalDataSource({required Box box}) : _box = box;

  final Box _box;

  @override
  SomeModel getSomeModel() {
    final encodedJson = _box.get('model_key');

    if (encodedJson != null) {
      final model = SomeModel.fromJson(json.decode(jsonStr));
      return model;
    } else {
      throw CacheException();
    }
  }

    @override
  Future<void> saveSomeModel(SomeModel model) async {
    await _box.put('model_key', json.encode(model.toJson()));
  }
}
Enter fullscreen mode Exit fullscreen mode

This is what a DataSource looks like locally. I reuse this structure to store unique values in a Hive Box, which is the equivalent of a table in SQL.

I always recommend that every class that you are going to test make an abstract class, so it is easier for them to make their Mocks with packages like Mocktail.

In the same way, define the dependencies that you are going to use as properties of the class to only provide you with an object with which you can perform the actions you need. We will see this last point in more detail in the RemoteDataSource.

https://media.giphy.com/media/3orieNQgdcxedKjA3e/giphy.gif

RemoteDataSource

import 'package:dio/dio.dart';

abstract class IRemoteDataSource {
  Future<SomeModel> getModel(int modelId);
}

class RemoteDataSource implements IRemoteDataSource {
  RemoteDataSource({
    required Dio client
  }) : _client = client;

  final Dio _client;

  @override
  Future<SomeModel> getModel(int modelId) async {
    try {
      final response = await _client.get('/models/$modelId/');

      if (response.statusCode != 200) {
        throw ServerException();
      }

      return SomeModel.fromJson(response.data);
    } catch (e) {
      throw ServerException();
    }
  }  
}
Enter fullscreen mode Exit fullscreen mode

There are packages that make it much easier for us to structure our code and dio is one of them. I have an article about Dio here, in case you would like to know more about the benefits it brings us and their capabilities.

It is a very similar structure to the LocalDataSource. We have a dependency injection pattern where we still define dependencies as properties and pass an instance to our private property. All equal.

The difference lies in the implementation of how we get the data, one accesses the database in Hive and the other goes to a remote API to take this data through Dio (HTTP).

Other implementations

For both types of DataSources, we can have different implementations so we make an interface that we can implement and define the body of the methods that we are going to use.

// imports remote data source abstract class

import 'package:http/http.dart' show Client;

class AnotherRemoteDataSource implements IRemoteDataSource {
  AnotherRemoteDataSource({
    required String url,
    required Client client,
  })   : _url = url,
        _client = client;

  final String _url;
  final Client _client;

  @override
  Future<SomeModel> getModel() async {
    try {
      final result = await _client.get(Uri(scheme: _url));

      if (response.statusCode != 200) {
        throw ServerException();
      }

      final decode = json.decode(result.body);
      return SomeModel.fromJson(decode);
    } catch (e) {
      throw ServerException();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This other implementation uses Dart's official http package and we can see that the result will be the same, only with a different way of doing it with another package.

There are 2 great advantages that Dio's package offers us compared to HTTP. One is decoding our response from a pure JSON string to a Dart Map <String, dynamic>. The other we see in the next point.

Reusing code

As you have seen from previous articles, CleanScope focuses on developing a project based on specific features. By abstracting our data logic into DataSources and Repositories we can arrive at a structure that, combined with Dio, is very orderly.

The following example is following how the creation of our data layer to be used as a dependency for our project would look like using flutter_bloc:

final client = Dio(BaseOptions(baseUrl: AppConfig.apiUrl));

...

MultiRepositoryProvider(
  providers: [
    RepositoryProvider(
      create: (_) => SomeRepository(
        localDataSource: LocalDataSource(box: Box()),
    remoteDataSource: RemoteDataSource(
          client: client),
        ),
      ),
    ],
  child: const App(),
);
Enter fullscreen mode Exit fullscreen mode

In the following article we are going to go into detail about how the repository already implemented looks like, but for the moment we know that it receives its 2 DataSources as properties.

Dio allows us to reuse instances of its clients with specific configurations so that we make direct calls to endpoints without specifying a base URL. This operation can be seen in the GET of the RemoteDataSource that I built previously with Dio:

final response = await _client.get('/models/$modelId/');
Enter fullscreen mode Exit fullscreen mode

That's one of the ways our DataSources are best used.

https://media.giphy.com/media/QoesEe6tCbLyw/giphy.gif

As always...

You can share this article to help another developer to continue improving their productivity when writing applications with Flutter.

There's a Spanish version of this article on Medium. You're welcome. 🇪🇸

Also if you liked this content, you can find even more and keep in contact with me on my socials:

  • dev.to - where you're reading this article.
  • GitHub - where are my code repositories in case you like the examples.
  • LinkedIn - where I connect professionally.
  • Medium - where I publish my Spanish articles.
  • Twitter - where I express my short thoughts and share my content.
  • Twitch - where I do informal live shows from which I take clips with specific information.
  • YouTube - where I publish the clips that come out of my lives.

Discussion (1)

Some comments have been hidden by the post's author - find out more