Mobile ApplicationTechnology

Step-by-Step Guide to Implement Proto DataStore in Android App Development

Introduction of Pro DataStore

Proto DataStore is a library for storage purposes introduced by Android Jetpack. Using this, we can store medium-sized data like the recent searched locations list. 

We can use SharedPreferences for the same purpose, but it is not the main thread-safe and storing a list using it is not a good option.

Moreover, Kotlin coroutines and Flow make data storing, using Proto Datastore, asynchronous and consistent. It also provides transaction support.

Good to have knowledge of Proto DataStore

  • Dependency Injection Using Hilt(Basics)
  • Knowledge about MVVM(Basics)
  • Flow and Coroutine(Basics)
Tools Tech Stack Using Were Using

Why it is Important?

  • Proto DataStore is the main thread-safe storage option that makes the app smoother as it does not stop the main thread. 
  • It is the safest way to store data. We need to create our data structure for storing data using protocol buffers.
  • Uses Flow and Coroutines, which stores and reads data asynchronously. 
  • Accessible from UI thread as it uses Dispacher.IO underneath its implementation. 
  • It provides transaction support which was missing in SharedPreference. 

Features of Proto DataStore

  • Easy to integrate compared to Room or any other databases. 
  • Easy to store structured data, which is not possible using SharedPreference.
  • Provides a hinder-free experience to the user as it works on a separate thread.
  • Frequent changes in the data can be observed in a facile manner using Flow and Coroutines.
  • Protocol buffers get checked for data type mistakes during compilation. Therefore no type-safety errors can occur on run time.

How does it Work?

Add below code in build.gradle file which resides in the app folder and sync the project afterward. 

plugins {
   id "com.google.protobuf" version "0.8.17"

}
dependencies {
/*************************************************************
*  Proto Data Store
*************************************************************/
implementation  "androidx.datastore:datastore-core:1.0.0"
implementation  "com.google.protobuf:protobuf-javalite:3.18.0"

/*************************************************************
*  Preference Data Store(Excluding this might cause some errors.)
*************************************************************/
implementation "androidx.datastore:datastore-preferences:1.0.0"
}
protobuf {
   protoc {
       artifact = "com.google.protobuf:protoc:3.14.0"
   }
   generateProtoTasks {
       all().each { task ->
           task.builtins {
               java {
                   option 'lite'

               }
           }
       }
   }
}

2. Create .proto File at Shown Location

3. Making Data Structure in .proto File

You have to define your data structure in a .proto file. 

Here, Location is a model data class that contains information about a location. In this 1,2,3,4 are just unique numbers known as Field Numbers. Each variable should be assigned a unique field number. Try to allocate these field numbers between 1 to 15, as it takes only 1-byte memory. Field Number larger than 15 will take 2 bytes of memory space, so try to avoid it as long as possible.

Recent Locations is the class that contains the list of Location objects.

Provide your package name in the java_package parameter. All the generated classes will be stored in the package name provided by you.

Build the project when you are done changing the proto file. 

For more information on proto buffer, check out this link.

syntax = "proto3";

option java_package = "com.kotlin.mvvm.structure.datastore";//Replace it with your package.
option java_multiple_files = true;

// This is the Location class
message Location{
 int32 id = 1;  // Unique ID number for this location.
 string name = 2;
 double longitude=3;
 double latitude=4;
}

// This is object which will contain the list of recent locations
message RecentLocations {
 repeated Location location = 1;
}

4. Create a Hilt Module for Dependency Injection

You need to create a serializer for your proto class which will be used by Proto DataStore internally. This is boilerplate code which will be repeated every time you implement a Proto DataStore so just copy it and replace the class name with your class name.

Here, RecentLocationsSerializer is created for serializing the RecentLocations class. 

After defining the serializer, create the Proto DataStore using the “by dataStore” property delegate as shown in the code.

Now, provide this DataStore object using Hilt. For this demo, I have supplied this as a constructor parameter of DefaultDataRepostiory class.

@Module
@InstallIn(SingletonComponent::class)
class AppModule {

    /**
    * Some other code…
    */ 


   /**
    * For Proto Data Store
    */
   @Suppress("BlockingMethodInNonBlockingContext")
   object RecentLocationsSerializer : Serializer<RecentLocations> {
       override val defaultValue: RecentLocations = RecentLocations.getDefaultInstance()

       override suspend fun readFrom(input: InputStream): RecentLocations {
           try {
               return RecentLocations.parseFrom(input)
           } catch (exception: InvalidProtocolBufferException) {
               throw CorruptionException("Cannot read proto.", exception)
           } catch (e: java.io.IOException) {
               e.printStackTrace()
               throw e
           }
       }

       override suspend fun writeTo(t: RecentLocations, output: OutputStream) = t.writeTo(output)
   }

   private val Context.recentLocationsDataStore: DataStore<RecentLocations> by dataStore(
       fileName = "RecentLocations.pb",
       serializer = RecentLocationsSerializer
   )

   @Provides
   @Reusable
   fun provideProtoDataStore(@ApplicationContext context: Context) =
   	context.recentLocationsDataStore

   @Provides
   @Reusable
   internal fun providesDataRepository(
      @ApplicationContext context: Context,
      recentLocationsDataStore: DataStore<RecentLocations>
   ): DataRepository {
      return DefaultDataRepository(
          context,
          recentLocationsDataStore
      ) as DataRepository
   }
}

5. Create a Repository Interface

Making Repository Interface helps in unit testing as we can create a Fake Repository easily.

interface DataRepository {

   /**
    * This method is used for the purpose of fetching the recent locations items from the data store.
    */
   suspend fun getRecentLocations(): LiveData<List<Location>>

   /**
    * This method is used for the purpose of adding the recent locations items in the data store.
    */
   suspend fun addRecentLocations(locations: List<Location>)

   /**
    * This method is used for the purpose of clearing the recent locations items.
    */
   suspend fun clearAllRecentLocations()

}

6. Implement Repository

Here is the implementation for the repository interface we created before. 

To read data, dataStoreObject.data can be used as shown in getRecentLocations() implementation.
To add, update or clear data, we can use dataStoreObject.updateData which will provide us the DataStore class instance which we can manipulate as per our need. You can check out addRecentLocations() and clearAllRecentLocations() method implementations for knowing the usage of dataStoreObject.updateData method.

class DefaultDataRepository @Inject constructor(
   private val context: Context,
   private val recentLocationsDataStore: DataStore<RecentLocations>
) : DataRepository {


   override suspend fun getRecentLocations(): LiveData<List<Location>> {
       return recentLocationsDataStore.data.asLiveData()
           .switchMap { MutableLiveData(it.locationList) }
   }

   override suspend fun addRecentLocations(locations: List<Location>) {
       recentLocationsDataStore.updateData { recentLocations: RecentLocations ->
           recentLocations.toBuilder().addAllLocation(locations).build()
       }
   }

   override suspend fun clearAllRecentLocations() {
       recentLocationsDataStore.updateData {
           it.toBuilder().clear().build()
       }
   }
}

7. Creating Recyclerview Adapter and Raw xml layout to show Recent Locations

For showing the list of recent locations, create a list or recycler view adapter and raw layout.

proto datastore cta

8. Creating View Model

Finally, we will integrate all the created classes, repositories and adapters in the view model. 

@HiltViewModel
class ProtoViewModel @Inject constructor(
   private val dataRepository: DataRepository
) :ViewModel() {
   //Initializer
   init {
       //Getting all recent locations
       getAllRecentLocations()
   }

    /**
    * Some other code…
    */ 


   //Private variables
   private lateinit var _recentLocations: LiveData<List<Location>>

   //Live data variables
   val recentLocations: LiveData<List<Location>> = _recentLocations

   private fun getAllRecentLocations() = viewModelScope.launch {
       _recentLocations = dataRepository.getRecentLocations()
   }

   fun addRecentLocations(locations:List<Location>) = viewModelScope.launch {
       dataRepository.addRecentLocations(locations)
   }

   fun clearRecentLocations() = viewModelScope.launch {
       dataRepository.clearAllRecentLocations()
   }
}

9. Use the View Model in Your Activity/Fragment

Now, use the view model instance in your activity/fragment to perform DataStore operations.

@AndroidEntryPoint
class FragmentProtoDataStoreDemo : Fragment(R.layout.fragment_proto_data_store_demo) {
   /**
    * Proto View Model instance.
    */
   private val protoViewModel: ProtoViewModel by viewModels()

   /**
    * Variables for storing fake location data.
    */
   private var lastId = 0
   private var lastLatitude = 0.0
   private var lastLongitude = 0.0

    /**
    * Some other code…
    */ 


   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
       …
binding.fragmentProtoDataStorePlusFab.setOnClickListener {
           protoViewModel.addRecentLocations(
               listOf(
                   Location.newBuilder()
                       .setId(++lastId)
                       .setName("Location Number : $lastId")
                       .setLatitude(++lastLatitude)
                       .setLongitude(++lastLongitude)
                       .build()
               )
           )
      }
binding.fragmentProtoDataStoreClearFab.setOnClickListener{
           protoViewModel.clearRecentLocations()
      }
protoViewModel.recentLocations.observe(viewLifecycleOwner) {
               recentLocationsListAdapter.submitList(it.toMutableList())
      }

   }
}

After Running the App will Look Something like This:

On the click of the Add Floating Action button, a new location will be added and reflected.

On the click of the Clear Floating Action Button, all locations will be deleted and reflected.

Clear Floating Action Button

Challenges and Solutions in Pro DataStore Implementation

Challenges:

If you are following google code labs or any other sources then you might have added only two dependencies in the project build.gradle file. 

implementation  “androidx.datastore:datastore-core:1.0.0”

implementation  “com.google.protobuf:protobuf-javalite:3.18.0”

This might cause an “Unresolved reference: dataStore” issue as shown in the below image.

dataStore invalid ref

Solution:

This can be solved easily by adding the following dependency. Though it is the dependency for the preference data store, it solves the issue perfectly as shown in the below picture.

implementation “androidx.datastore:datastore-preferences:1.0.0”

dataStore found

Conclusion

Proto DataStore is a great way to store small to mid-range structured data in Kotlin Android Project. It can be implemented pretty fast and doesn’t require much change. Moreover, It provides a better user experience due to main-thread safety. 

If you are making an application and there is a need to store structured information, then you should consider using Proto DataStore.

proto datastore cta

FAQs for Proto DataStore:

1. What is Proto DataStore used for?

Proto DataStore is used for storing small to medium size data. It is one of the alternatives for SharedPreference provided by Google. 

2. Should we change the current SharedPreference Implementation and use this instead?

No. If you are already using SharedPreference in your application, and it works perfectly with it, then there is no need to implement this. Moreover, this only works for Kotlin projects. 

3. Why use this in place of Room or any other databases?

Suppose you have created an application without any database. Now, you need to store a list of some items. Using a database is not the best decision in this case as it will take too much time and effort. However, you have to change its version with each change. 

You can surely go for a database if there is a large set of data or multiple lists that you need to store.

4. When to use Preference DataStore and when Proto DataStore?

When there is key-value pair data, then using preference DataStore will be a better choice. For example, storing the username of the user. 

You can use Proto DataStore when there is data that cannot be stored using primitive data types.

lets start your project

Ankit Mishra

Ankit Mishra is an Android Developer at OneClick IT Consultancy Pvt Ltd. He is Passionate about to think forward & Android Ninja. Having hands on experience & skills to building, integrating, supporting and testing android applications for mobile and tablet devices on the android platform.

Related Articles

Back to top button