Working with Data Assets | Flutter

Working with Data Assets |  Flutter

Working with Data Assets | Flutter

Hello, if you are on the path of learning Flutter/Dart or you are just interested in reading about the path of learning, subscribe to my channel on telegram, I will be glad to see you! And today we’ll talk about working with Data Assets in Flutter!

In this article, you will learn how Flutter and Dart come together to manage data resources. Over the course of this article, you'll see how the location of your data affects how you access the information you need. Throughout this chapter, follow the steps to explore the various methods of embedded, asset-based, and remote data in your application.

You will learn how to:
• Reorganize data for the application
• Use information from a local JSON file
• Work with data located in the assets folder
• Process remote data using Future
• Automate JSON in Dart class

In this article, we will use two example JSON files to demonstrate how to load external data. The format of each file is shown in the following examples:

Example 1: Single Layer JSON

{
 "1": "January",
 "2": "February",
 "3": "March",
 "4": "April"
}

Example 2: Multi-user JSON

{
 "data": [
 {
 "title": "January"
 },
 {
 "title": "February"
 },
 {
 "title": "March"
 }
 ]
}

Strategic Data Access

Problem

You have data available, but you're not sure where it should be located.

Solution

Consider how the data will be used in the application and apply the appropriate strategy to access information efficiently. When choosing the right location for your data, consider: • Amount of data that will be stored • Frequency of updates

Discussion

Inside your application, data will be provided through a data pipeline, which is responsible for loading information in a timely manner.

Figure 13-1 shows three common data access patterns based on where the application data will be located. During the development process, processing data resources will be essential to create more advanced applications.

  Figure 13-1.  Data pipeline

Figure 13-1. Data pipeline

Embedded/local data represents data co-located with the application. Remember that if you are merging data with an application, you must enable a separate application update method to replace the required data set. Use embedded data where you have a large data set or data that does not need to be updated frequently.

The Assets folder data is often used for small datasets that provide initial functionality without requiring the user to do the bootstrap. Loading data from the assets folder is a good approach when the data does not require frequent updates and can therefore be included in the application. Storing data locally is not always beneficial. A key challenge is ensuring that stored local data does not become outdated. One way to minimize data staleness is to override the local assets folder data, such as applying a date or refresh cycle to the data stored in the assets folder. If you don't, be sure to provide a way to include new data when you update your application. The point is that storing data locally in the assets folder can be a quick way to organize data so that it is accessible. A typical use case for this feature is to store data that is not time sensitive.

Remote data access is a strategy used by many applications. Remote data provides greater flexibility because it is isolated from the application, so it can be changed independently. However, avoid associating the loading of significant data sets with deleted data, as this may introduce errors and cause the application to become unusable. Additionally, accessing data objects will use asynchronous code used to handle the indefinite duration of the function call. When loading datasets, always consider using asynchronous rather than synchronous code to provide the best user experience.

Once you have the data available, another consideration will be the frequency with which it will need to be updated. Again, consider the size of the dataset. A large data set does not lend itself to frequent over-the-air updates. If your situation requires a large data set, you will need to develop an appropriate mechanism to handle data updates according to your requirements.

Data refactoring

Problem

You want to improve code readability when using data embedded in your application.

Solution

To improve code readability, create an independent data class responsible for storing your data. Separating the data into a separate class makes it easier to process the data. We use a separate MyData class to provide two specific benefits of isolation and abstraction. The two use cases are discussed in more detail below.

Here's an example where the data being processed is in a class called MyData:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyData {
  final List<String> items = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December'
  ];

  MyData();
}

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);
  final MyData data = MyData();

  @override
  Widget build(BuildContext context) {
    const title="MyAwesomeApp";
    List items = data.items;
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(items[index]),
            );
          },
        ),
      ),
    );
  }
}

Discussion

The MyData class definition is responsible for declaring the data that will be accessed in the application. The data variable is based on the MyData class. The underlying data is accessed by declaring a data variable of type MyData, which is responsible for accessing information associated with the MyData class.

In the example, we create a new class called MyData. As stated earlier, we do this to take advantage of data isolation and abstraction.

Data isolation improves the overall readability of your code. By creating a class to hold our information, we can understand the code more easily. The MyData class is used to store a list of strings. Since the definition of data and its use are separated, our program is easier to read because we can clearly see the responsibilities of each section of code.

Data abstraction provides an opportunity to simplify the agreement between use and implementation. In the example, to use the data class, we simply declare a reference to it. When we create an instance variable that represents an object containing a data structure defined in the MyData class. At this point, we don't know much about the implementation other than that it provides the necessary data. As an application becomes more complex, it is useful to be able to abstract the definitions. In the previous example, we moved our data into a specific class and can extend that data class to perform additional actions as needed.

Generating Dart classes from JSON

Problem

You want to create custom Dart classes without having to write annotations or learn JSON serialization.

Solution

Use one of the many open source utilities, such as the online JSON to Dart converter or JSON to Dart. The utility will determine the classes needed to transfer your data in JSON format.

Here are some examples of generated classes showing the output when using the Sample 2 JSON dataset:

class Month {
  List<Data>? data;
  Month({this.data});
  Month.fromJson(Map<String, dynamic> json) {
    if (json['data'] != null) {
      data = <Data>[];
      json['data'].forEach((v) {
        data!.add(new Data.fromJson(v));
      });
    }
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.data != null) {
      data['data'] = this.data!.map((v) => v.toJson()).toList();
    }
    return data;
  }
}

class Data {
  String? title;
  Data({this.title});
  Data.fromJson(Map<String, dynamic> json) {
    title = json['title'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['title'] = this.title;
    return data;
  }
}

Discussion

As shown in Figure 13-2, we use an external tool to create a series of Dart classes to consume a JSON file. The example classes were generated by the JSON to Dart site and are based on the sample JSON dataset 2. Add the class name Month to generate an example similar to the previous one.

  Figure 13-2.  Example converting JSON to class

Figure 13-2. Example converting JSON to class

At first glance, the generated code may seem quite scary; however, we actually just need to update three settings to match our application's requirements and the latest Dart guidelines:

  • Dart will likely tell you that there is an “unnecessary new keyword” in your code. Dart has made the use of the new keyword optional, so you can safely remove it if the compiler complains about its presence in your code:

      Month.fromJson(Map<String, dynamic> json) {
        if (json['data'] != null) {
          data = <Data>[];
          json['data'].forEach((v) {
            data!.add(new Data.fromJson(v));
          });
        }
      }
  • You may stumble upon the delightful message “Use collection literals whenever possible.” In your code, you need to update Map() to {} to remove this message and make the compiler happy again:

    Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = Map<String, dynamic>();
        if (this.data != null) {
          data['data'] = this.data!.map((v) => v.toJson()).toList();
        }
        return data;
      }
  • The last message concerns the use of the this keyword. If you see “Don't access elements with 'this' unless shadowing is avoided”, then that's telling you to remove that reference from the variable because it's not needed:

    Map<String, dynamic> toJson() {
       final Map<String, dynamic> data = <String, dynamic>{};
       data['title'] = this.title;
       return data;
    }

Once you make these changes, your Dart class will be ready to use in your Flutter app. Although the transition from JSON can be done manually, it is more efficient and less error-prone to use a utility to perform this task. A typical use case for this approach is when there won't be many changes needed to the data set.

Using an automated JSON wrapping solution can be more efficient where you need fast rendering of a JSON dataset. If you have a more complex data structure, this can be even more useful since you no longer have to decipher the appropriate structure to use. Use a tool like this to dynamically read and process the desired structure.

Asynchronously using local JSON data

Problem

You need a way to use a string containing information in JSON format.

Solution

Use Dart's built-in JSON processing capabilities to parse JSON-formatted information. Without using a package, Dart processing can be difficult.

Here's an example that uses the built-in JSON data set from Example 2. The JSON data is loaded asynchronously, assigned to a variable, and converted to a string:

import 'package:flutter/material.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

// Example 2: JSON Dataset
class MyData {
  final String items="{"data": [
	 { "title": "January" },
	 { "title": "February" },
	 { "title": "March" },
 ] }";
}
class DataSeries {
  final List<DataItem> dataModel;
  DataSeries({required this.dataModel});
  factory DataSeries.fromJson(Map<String, dynamic> json) {
    var list = json['data'] as List;
    List<DataItem> dataList =
        list.map((dataModel) => DataItem.fromJson(dataModel)).toList();
    return DataSeries(dataModel: dataList);
  }
}

class DataItem {
  final String title;
  DataItem({required this.title});
  factory DataItem.fromJson(Map<String, dynamic> json) {
    return DataItem(title: json['title']);
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Local JSON Future Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(
        title: 'Local JSON Future Demo',
        key: null,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Future<String> _loadLocalData() async {
  final MyData data = MyData();

  return data.items;
}

class _MyHomePageState extends State<MyHomePage> {
  Future<DataSeries> fetchData() async {
    String jsonString = await _loadLocalData();
    final jsonResponse = json.decode(jsonString);
    DataSeries dataSeries = DataSeries.fromJson(jsonResponse);
    print(dataSeries.dataModel[0].title);
    return dataSeries;
  }

  late Future<DataSeries> dataSeries;
  @override
  void initState() {
    super.initState();
    dataSeries = fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder<DataSeries>(
          future: dataSeries,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return ListView.builder(
                itemCount: snapshot.data!.dataModel.length,
                itemBuilder: (BuildContext context, int index) {
                  return ListTile(
                    title: Text(snapshot.data!.dataModel[index].title),
                  );
                },
              );
            } else if (snapshot.hasError) {
              return Text("Error: ${snapshot.error}");
            }
            return const CircularProgressIndicator();
          }),
    );
  }
}

Discussion

The example code introduces the use of initState and FutureBuilder. initState is a method used to perform tasks during the initialization phase of a class. Use this method to load resources before using them in your application, as shown in Figure 13-3. Once this step is completed, the data structure should be loaded with local embedded data.

  Figure 13-3.  Example of loading embedded data

Figure 13-3. Example of loading embedded data

The initState call is a one-time call associated with initializing _MyHome PageState. During this method, make a call to fetchData, which is a generic asynchronous method used to load and process data ready for use in the application. We use this method to reference loading data as well as processing the information returned in this case, turning the JSON object into a string that will be displayed in the ListView.

To transfer our input data from the JSON dataset, we use the imported dart:convert package. The package includes json.decode which takes string input and returns JSON.

We're doing this step for demonstration purposes, as we'll turn this information back into a string. Now we have the data converted to JSON, we can use the application library to convert the data into a list structure. The final step is to map the data into a DataSeries construct.

To load our data, we use a private method called _loadLocalData.

This method is also asynchronous and marked private and simply returns the value of our data structure containing our embedded JSON. The method is marked private to indicate that it is to be used within the class and is not intended to be publicly accessible. This is the method used to determine the data to be loaded.

FutureBuilder is another building method that will help you process data efficiently. If the asynchronous process is not completed, FutureBuilder is smart enough to wait. FutureBuilder will wait for our data to become available; however, during this period it will show a progress bar indicating that a background activity is running. Once the data is available, FutureBuilder will display the data retrieved using the fetchData method. At this point, the data loading process will complete and FutureBuilder will display information based on the associated widget tree, as shown in Figure 13-4.

In our application, FutureBuilder uses a dataset labeled Future. The future is asynchronous calling, which improves the user experience when accessing data by offloading the main thread. Dart guidelines indicate that we should always use an asynchronous call for long-running activities such as reading files, querying a database, and retrieving web page results.

  Figure 13-4.  Example of rendering embedded data

Figure 13-4. Example of rendering embedded data

The illustrations in Figure 13-4 represent a very common data loading pattern that you can use with large data sets. Remember the sequence of the diagram and which aspects need to be defined as asynchronous.

Using a JSON dataset from the Assets folder

Problem

You want to use a custom assets folder to host a file that will be programmatically used as input.

Solution

Use the assets folder to place information that will be loaded into your application. The assets folder is general-purpose storage for your application.

Update pubspec.yaml to point to the generated assets/example2.json directory:

flutter:
 uses-material-design: true
 assets:
 - assets/example2.json

Here's an example of how to access a JSON dataset located in your application's assets folder:

import 'package:flutter/material.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

class DataSeries {
  final List<DataItem> dataModel;
  DataSeries({required this.dataModel});
  factory DataSeries.fromJson(Map<String, dynamic> json) {
    var list = json['data'] as List;
    List<DataItem> dataList =
        list.map((dataModel) => DataItem.fromJson(dataModel)).toList();
    return DataSeries(dataModel: dataList);
  }
}

class DataItem {
  final String title;
  DataItem({required this.title});
  factory DataItem.fromJson(Map<String, dynamic> json) {
    return DataItem(title: json['title']);
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'JSON Future Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(
        title: 'JSON Future Demo',
        key: null,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Future<String> _loadAssetData() async {
  final AssetBundle rootBundle = _initRootBundle();
  return await rootBundle.loadString('assets/example2.json');
}

class _MyHomePageState extends State<MyHomePage> {
  Future<DataSeries> fetchData() async {
    String jsonString = await _loadAssetData();
    final jsonResponse = json.decode(jsonString);
    DataSeries dataSeries = DataSeries.fromJson(jsonResponse);
    print(dataSeries.dataModel[0].title);
    return dataSeries;
  }

  late Future<DataSeries> dataSeries;
  @override
  void initState() {
    super.initState();
    dataSeries = fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: FutureBuilder<DataSeries>(
          future: dataSeries,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return ListView.builder(
                itemCount: snapshot.data!.dataModel.length,
                itemBuilder: (BuildContext context, int index) {
                  return ListTile(
                    title: Text(snapshot.data!.dataModel[index].title),
                  );
                },
              );
            } else if (snapshot.hasError) {
              return Text("Error: ${snapshot.error}");
            }
            return const CircularProgressIndicator();
          }),
    );
  }
}

Discussion

In the example, the code follows the pattern described in paragraph 13.3. The notable difference is that the _loadAssetData method is introduced, which is used to access data located in the application assets folder.

If you are using DartPad, please be aware that unfortunately this site does not currently support downloading local resources.

Figure 13-5 shows how the resource loading process works in Flutter. To load data from the assets folder, you need to access the root package. rootBundle contains the resources that were included in the application when built. Any asset added to the assets subsection of pubspec.yaml will be accessible through this property.

  Figure 13-5.  Example of loading Assets data

Figure 13-5. Example of loading Assets data

The assets for a Flutter app are stored in an AssetBundle. Resources added to the assets folder in Flutter pubspec.yaml are accessible through this property. To access information in your application, create a rootBundle variable of type AssetBundle. From this rootBundle variable you will have access to the assets declared in your application:

final AssetBundle rootBundle = _initRootBundle();
return await rootBundle.loadString('assets/example2.json');

If you decide to use an alternative structure in your assets folder, be sure to include the full path to the item you want to download.

Once the folder and data are available, update pubspec.yaml. Adding information to the (local) resources folder is a good way to store application data without having to go to an external solution such as a database or API.

Accessing remote JSON data

Problem

You want to receive information from an external remote API.

Solution

Use the Dart HTTP package to access remote data sources. The package allows you to access and retrieve information hosted on an external server and use it through your application.

Here's an example that adds the JSON and async packages used to access remote data:

import 'package:http/http.dart' as http;
import 'dart:async' show Future;
import 'dart:convert';

Future<String> _loadRemoteData() async {
  final response = await (http.get(Uri.parse('https://oreil.ly/ndCPN')));
  if (response.statusCode == 200) {
    print('response statusCode is 200');
    return response.body;
  } else {
    print('Http Error: ${response.statusCode}!');
    throw Exception('Invalid data source.');
  }
}

Discussion

In the example, the code follows the pattern described in paragraph 13.3. The notable difference is that the asynchronous _loadRemoteData method, shown in Figure 13-6, is used to access data from the Internet.

  Figure 13-6.  Example of remote data download

Figure 13-6. Example of remote data download

The dart:async package is used to perform asynchronous operation for network calls. Additionally, the dart:convert package is required because the API used returns JSON. Make sure these packages have been added to your application before attempting to use them.

Using an HTTP package in Dart greatly simplifies the process of retrieving external data. Similar to the examples for local and embedded data, the remote parameter declares a _loadRemoteData parameter. In this method, we make a remote call to the Uniform Resource Identifier (URI) containing our upload file. The call will return a response containing a status code that we need to check to see if we completed our request successfully.

When working with remote data, pay attention to the response codes that are returned. In general, HTTP 200 indicates a successful response, HTTP 400 indicates an error in the request, and 500 indicates a server error. Having a remote endpoint that follows common HTTP return codes can save significant debugging effort.

If the response is valid (i.e. HTTP 200), we can return a response body containing the requested JSON data. The request uses the future to tell our application that we are using asynchronous calls. Use Future when loading external data, especially if you don't know how big the data is to be retrieved. Making remote data access an asynchronous call will improve the overall performance of your application without blocking the presentation layer.

Once the data is loaded, you can process it in the usual way, depending on the needs of your application.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *