Hilt injection in Android Services - android

I want to inject a class in Service. Lets have a look at the code below:
class DeviceUtil #Inject constructor() {
...
}
#AndroidEntryPoint
class LocationUpdateService : Service() {
#Inject
lateinit var deviceUtil: DeviceUtil
...
}
#Inject lateinit var deviceUtil: DeviceUtil is working fine in Activity but not working in Service.
Its giving the error: kotlin.UninitializedPropertyAccessException: lateinit property deviceUtil has not been initialized

For those dummies like me. As said by OP in the comments, a full example on how you can inject object in your service like so:
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import com.yourapp.services.UserService
#AndroidEntryPoint
class MyService: Service() {
#Inject lateinit var userService: UserService
override fun onCreate() {
super.onCreate()
userService.getUserList()
.subscribe { userList -> Log.d("tag", "users: $userList") }
}
override fun onBind(intent: Intent?): IBinder? {
return object: Binder() {
// ...
}
}
}
As for the service you're injecting make sure it has the #Inject annotation in its constructor like so:
class UserService #Inject() constructor() {
// ...
}

Related

Can abstract ViewModel class can be injected by HIlt?

I have a BaseViewModel class.
abstract class BaseViewModel : ViewModel() {
abstract fun sendLog(msg: String)
}
And now I need to inherit this in all the ViewModels. It's a lot of work.
I just want to implement once and use it in other ViewModels too.
So, Can I do this like this?
#HiltViewModel
abstract class BaseViewModel : ViewModel() {
#Inject construct(logger: Logger)
open fun sendLog(msg: String){
viewModelScope.launch{
logger.callApi(msg)
}
}
}
or is there any better way???
I assume that you have only one instance of Logger, so that you can get it lazyly. With a bit of Kotlins syntactic sugar and the ability to set a body for interface methods you can do something like this:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class Logger {
fun log(text: String) = println(text)
}
interface Foo {
fun ViewModel.logSomething(msg: String) {
viewModelScope.launch {
logger.log(msg)
}
}
companion object {
#Inject
lateinit var logger: Logger
}
}
#HiltViewModel
open class BaseViewModel : ViewModel(), Foo
class MainViewModel : BaseViewModel() {
init {
logSomething("That's a test")
}
}
If that doesn't fit your needs feel free to comment below

LiveData (backed by Flow) in Test is reflecting old value

I am trying to write a test for my View Model that verifies when I call setFirstTime, the state of the view model contains the updated value for firstTime set to false.
The UserPreferencesRepository provides a Flow of the preferences to the viewmodel, which exposes them as LiveData (using asLiveData extension).
Here is my test I am having trouble with:
MainViewModelTest.kt
package com.example.fitness.main
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.fitness.MainCoroutineRule
import com.example.fitness.data.UserPreferencesRepository
import com.example.fitness.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class MainViewModelTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#get:Rule
#ExperimentalCoroutinesApi
var mainCoroutineRule = MainCoroutineRule()
private lateinit var mainViewModel: MainViewModel
#Inject
lateinit var userPreferencesRepository: UserPreferencesRepository
#Before
#ExperimentalCoroutinesApi
fun init() {
hiltRule.inject()
// Execute all pending coroutine actions in MainViewModel initialization
mainCoroutineRule.runBlockingTest {
mainViewModel = MainViewModel(userPreferencesRepository)
}
}
#ExperimentalCoroutinesApi
#Test
fun `#setFirstTime marks the user as have opened the app at least once`() {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainCoroutineRule.runBlockingTest {
mainViewModel.setFirstTime()
}
# Failing assertion. Comes back as `true` when I expect it to be `false`
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
MainViewModel.kt
package com.example.fitness.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.example.fitness.data.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
#HiltViewModel
class MainViewModel #Inject constructor(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
val state = userPreferencesRepository.userPreferencesFlow.asLiveData()
/**
* Persists a value signifying that the user has started the app before.
*/
fun setFirstTime() {
viewModelScope.launch {
userPreferencesRepository.updateFirstTime(false)
}
}
}
UserPreferencesRepository
package com.example.fitness.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
data class UserPreferences(
val firstTime: Boolean
)
class UserPreferencesRepository #Inject constructor(private val dataStore: DataStore<Preferences>) {
private object PreferencesKeys {
val FIRST_TIME = booleanPreferencesKey("first_time")
}
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val firstTime = preferences[PreferencesKeys.FIRST_TIME] ?: true
UserPreferences(firstTime)
}
suspend fun updateFirstTime(firstTime: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.FIRST_TIME] = firstTime
}
}
}
I verified via the debugger that the body of the dataStore.edit code is being run prior to the last assertion of the test. I also noticed that the body of dataStore.data.map is also being run after the update, with the correctly populated preferences set to false. It appears that running the test in debug mode and quickly stepping through my break points results in a passing test, but running the test normally produces a failure, which leads me to believe there is some race condition present.
I am basing my work off of a Google Codelab. Any help would be greatly appreciated.
I managed to determine what the issue was. When I am creating my DataStore in the app, I am using the default coroutine scope, which is Dispatchers.IO. In my tests, I was replacing the main coroutine with kotlinx.coroutines.test.TestCoroutineDispatcher, but I needed to somehow instantiate the DataStore with a TestCoroutineScope as well, so that those saving actions would run synchronously.
Taking a lot of liberties from this extremely helpful article, my final code looks like:
MainViewModelTest.kt
#RunWith(AndroidJUnit4::class)
class MainViewModelTest : DataStoreTest() {
private lateinit var mainViewModel: MainViewModel
#Before
fun init() = runBlockingTest {
val userPreferencesRepository = UserPreferencesRepository(dataStore)
mainViewModel = MainViewModel(userPreferencesRepository)
}
#Test
fun `#setFirstTime marks the user as having opened the app at least once`() = runBlockingTest {
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(true))
mainViewModel.setFirstTime()
assertThat(mainViewModel.state.getOrAwaitValue().firstTime, `is`(false))
}
}
DataStoreTest.kt
abstract class DataStoreTest : CoroutineTest() {
private lateinit var preferencesScope: CoroutineScope
protected lateinit var dataStore: DataStore<Preferences>
#Before
fun createDatastore() {
preferencesScope = CoroutineScope(testDispatcher + Job())
dataStore = PreferenceDataStoreFactory.create(scope = preferencesScope) {
InstrumentationRegistry.getInstrumentation().targetContext.preferencesDataStoreFile(
"test-preferences-file"
)
}
}
#After
fun removeDatastore() {
File(
ApplicationProvider.getApplicationContext<Context>().filesDir,
"datastore"
).deleteRecursively()
preferencesScope.cancel()
}
}
CoroutineTest.kt
abstract class CoroutineTest {
#Rule
#JvmField
val rule = InstantTaskExecutorRule()
protected val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
#Before
fun setupViewModelScope() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanupViewModelScope() {
Dispatchers.resetMain()
}
#After
fun cleanupCoroutines() {
testDispatcher.cleanupTestCoroutines()
testDispatcher.resumeDispatcher()
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest(block)
}

Getting error in hilt HiltViewModel annotated class should contain exactly one #Inject annotated constructor

Error: How to resolve this, getting this wiered error even though I am not doing any inject in view model
/Users/user/Documents/Personal/android-in-app-review-engine/Application/app/build/tmp/kapt3/stubs/debug/com/inappreview/code/MainActivityViewModel.java:7: error: [Hilt]
public final class MainActivityViewModel extends androidx.lifecycle.ViewModel {
^
#HiltViewModel annotated class should contain exactly one #Inject annotated constructor.
[Hilt] Processing did not complete. See error above for details.
MainActivity.kt
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), InAppReviewView {
#Inject
lateinit var inAppReviewManager: InAppReviewManager
private val viewModel : MainActivityViewModel by viewModels()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.setInAppReviewView(this)
setOnClickListener()
}
private fun setOnClickListener() {
binding.startReviewProcess.setOnClickListener {
viewModel.startReview()
}
}
override fun showReviewFlow() {
val dialog = InAppReviewPromptDialog()
dialog.show(supportFragmentManager, null)
}
}
MainActivityViewModel.kt
#HiltViewModel
class MainActivityViewModel : ViewModel() {
private lateinit var inAppReviewView: InAppReviewView
/**
* Sets an interface that backs up the In App Review prompts.
* */
fun setInAppReviewView(inAppReviewView: InAppReviewView) {
this.inAppReviewView = inAppReviewView
}
/**
* Start In App Review
* */
fun startReview() {
inAppReviewView.showReviewFlow()
}
}
App.kt
#HiltAndroidApp
class App : Application()
Since you are not injecting anything remove the #HiltViewModel, you only need that when you want to inject something into your ViewModel

Cannot create an instance of class ViewModel Caused by: java.lang.InstantiationException: java.lang.Class .ViewModel has no zero argument constructor

Here is my ViewModel class
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ViewModel (private val context: Context) : ViewModel() {
private var leadslist = MutableLiveData<Response>()
init {
val Repository: Repository by lazy {
Repository
}
leadslist = Repository.getMutableLiveData(context)
}
fun getLeadsList(): MutableLiveData<Response> {
return leadslist
}
}
Here is how I am calling it in my Fragment.
viewmodel = ViewModelProvider(requireActivity()).get(ViewModel::class.java)
I know there are other answers already but nothing seems to work. Please help. Stuck on this for a pretty long time.
I have tried ViewModelFactory too.
ViewModelProvider(requireActivity(),ViewModelFactory(requireActivity())).get(ViewModel::class.java)
and used this code for ViewModelFactory class
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class ViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ViewModel(context) as T
}
}
still, it doesn't work.
Then I tried this
viewmodel= ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
.create(ViewModel::class.java)
and it didn't work again.
And how do create this zero-argument constructor in my ViewModel.
And why can't I create instance.
Also, with one of the approach I was able to compile and run but in that the viewmodel.observe function wasn't executing.
Please help. Thanks.
The parameter context must be initialized.
like:
class ViewModel (private val context: Context = requireActivity().application) : ViewModel()

error: #Component.Builder is missing setters for required modules or components in Dagger 2 instead of having setter method

I am new to dagger 2. I was making a CarComponent on kotlin, I was trying to call my DaggerCarComponent with horsePower value without calling petrolEngineModule. the following is my code :
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
#Component (
modules = [WheelModule::class, PetrolEngineModule::class]
)
interface CarComponent {
fun getCar(): Car
fun inject(mainActivity: MainActivity)
#Component.Builder
interface Builder {
#BindsInstance
fun horsePower(horsePower : Int) : Builder
fun build(): CarComponent
}
}
this is PetrolEngine.kt:
package com.example.daggerapp
import android.util.Log
import javax.inject.Inject
class PetrolEngine : Engine {
private var horsePower : Int
#Inject constructor(horsePower: Int){
this.horsePower = horsePower
}
override fun start() {
Log.d("Engine", "Broom..., horsePower: ${this.horsePower}")
}
}
this is PetrolEngineModule.kt:
package com.example.daggerapp
import dagger.Module
import dagger.Provides
import javax.inject.Inject
#Module
class PetrolEngineModule {
private var horsePower: Int
#Inject constructor(horsePower: Int) {
this.horsePower = horsePower
}
#Provides
fun provideHorsePower(): Int {
return horsePower
}
#Provides
fun provideEngine(engine: PetrolEngine): Engine
{
return engine
}
}
I added the DaggerComponent here as DaggerCarComponent :
package com.example.daggerapp
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import javax.inject.Inject
class MainActivity : AppCompatActivity() {
#Inject
lateinit var car:Car
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val daggerCar: CarComponent = DaggerCarComponent.builder().petrolEngineModule(PetrolEngineModule(140)).build()
daggerCar.inject(this)
Log.d("Car instance", "$car")
car.drive()
}
}
I was following this tutorial: https://www.youtube.com/watch?v=3tIvekCTSJg&list=PLrnPJCHvNZuA2ioi4soDZKz8euUQnJW65&index=8
In your Builder:
#BindsInstance
Builder horsePower(#Named("horse_power") int horsePower);
After this you will be able to pass horsePower from MainActivity without passing instance of PetrolEngineModule.And same way in ur PetrolEngine constructor:
#Inject
public PetrolEngine(#Named("horse_power") int horsePower) {
this.horsePower = horsePower;
}
And in your PetrolEngineModule u can remove everything and just leave #Provides for PetrolEngine.
Remove #Inject in the module class, cause that what #BindsInstance is doing when pass horsePower it in #Component.Builder
Just an optimization for future viewers. Just replace the existing code with this. Now Dagger will not create the --Provide-- class to provide the instance of PetrolEngine.
Please improve the code if you find anything wrong.
#Module
abstract class PetrolEngineModule {
#Binds
abstract fun bindEngine(engine: PetrolEngine): Engine
}

Categories

Resources