How to inject Kotlin extentsion properties using Dagger-Hilt - android

I'm trying to implement Proto Datastore using Kotlin serialization and Hilt.
Reference: https://medium.com/androiddevelopers/using-datastore-with-kotlin-serialization-6552502c5345
I couldn't inject DataStore object using the new DataStore creation syntax.
#InstallIn(SingletonComponent::class)
#Module
object DataStoreModule {
#ExperimentalSerializationApi
#Singleton
#Provides
fun provideDataStore(#ApplicationContext context: Context): DataStore<UserPreferences> {
val Context.dataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferencesSerializer
)
return dataStore
}
}
I get the lint message Local extension properties are not allowed
How can inject this Kotlin extension property? Or is there any way to inject dataStore object?

You can't use extensions in a local context, you should call this way:
#InstallIn(SingletonComponent::class)
#Module
object DataStoreModule {
#ExperimentalSerializationApi
#Singleton
#Provides
fun provideDataStore(#ApplicationContext context: Context): DataStore<UserPreferences> =
DataStoreFactory.create(
serializer = UserPreferencesSerializer,
produceFile = { context.dataStoreFile("user_pref.pb") },
)
}

Found a way to do this using Predefined qualifers in Hilt
Now no DataStoreModule class. I directly inject the application context into a Datastore Manager class. Below is the code.
#Singleton
class DataStoreManager #Inject constructor(
#ApplicationContext private val context: Context,
) {
private val Context.userPreferencesDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_pref.pb",
serializer = UserPreferencesSerializer
)
val userPreferencesFlow: Flow<UserPreferences> =
context.userPreferencesDataStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Timber.e("Error reading sort order preferences. $exception")
emit(UserPreferences())
} else {
throw exception
}
}
suspend fun updateUid(uid: String) {
context.userPreferencesDataStore.updateData { userPreferences ->
userPreferences.copy(uid = uid)
}
}
suspend fun getUid(): String {
return userPreferencesFlow.first().uid
}
}
This works like a charm.

Related

How should I structure my code if I am using Moshi to parse form a local JSON file in Kotlin for Android?

So I have a local json that is basically a survey object that contains a survey and some details/attributes(List, Header, Footer, ids etc) relating to the survey. I created a Data Class model using Moshi code gen adapters and it works but I want to know what is the best way to achieve this. Currently this is my thought process. I'm a bit confused on what should be in my DI Module vs what should my repository look like, do I need a DI concrete class, interface etc?
Here is where I am currently at
Hilt Module:
#Provides
#Singleton
fun provideModelAdapter(#ApplicationContext context: Context) : JsonAdapter<Model> {
val moshi: Moshi = Moshi.Builder().build()
return moshi.adapter(Model::class.java)
}
Then would my repository use a DAO interface? If so what would that look like or would it get the json model from in the repo?
Repository:
class Repository #Inject constructor(
#ApplicationContext private val context: Context,
private val adapter: JsonAdapter<ChltSurveyModel>
) {
suspend fun getModel() {
val json: String = context.readFromAssets(MODEL_FILENAME)
adapter.fromJson()
}
}
ViewModel:
#HiltViewModel
class ViewModel #lInject constructor(
private val repository: Repository
) : ViewModel() {
private val _model = MutableLiveData<Model>()
val model: LiveData<Model> = _model
init {
getModel()
}
fun getModel() = viewModelScope.launch {
try {
_model.value = repository.getModel()
} catch (e: JsonDataException) {
e.printStackTrace()
}
}
}
Util Class to Read JSON
fun Context.readFromAssets(filename: String): String = try {
val reader = BufferedReader(InputStreamReader(assets.open(filename)))
val sb = StringBuilder()
var line = reader.readLine()
while (line != null) {
sb.append(line)
line = reader.readLine()
}
reader.close()
sb.toString()
} catch (exp: Exception) {
println("Failed reading line from $filename -> ${exp.localizedMessage}")
""
}

Dependency injection using hilt on singleton private constructor class

I'm new to hilt. So i want to try dependency injection with hilt on my project which use MVVM architecture.
The structure look like this: JsonHelper -> RemoteDataSource -> Repository -> ViewModel.
The problems occur when i try to inject my DI on RemoteDataSource and Repository since these classes are singleton class and have a private constructor.
The error codes look like this
..location\RemoteDataSource.java:40: error: Dagger does not support injection into Kotlin objects
public static final class Companion {
..location\Repository.java:30: error: Dagger does not support injection into Kotlin objects
public static final class Companion {
And these are my RemoteDataSource and Repository codes, i have tried injecting it on the constructor but it says Dagger can't inject on private constructors so then i tried to inject it on the function but still didn't work
RemoteDataSource.kt
#Singleton
class RemoteDataSource private constructor(private val jsonHelper: JsonHelper) {
companion object {
#Volatile
private var instance: RemoteDataSource? = null
#Inject
fun getInstance(jsonHelper: JsonHelper): RemoteDataSource =
instance ?: synchronized(this) {
instance ?: RemoteDataSource(jsonHelper).apply { instance = this }
}
}
fun getAllRemoteMovies(moviesCallback: LoadMoviesCallback) {
moviesCallback.onAllMoviesReceived(jsonHelper.loadRemoteMovies())
}
fun getAllRemoteTVShows(tvshowCallback: LoadTVShowCallback) {
tvshowCallback.onAllTVShowsReceived(jsonHelper.loadRemoteTVShows())
}
interface LoadMoviesCallback {
fun onAllMoviesReceived(moviesResponses: ArrayList<MovieItem>)
}
interface LoadTVShowCallback {
fun onAllTVShowsReceived(tvshowResponses: ArrayList<TVShowItem>)
}
}
Repository.kt
#Singleton
class Repository private constructor(private val remoteDataSource: RemoteDataSource) : DataSource {
companion object {
#Volatile
private var instance: Repository? = null
#Inject
fun getInstance(remoteDataSource: RemoteDataSource): Repository =
instance ?: synchronized(this) {
instance ?: Repository(remoteDataSource).apply { instance = this }
}
}
override fun getAllRemoteMovies(): LiveData<ArrayList<MovieItem>> {
val remoteMoviesResult = MutableLiveData<ArrayList<MovieItem>>()
remoteDataSource.getAllRemoteMovies(object : RemoteDataSource.LoadMoviesCallback {
override fun onAllMoviesReceived(moviesResponses: ArrayList<MovieItem>) {
remoteMoviesResult.value = moviesResponses
}
})
return remoteMoviesResult
}
override fun getAllRemoteTVShows(): LiveData<ArrayList<TVShowItem>> {
val remoteTVShowsResult = MutableLiveData<ArrayList<TVShowItem>>()
remoteDataSource.getAllRemoteTVShows(object : RemoteDataSource.LoadTVShowCallback {
override fun onAllTVShowsReceived(tvshowResponses: ArrayList<TVShowItem>) {
remoteTVShowsResult.value = tvshowResponses
}
})
return remoteTVShowsResult
}
}
And this is my injection module
RemoteDataSourceModule.kt
#Module
#InstallIn(ActivityComponent::class)
object RemoteDataSourceModule {
#Singleton
#Provides
fun provideJsonHelper(context: Context): JsonHelper {
return JsonHelper(context)
}
#Singleton
#Provides
fun provideRemoteDataSource(jsonHelper: JsonHelper): RemoteDataSource {
return RemoteDataSource.getInstance(jsonHelper)
}
#Singleton
#Provides
fun provideRepository(remoteDataSource: RemoteDataSource): Repository {
return Repository.getInstance(remoteDataSource)
}
}
So how can i solve this problem without changing the class constructor to public?
#Singleton annotation is enough to notify that the class is a singleton class, so i just remove the companion object and changes private constructor with a public constructor so the code will look like this:
#Singleton
class RemoteDataSource #Inject constructor(private val jsonHelper: JsonHelper) {
// Your codes
}

error: [Dagger/MissingBinding] *.AuthRepository cannot be provided without an #Provides-annotated method

I tried to create a registration using MVVM + Repository pattern with DI, and I used #ViewModelInject and everything was OK, but now #ViewModelInject is deprecated and I changed #ViewModelInject to #HiltViewModel + #Inject constructor() and faced with the error: [Dagger/MissingBinding] *.AuthRepository cannot be provided without an #Provides-annotated method. I tried to add a #Provides annotation for the register function in the interface but faced with another error
Execution failed for task ':app:kaptDebugKotlin'.
A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
java.lang.reflect.InvocationTargetException (no error message)
AuthViewModel
#HiltViewModel
class AuthViewModel #Inject constructor(
private val repository: AuthRepository,
private val applicationContext: Context,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main
) : ViewModel() {
private val _registerStatus = MutableLiveData<Event<Resource<AuthResult>>>()
val registerStatus: LiveData<Event<Resource<AuthResult>>> = _registerStatus
private val _loginStatus = MutableLiveData<Event<Resource<AuthResult>>>()
val loginStatus: LiveData<Event<Resource<AuthResult>>> = _loginStatus
fun login(email: String, password: String) {
if(email.isEmpty() || password.isEmpty()) {
val error = applicationContext.getString(R.string.error_input_empty)
_loginStatus.postValue(Event(Resource.Error(error)))
} else {
_loginStatus.postValue(Event(Resource.Loading()))
viewModelScope.launch(dispatcher) {
val result = repository.login(email, password)
_loginStatus.postValue(Event(result))
}
}
}
fun register(email: String, username: String, password: String, repeatedPassword: String) {
val error = if(email.isEmpty() || username.isEmpty() || password.isEmpty()) {
applicationContext.getString(R.string.error_input_empty)
} else if(password != repeatedPassword) {
applicationContext.getString(R.string.error_incorrectly_repeated_password)
} else if(username.length < MIN_USERNAME_LENGTH) {
applicationContext.getString(R.string.error_username_too_short, MIN_USERNAME_LENGTH)
} else if(username.length > MAX_USERNAME_LENGTH) {
applicationContext.getString(R.string.error_username_too_long, MAX_USERNAME_LENGTH)
} else if(password.length < MIN_PASSWORD_LENGTH) {
applicationContext.getString(R.string.error_password_too_short, MIN_PASSWORD_LENGTH)
} else if(!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
applicationContext.getString(R.string.error_not_a_valid_email)
} else null
error?.let {
_registerStatus.postValue(Event(Resource.Error(it)))
return
}
_registerStatus.postValue(Event(Resource.Loading()))
viewModelScope.launch(dispatcher) {
val result = repository.register(email, username, password)
_registerStatus.postValue(Event(result))
}
}
}
AppModule
#Module
#InstallIn(SingletonComponent::class)
object AppModule {
#Singleton
#Provides
fun provideMainDispatcher() = Dispatchers.Main as CoroutineDispatcher
#Singleton
#Provides
fun provideApplicationContext(#ApplicationContext context: Context) = context
#Singleton
#Provides
fun provideGlideInstance(#ApplicationContext context: Context) =
Glide.with(context).setDefaultRequestOptions(
RequestOptions()
.placeholder(R.drawable.ic_image)
.error(R.drawable.ic_error)
.diskCacheStrategy(DiskCacheStrategy.DATA)
)
}
AuthModule
#Module
#InstallIn(ActivityComponent::class)
object AuthModule {
#ActivityScoped
#Provides
fun provideAuthRepository() = DefaultAuthRepository() as AuthRepository
}
AuthRepository
interface AuthRepository {
suspend fun register(email: String, username: String, password: String): Resource<AuthResult>
suspend fun login(email: String, password: String): Resource<AuthResult>
}
DefaultAuthRepository
class DefaultAuthRepository : AuthRepository {
val auth = FirebaseAuth.getInstance()
val users = FirebaseFirestore.getInstance().collection("users")
override suspend fun register(
email: String,
username: String,
password: String
): Resource<AuthResult> {
return withContext(Dispatchers.IO) {
safeCall {
val result = auth.createUserWithEmailAndPassword(email, password).await()
val uid = result.user?.uid!!
val user = User(uid, username)
users.document(uid).set(user).await()
Resource.Success(result)
}
}
}
override suspend fun login(email: String, password: String): Resource<AuthResult> {
TODO("Not yet implemented")
}
}
//Dagger - Hilt
implementation 'com.google.dagger:hilt-android:2.31.2-alpha'
kapt 'com.google.dagger:hilt-android-compiler:2.31.2-alpha'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha03'
enter image description here
#Module
#InstallIn(ActivityComponent::class)
abstract class AuthModule{
#Binds
abstract fun bindAuthRepository(impl: DefaultAuthRepository): AuthRepository
}
With new hilt version lots of stuff has been changed.
You also have to upgrade your hilt android, hilt compiler and hilt gradle plugin to:2.31-alpha
I made mock sample exactly the way you did i had same issue, after going through hilt's docs i found new way to inject dependencies to viewModels, you have to make separate module for dependencies which are going to inject in the viewModel with special component called ViewModelComponent:
#Module
#InstallIn(ViewModelComponent::class) // this is new
object RepositoryModule{
#Provides
#ViewModelScoped // this is new
fun providesRepo(): ReposiotryIMPL { // this is just fake repository
return ReposiotryIMPL()
}
}
here is what docs says about ViewModelComponent and ViewModelScoped
All Hilt View Models are provided by the ViewModelComponent which follows the same lifecycle as a ViewModel, i.e. it survives configuration changes. To scope a dependency to a ViewModel use the #ViewModelScoped annotation.
A #ViewModelScoped type will make it so that a single instance of the scoped type is provided across all dependencies injected into the Hilt View Model.
link: https://dagger.dev/hilt/view-model.html
then your viewModel:
#HiltViewModel
class RepoViewModel #Inject constructor(
application: Application,
private val reposiotryIMPL: ReposiotryIMPL
) : AndroidViewModel(application) {}


How to Inject Moshi/Gson in Room TypeConvertors using Hilt?

I am trying out hilt and i want to inject moshi for serializing and deserializing.
Here's a code sample from a github Repo which is not using di:
open class InfoTypeConverter {
private val moshi = Moshi.Builder().build() //not using dependency injection
#TypeConverter
fun fromString(value: String): PokemonInfo.Type? {
val adapter: JsonAdapter<PokemonInfo.Type> = moshi.adapter(PokemonInfo.Type::class.java)
return adapter.fromJson(value)
}
#TypeConverter
fun fromInfoType(type: PokemonInfo.Type): String {
val adapter: JsonAdapter<PokemonInfo.Type> = moshi.adapter(PokemonInfo.Type::class.java)
return adapter.toJson(type)
}
}
I am Trying out random stuff to field inject this like annotaion with #AndroidEntryPoint/#EntryPoint and obviously it's not working.
Including Moshi in the Hilt dependency graph is as simple as adding this class:
#Module
#InstallIn(ApplicationComponent::class)
object DataModule {
#Singleton
#Provides
fun provideMoshi(): Moshi {
return Moshi.Builder().build()
}
}
#TypeConverters are for the Room database. If you want to use the Moshi from Hilt for them, you will have to try a bit. One way of doing this is:
Put #TypeConverter functions in a static context (object declaration) with an initializer
object InfoTypeConverter {
private lateinit var moshi: Moshi
fun initialize(moshi: Moshi){
this.moshi = moshi
}
#TypeConverter
fun fromString(value: String): PokemonInfo.Type? {
val adapter: JsonAdapter<PokemonInfo.Type> = moshi.adapter(PokemonInfo.Type::class.java)
return adapter.fromJson(value)
}
#TypeConverter
fun fromInfoType(type: PokemonInfo.Type): String {
val adapter: JsonAdapter<PokemonInfo.Type> = moshi.adapter(PokemonInfo.Type::class.java)
return adapter.toJson(type)
}
}
Initialize InfoTypeConverter before creating your RoomDatabase (here using the same module for the purpose):
#Module
#InstallIn(ApplicationComponent::class)
object DataModule {
#Singleton
#Provides
fun provideMoshi(): Moshi {
return Moshi.Builder().build()
}
#Singleton
#Provides
fun provideRoomDatabase(moshi: Moshi): YourDatabase {
InfoTypeConverter.initialize(moshi)
val yourDatabase: YourDatabase = /* create your room database here */
return yourDatabase
}
}

Using room as singleton in kotlin

I'm trying to use Room as singleton so I didn't have to invoke Room.databaseBuilder() -which is expensive- more than once.
#Database(entities = arrayOf(
Price::class,
StationOrder::class,
TicketPrice::class,
Train::class,
TrainCategory::class
), version = 2)
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun dao(): TrainDao
companion object {
fun createDatabase(context: Context): AppDatabase
= Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
}
}
Note:
Can't use Object because Room requires using abstract class.
singleton must be thread safe because multiple threads might access it at the same time.
must be able to take Context as an argument.
I have looked at all similar StackOverflow questions and none of them satisfy my requirements
Singleton with argument in Kotlin isn't thread-safe
Kotlin - Best way to convert Singleton DatabaseController in Android isn't thread-safe
Kotlin thread save native lazy singleton with parameter uses object
After some research, I found that I have two options.
Double-checked locking
Initialization-on-demand holder idiom
I considered implementing one of them, but this didn't felt right for Kotlin - too much boilerplate code.
After more research, I stumbled upon this great article which provides an excellent solution, which uses Double-checked locking but in an elegant way.
companion object : SingletonHolder<AppDatabase, Context>({
Room.databaseBuilder(it.applicationContext, AppDatabase::class.java, "train.db").build()
})
From the article:
A reusable Kotlin implementation:
We can encapsulate the logic to
lazily create and initialize a singleton with argument inside a
SingletonHolder class. In order to make that logic thread-safe, we
need to implement a synchronized algorithm and the most efficient
one — which is also the hardest to get right — is the double-checked
locking algorithm.
open class SingletonHolder<T, A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
#Volatile private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
Extra:
if you want Singleton with two arguments
open class SingletonHolder2<out T, in A, in B>(creator: (A, B) -> T) {
private var creator: ((A, B) -> T)? = creator
#Volatile private var instance: T? = null
fun getInstance(arg0: A, arg1: B): T {
val i = instance
if (i != null) return i
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg0, arg1)
instance = created
creator = null
created
}
}
}
}
In this particular case I would resort to using Dagger 2, or some other dependency injection library like Koin or Toothpick. All three libraries allow to provide dependancies as singletons.
Here's the code for Dagger 2 module:
#Module
class AppModule constructor(private val context: Context) {
#Provides
#Singleton
fun providesDatabase(): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"train.db")
.build()
}
}
AppComponent:
#Singleton
#Component(modules = arrayOf(
AppModule::class
))
interface AppComponent {
fun inject(viewModel: YourViewModel)
fun inject(repository: YourRepository)
}
Application class to provide injection:
class App : Application() {
companion object {
private lateinit var appComponent: AppComponent
val component: AppComponent get() = appComponent
}
override fun onCreate() {
super.onCreate()
initializeDagger()
}
private fun initializeDagger() {
component = DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
}
}
And then inject your database as singleton to wherever you need it (for example in your app's repository):
#Inject lateinit var appDatabase: AppDatabase
init {
App.component.inject(this)
}
Used #Volatile for thread safety.
public abstract class AppDatabase : RoomDatabase() {
abstract fun trainDao(): trainDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): Db = INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase ::class.java,
"train-db"
).build()
INSTANCE = instance
instance
}
}
}
taken from : https://developer.android.com/codelabs/android-room-with-a-view-kotlin#7
You could make use of the Kotlin standard library's
fun <T> lazy(LazyThreadSafetyMode.SYNCHRONIZED, initializer: () -> T): Lazy<T>
companion object {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "trains.db").build()
}
fun getDatabase(context: Context): AppDatabase {
this.context = context.applicationContext
return database
}
}
Personally though, I would normally add ApplicationContext-dependent singletons inside the Application, e.g.
<!-- AndroidManifest.xml -->
<manifest>
<application android:name="MyApplication">
...
class MyApplication : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(this, AppDatabase::class.java, "train.db").build()
}
}
You can even define an extension method for easy access as context.database.
val Context.database
get() =
generateSequence(applicationContext) {
(it as? ContextWrapper)?.baseContext
}.filterIsInstance<MyApplication>().first().database
Here's how i figured out...
#Database(entities = [MyEntity::class], version = dbVersion, exportSchema = true)
abstract class AppDB : RoomDatabase() {
// First create a companion object with getInstance method
companion object {
fun getInstance(context: Context): AppDB =
Room.databaseBuilder(context.applicationContext, AppDB::class.java, dbName).build()
}
abstract fun getMyEntityDao(): MyEntityDao
}
// This is the Singleton class that holds the AppDB instance
// which make the AppDB singleton indirectly
// Get the AppDB instance via AppDBProvider through out the app
object AppDBProvider {
private var AppDB: AppDB? = null
fun getInstance(context: Context): AppDB {
if (appDB == null) {
appDB = AppDB.getInstance(context)
}
return appDB!!
}
}
singleton in kotlin is real easy just do this
companion object {
#JvmStatic
val DATABASE_NAME = "DataBase"
#JvmField
val database = Room.databaseBuilder(App.context(), DataBase::class.java, DataBase.DATABASE_NAME).build()
}

Categories

Resources