In the following posts from our Jetpack DataStore series, we will cover several additional concepts to understand how DataStore interacts with other APIs, so that you’ll have everything at your disposal to use in a production environment. In this post specifically, we will be focusing on dependency injection with Hilt. We will be referring to the DataStore Preferences and Proto codelabs throughout this post for code samples.
Dependency injection with Hilt
In these codelabs, we interacted with our DataStore instances through the Preferences and Proto delegate construction once at the top level of a Kotlin file. We would then use the same instance throughout your application, as a singleton:
From our partners:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ // Preferences DataStore private val Context.dataStore by preferencesDataStore(name = USER_PREFERENCES_NAME) // Proto DataStore private val Context.userPreferencesStore by dataStore( fileName = DATA_STORE_FILE_NAME, serializer = UserPreferencesSerializer )
We require a singleton because creating more than one instance of DataStore for one given file can break all DataStore functionality. However, in a production environment we would usually obtain the DataStore instance via dependency injection. So let’s take a look at how DataStore works with Hilt, a dependency injection library that will help us reduce the boilerplate of doing manual dependency management. If you’re not familiar with Hilt, we encourage you to first go through the Using Hilt in your Android app codelab to learn the basic concepts such as Components
, Modules
and others.
Hilt setup
First, make sure you’ve added all the necessary setup steps to enable Hilt:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ // Root build.gradle dependencies { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } // Module build.gradle: plugins { id 'kotlin-kapt' id 'dagger.hilt.android.plugin' } dependencies { implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" }
All apps that use Hilt must contain an Application
class that is annotated with @HiltAndroidApp
to trigger Hilt’s code generation, including a base class for your application that serves as the application-level dependency container. In our case, we will create a simple TasksApp
and add it to our AndroidManifest.xml
:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @HiltAndroidApp class TasksApp : Application() // AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.codelab.android.datastore"> <application android:name=".TasksApp"/> </manifest>
Injecting Preferences DataStore
To obtain a Preferences DataStore instance through injection, we need to tell Hilt how to properly create it. We use the PreferenceDataStoreFactory
and add it to a Hilt module:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ private const val USER_PREFERENCES = "user_preferences" @InstallIn(SingletonComponent::class) @Module object DataStoreModule { @Singleton @Provides fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> { return PreferenceDataStoreFactory.create( corruptionHandler = ReplaceFileCorruptionHandler( produceNewData = { emptyPreferences() } ), migrations = listOf(SharedPreferencesMigration(appContext,USER_PREFERENCES)), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES) } ) } }
We’ve mentioned these parameters previously in the series, but let’s quickly recap:
corruptionHandler
(optional) — invoked if aCorruptionException
is thrown by the serializer when the data cannot be de-serialized which instructs DataStore how to replace the corrupted datamigrations
(optional) — a list ofDataMigration
for moving previous data into DataStorescope
(optional) — the scope in which IO operations and transform functions will execute; in this case, we’re reusing the same scope as the DataStore API default oneproduceFile
— generates theFile
object for Preferences DataStore based on the providedContext
andname
, stored inthis.applicationContext.filesDir
+datastore/
subdirectory
Now that we have a module to instruct Hilt on how to create our DataStore, we need to make a few more adjustments to be able to build successfully.
Hilt can provide dependencies to other Android classes that have the @AndroidEntryPoint
annotation, along with @HiltAndroidApp
for Application
and @HiltViewModel
for ViewModel
classes. If you annotate an Android class with @AndroidEntryPoint
, then you also must annotate other Android classes that depend on it. For example, if you annotate a Fragment
, then you must also annotate any activities where you use that Fragment
.
That means we need to add the following:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @AndroidEntryPoint class TasksActivity : AppCompatActivity() @HiltViewModel class TasksViewModel
In this sample, we don’t have any other complex injections, such as custom builders, factories, or interface implementations. So we can rely on Hilt and constructor injection for any other dependencies that we need to pass. We will use the @Inject
annotation in the constructor of a class to instruct Hilt how to provide its instances:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @HiltViewModel class TasksViewModel @Inject constructor( repository: TasksRepository, private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { class UserPreferencesRepository @Inject constructor( private val dataStore: DataStore<Preferences> ) class TasksRepository @Inject constructor()
Finally, let’s completely remove the preferencesDataStore
delegate from the top of our TasksActivity
as we don’t need it anymore. We will also change how our viewModel
is provided in TaskActivity
onCreate
, as its dependencies will now be injected:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ viewModel = ViewModelProvider(this).get(TasksViewModel::class.java)
Injecting Proto DataStore
Proto DataStore would follow a very similar pattern — you’d additionally need to provide a UserPreferencesSerializer
and a precise instruction on how to migrate from SharedPreferences
, which we’ve covered in more detail in the Proto DataStore post:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ private const val USER_PREFERENCES_NAME = "user_preferences" private const val DATA_STORE_FILE_NAME = "user_prefs.pb" @InstallIn(SingletonComponent::class) @Module object DataStoreModule { @Singleton @Provides fun provideProtoDataStore(@ApplicationContext appContext: Context): DataStore<UserPreferences> { return DataStoreFactory.create( serializer = UserPreferencesSerializer, produceFile = { appContext.dataStoreFile(DATA_STORE_FILE_NAME) }, corruptionHandler = null, migrations = listOf( SharedPreferencesMigration( appContext, USER_PREFERENCES_NAME ) scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) ) } }
That’s it! Now you’ll be able to run the app and verify that all the dependencies are now being injected properly.
To be continued
We’ve covered steps on how to inject DataStore with Hilt for better dependency management, setting up Hilt, using the PreferenceDataStoreFactory
in our DataStoreModule
to instruct Hilt on how to provide our DataStore instance correctly, and finally, providing it to classes that require it.
Join us for the next post of our series where we will be looking into how to use Data Store with Kotlin Serialization.
By Simona Stojanovic
Source Medium
For enquiries, product placements, sponsorships, and collaborations, connect with us at [email protected]. We'd love to hear from you!
Our humans need coffee too! Your support is highly appreciated, thank you!