top of page

How to Handle Data Locally in Flutter

It’s a rare case when there’s no need of storing some sort of data in an app. Flutter provides various ways to do so.


For some apps we just need to store some key-value pairs, this can be done using the shared_preferences package. For some other app, we may need to store and query a large amount of data. In such a case we can use the SQLite database via the sqflite plugin available on pub.dev. Another way to persist data is to read and write files to disc.

This article specifically deals with storing data in files. The recipe will also deal with the case of storing nested data models, which is not easy to achieve using thesqfliteplugin.

This cookbook will take you through the following steps:

  1. Add the required dependencies.

  2. Create theFileHandlerclass.

  3. Find the correct local path and create a reference to the file location.

  4. Implement the read, write, update and delete methods.


Below is the nested User data model which will be used for this example:

class User extends Equatable 
{
    const User(
    {
        required this.id,
        required this.email,
        required this.name,
        required this.phone,
        required this.userAddress,  
    });
    
    final int id;
    final String name;
    final String email;
    final String phone;
    final Address userAddress;
    
    User.fromJson(Map<String, dynamic> map): 
        id = (map['id'] as num).toInt(),        
        name = map['name'] as String,        
        email = map['email'] as String,        
        phone = map['phone'] as String,        
        userAddress =Address.fromJson(map['userAddress']);
        
    Map<String, dynamic>toJson() {
        return {
            'id': id,
            'name': name,
            'email': email,
            'phone': phone,
            'userAddress': userAddress.toJson(),    
        };  
   }
   
   @override
   StringtoString() {
       return 'User:\n\tId: $id\n\tName: $name\n\tEmail: $email\n\tPhone: $phone\n\tAddress: ${userAddress.toString()}';  
   }
   
   @override
   List<Object> get props => [name, email, id, phone, userAddress];
}

class Address extends Equatable 
{
    const Address({
        required this.houseNo,
        required this.locality,
        required this.city,
        required this.state,  
    });
    
    final String houseNo;
    final String locality;
    final String city;
    final String state;
    
    Address.fromJson(Map<String, dynamic> map): 
        houseNo = map['houseNo'],        
        locality = map['locality'],        
        city = map['city'],        
        state = map['state'];
        
    Map<String, dynamic>toJson() {
        return {
            'houseNo': houseNo,
            'locality': locality,
            'city': city,
            'state': state,    
        };  
    }
    
    @override
    StringtoString() {
        return'Address: $houseNo, $locality, $city, $state\n';  
    }
    
    @override
    List<Object> get props => [houseNo, locality, city, state];
}

Add the Required Dependencies

Add the path_provider dependency in your pubspec.yaml file.

dependencies:
    path_provider: ^2.0.1

Create the FileHandler Class

We need to create a singleton class because we want only a single instance of the class throughout the application. Now let’s find the local path and create a reference to the file.

class FileHandler {
    // Makes this a singleton class, as we want only want a single
    // instance of this object for the whole application
    FileHandler._privateConstructor();
    static final FileHandler instance = FileHandler.            
                                            _privateConstructor();
}

Find the Correct Local Path and Create a Reference to the File Location

The path_provider plugin provides a platform-neutral way to access commonly used locations on the device’s file system. The plugin currently supports access to two file system locations:

  • Temporary Directory: A temporary directory (cache) that the system can clear at any time.

  • Documents Directory: A location to store files that only the app can access. The system clears the directory only when the app is deleted.

For persisting data, we store the file in the documents directory and create a reference to it. You can use the dart:io library to achieve this.

static File? _file;

static final _fileName ='user_file.txt';

// Get the data file
Future<File>get file async {
    if (_file !=null) return _file!;    
    _file = await_initFile();
    return _file!;  
}

// Inititalize file
Future<File>_initFile() async {
    final _directory = await getApplicationDocumentsDirectory();
    final _path = _directory.path;
    return File('$_path/$_fileName');  
}

Implement the Read, Write, Update, and Delete Methods

For reading and writing data models to file you need to encode data using the jsonEncode() method available in the dart:convert library. This converts the given Object to a string. Then for reading the file you can use the jsonDecode() method to convert the string to a JSON object. You can then retrieve the object from the map easily.

To write data to file, you need to maintain a Set of Users. When a newUseris added, first add it to this Set then use this to write to the file. Using Set ensures that an element is added only once in the file. Also, you need to extend your class with Equatable class available in theequatablepackage to compare instances of theUserclass. Equatable helps to implement value-based comparing of objects without explicitly overriding == and hashcode. You can check this article to learn more about Equatable.

You also need to implement the toJson() and fromJson() methods for the classes to convert the class objects to and from a JSON map. Check out this page from Flutter docs to know more about JSON serialization.

static Set<User> _userSet = {};

Future<void>writeUser(User user) async {
    finalFile fl = await file;    
    _userSet.add(user);
    
    // Now convert the set to a list as the jsonEncoder cannot encode
    // a set but a list.
    final _userListMap = _userSet.map((e) => e.toJson()).toList();
    
    await fl.writeAsString(jsonEncode(_userListMap));  
}

Now to read the file, use the readAsString() method, then using the jsonDecode() method convert the string to a JSON object (here, a list of Map<String, dynamic>).

Future<List<User>>readUsers() async {
    final File fl = await file;
    final _content = await fl.readAsString();
    
    final List<dynamic> _jsonData =jsonDecode(_content);
    final List<User> _users = _jsonData        
        .map(          
            (e) => User.fromJson(e as Map<String, dynamic>),        
        )        
        .toList();
    return _users;  
}

Similarly, implement the update() and delete() methods.

Future<void>deleteUser(User user) async {
    final File fl = await file;   
    
      _userSet.removeWhere((e) => e == user);
      final _userListMap = _userSet.map((e) => e.toJson()).toList();
      
      await fl.writeAsString(jsonEncode(_userListMap));  
}
Future<void>updateUser({
  required int id,
  required User updatedUser,  
  }) 
  async {    
    _userSet.removeWhere((e) => e.id == updatedUser.id);
    await writeUser(updatedUser);  
}

Now you are all set, just create an instance of the FileHandler class using FileHandler.instance and use the methods to read and write data to a file.


Conclusion

Yes, it’s that easy to handle data locally in Flutter by reading and writing a file! There are many ways to persist data locally in Flutter. The method to use depends on the type and scale of the data. As far as nested data models (like the one used above) are considered, the easiest way to store such type of data is by storing it in files.

However, there can still be some better methods out there, please leave a comment or reach out to me here if you know one!.

Check out the Persist data with SQLite and Store key-value data on disc cookbooks on handling data by the Flutter team for more info about other methods.

The repo for the above project can be found here. You can also check out theDatabaseHandlerclass in thedatabase_handler.dartfile to see how to persist data with SQLite.



Source: Better Programming


The Tech Platform

0 comments
bottom of page