How to pass viewmodel to fragment using launchFragmentInHiltContainer - android

I was trying to write a test case for my fragment. The fragment kind of look like this,
#AndroidEntryPoint
class MainFragment : BaseFragment() {
val viewModel: HomeFragmentViewModel by viewModels()
......
}
Now in my test class,
#RunWith(AndroidJUnit4::class)
#LargeTest
#HiltAndroidTest
#ExperimentalCoroutinesApi
class MainFragmentTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Before
fun init() {
hiltRule.inject()
}
#Test
fun testFragmentDisplays() {
val fakeRepository = FakeRepository()
val scenerio = launchFragmentInHiltContainer<MainFragment>() {
//(this as MainFragment).viewModel = HomeFragmentViewModel(fakeRepository)
}
}
}
The problem is viewModel in the Fragment is val and it has to be val because I am using viewModels() to create the viewmodel. Is there anyway I can create the viewmodel and use in my fragment?
Thanks in Advance.

Make Your BaseFragment Like This
abstract class BaseFragment<VM: ViewModel>: Fragment() {
protected lateinit var viewModel: VM
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(getViewModelClass())
.....
}
}
Then Extend it Like This
#AndroidEntryPoint
class MainFragment : BaseFragment<HomeFragmentViewModel>() {
......
}
And HomeFragmentViewModel Should be like this
#HiltViewModel
class HomeFragmentViewModel #Inject constructor(
private val YourRepository: YourRepository
) : ViewModel() {
........
}

Related

Injecting a repository with lateinit is leading to crash

I have injected my repository in an Activity with lateinit declaration. However, when I am calling the method of repository it is resulting in a crash saying lateinit property clearDbRepository has not been initialized.
class StartActivity : BaseActivity() {
private lateinit var binding: StartEmptyPageBinding
#Inject
lateinit var clearDbRepository: ClearDbRepository
override fun setupViews() {
lifecycleScope.launch {
clearDbRepository.clearLocalDatabase()
}
}
}
My ClearDbRepository is:
#Singleton
class ClearDbRepository #Inject constructor(
private val mainDatabase: LocalDB
) {
suspend fun clearLocalDatabase() = withContext(Dispatchers.IO) {
mainDatabase.clearAllTables()
}
}
If you are using the Hilt library, then most probably, according to your code snippet, you're missing an annotation. You must add the appropriate annotation above your Activity class, like this:
#AndroidEntryPoint
class StartActivity : BaseActivity() { }

Cannot create an instance of class ViewModel using dagger hilt

My ViewModel:
class LoginViewModel #ViewModelInject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
val currentResult: MutableLiveData<String> by lazy {
MutableLiveData<String>()
}
fun loginUseCase(username: String, password: String) {
viewModelScope.launch {
loginUseCase.invoke(username, password).apiKey.let {
currentResult.value = it
}
}
}
}
Is being used by my MainActivity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val loginViewModel: LoginViewModel by viewModels()
And I know that the ViewModelProvider is expecting a empty constructor but I need to use the LoginUseCase:
class LoginUseCase #Inject constructor(
private val apiService: ApiServiceImpl
) : UseCase<Unit>() {
suspend operator fun invoke(username: String, password: String) =
apiService.login(username, password)
}
Inside the modelView, but i get the error:
Cannot create an instance of class com.example.myboards.ui.login.LoginViewModel
in runtime, and I dont know how I could manage the LoginUseCase inside the LoginViewModel
Provide a ViewModel by annotating it with #HiltViewModel and using the #Inject annotation in the ViewModel object's constructor.
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
...
}
Hilt needs to know how to provide instances of ApiServiceImpl, too. Read here to know how to inject interface instances with #Binds.
Let me know If you still have a problem.

ViewModel Unit test

I have a viewmodel that only emits the value of repo when I subscribe to it in the activity. I am trying to unit test the viewmodel (see code below) but I am getting NPE because repo is null. How can I unit test it? Is it possible?
class MainViewModel #ViewModelInject constructor(mainRepository: MainRepository) : ViewModel() {
val repo: LiveData<Resource<List<Repository>>> = mainRepository.getRepositories()
}
#RunWith(JUnit4::class)
class MainViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val mainRepository = mock(MainRepository::class.java)
private lateinit var mainViewModel: MainViewModel
#Before
fun init() {
mainViewModel = MainViewModel(mainRepository)
}
#Test
fun testGetRepos() {
mainViewModel.repo.observeForever(mock()) /* NPE at this point as repo is null*/
verify(mainRepository).getRepositories()
}
}
Create an interface something like IMainRepository have your actual repository class implement it
class MainRepository : IMainRepository
Then change your ViewModel constructor to accept the interface
class MainViewModel #ViewModelInject constructor(mainRepository: IMainRepository) : ViewModel()
Then create a Mock class that implements the interface and what it is suppose to do
class MockMainRepository : IMainRepository
in your test create a new instance of the mock class and pass that to your ViewModel to test
private val mockMainRepository = MockMainRepository()
#Before
fun init() {
mainViewModel = MainViewModel(mainRepository)
}

Viewmodel not retaining values after changing activities using dagger dependency injection

I'm trying to share a ViewModel between two different activities. The first activity is LoginActivity, there I make a request to an api using the users username and password to get a user object. Using live data that user is then changed in the ViewModel. To test I also have a variable that is a string and I change it once i make the API call.
My ViewModel:
class MainViewModel #Inject constructor() : ViewModel(){
var _request = MutableLiveData<LoginRequest>()
val userLogin: LiveData<LoggedInUser> = Transformations
.switchMap(_request){req ->
MainRepository.authenticatePlease(req.username, req.password)
}
var rando = "rando rando"
fun setUser(username: String, password: String){
val update = LoginRequest(username, password)
rando = "changed"
if(_request.value == update){
return
}
_request.value = update
}
}
As you can see the value of the variable rando is changed from rando rando to changed
In my first activity looks like this:
class LoginActivity : DaggerAppCompatActivity() {
#Inject lateinit var modelFactory: ViewModelProvider.Factory
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// AndroidInjection.inject(this)
val button: Button = findViewById(R.id.button)
button.setOnClickListener { onButtonClick() }
val label: TextView = findViewById(R.id.counter)
viewModel = ViewModelProvider(this, modelFactory).get(MainViewModel::class.java)
println("FACTORY: ${modelFactory.hashCode()}")
println("MODEL: ${viewModel.hashCode()}")
viewModel.userLogin.observe(this, Observer{ user ->
println("Debug LOGIN: ${user}")
println("RANDOM: ${viewModel.rando}")
if(user != null){
label.text = user.user.name
redirectToLogin()
}
})
}
fun onButtonClick(){
var username: EditText = findViewById(R.id.username)
var password: EditText = findViewById(R.id.password)
println(username.text.toString())
println(password.text.toString())
viewModel.setUser(username.text.toString(), password.text.toString())
}
fun redirectToLogin(){
println("Login done!")
val intent = Intent(this, SampleActivity::class.java)
intent.putExtra("extra", viewModel.userLogin.value)
startActivity(intent)
}
}
The second activity looks like this:
class SampleActivity : DaggerAppCompatActivity() {
lateinit var viewModel: MainViewModel
#Inject lateinit var modelFactory: ViewModelProvider.Factory
override fun onCreate(savedInstanceState: Bundle?) {
// AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sample)
// val loggedInUser: LoggedInUser? = intent.getParcelableExtra("extra")
// println("extra: $loggedInUser")
viewModel = ViewModelProvider(this, modelFactory).get(MainViewModel::class.java)
println("FACTORY: ${modelFactory.hashCode()}")
println("MODEL: ${viewModel.hashCode()}")
println(" $ WORKS: ${viewModel.userLogin.value.toString()}")
println(" $ WORKS: ${viewModel.userLogin.value}")
println(" $ WORKS: ${viewModel.rando}")
}
}
One thing to note is that the hashcode for my ViewModelFactory is the same in both activities, but the hashcodes for the ViewModels are different. Moreover, the variable rando in the ViewModel is rando rando in the second activity as opposed to changed
Thanks!
EDIT
Here is my ViewModelFactory:
#Singleton
class ViewModelProviderFactory #Inject constructor(
private val creators: Map<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass]
?: throw IllegalArgumentException("Unknown model class $modelClass")
return creator.get() as T
}
AppComponent.kt:
#Singleton
#Component(
modules = [
AppModule::class,
AndroidInjectionModule::class,
ViewModelModule::class,
ActivityModule::class
]
)
interface AppComponent {
fun inject(app: MainApplication)
#Component.Factory
interface Factory {
fun create(#BindsInstance context: Context): AppComponent
}
}
ActivityModule.kt:
#Module
abstract class ActivityModule {
#ContributesAndroidInjector
abstract fun contributeLoginActivity(): LoginActivity
#ContributesAndroidInjector
abstract fun contributeSampleActivity(): SampleActivity
}
AppModule.kt:
#Module(includes = [ViewModelModule::class])
class AppModule {
#Provides
#Named("mainViewModel")
fun provideMainViewModel(): MainViewModel =
MainViewModel()
}
and ViewModelModule.kt:
#Module
abstract class ViewModelModule {
#Binds
#IntoMap
#ViewModelKey(MainViewModel::class) // PROVIDE YOUR OWN MODELS HERE
internal abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
#Binds
internal abstract fun bindViewModelProviderFactory(factory: ViewModelProviderFactory): ViewModelProvider.Factory
}
one thing to notice is that in ViewModelModule I get the following warning Function bindViewModelProviderFactory is never used and
Function bindMainViewModel is never used

Espresso UI error on ViewModelProviders.of

I'm trying to write an espresso test in my application. In the application, I use dagger 2 and architecture components (LiveData, etc.). The test has a function to help me create fake injections for the tested activity. When I use it to mock the ManViewModel it works with no problem and I can run the test. But when I want to set a mock value for the ViewModelProvider.Factory the test throws an error in MainActivity: ViewModelProviders.of(th…iewModelImpl::class.java) must not be null
I have debugged the test and when I'm assigning the mock value it isn't null and in the main activity the values are not null but steel I get the error.
Some help would be appreciated.
The code for MainActivity:
class MainActivity : BaseActivity(), AnimateFactsImage {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
#Inject
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
mainViewModel = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
// Here is the error: ViewModelProviders.of(th…iewModelImpl::class.java) must not be null
}
}
The code for MainActiviyTest:
#RunWith(AndroidJUnit4::class)
class MainActivityView {
#get:Rule
val activityTestRule = object : ActivityTestRule<MainActivity>(MainActivity::class.java, true, false) {
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val myApp = InstrumentationRegistry.getTargetContext().applicationContext as MyApplication
myApp.activityDispatchingAndroidInjector = createFakeActivityInjector<MainActivity> {
mainViewModel = mainView
viewModelFactory = mockMPF // TODO: fix problem, null pointer in activity
}
}
}
private val mockMPF = Mockito.mock(ViewModelProvider.Factory::class.java)
private var mainView = Mockito.mock(MainViewModel::class.java)
private val repoLiveData = MutableLiveData<ApiResponse<Result>>()
#Before
fun setup() {
Mockito.`when`(mainView.getFactsList()).thenReturn(repoLiveData)
}
#Test
fun isListDisplayed() {
repoLiveData.postValue(ApiResponse(Result("title", arrayListOf(Row("title", "des", "img"))), null))
activityTestRule.launchActivity(null)
onView(withId(R.id.recycler_adapter)).check(matches(isDisplayed()))
}
}

Categories

Resources