Dagger can not inject ViewModel with KClass - mvvm

I am moving my project java to kotlin, but got some confusion about KClass and Class
Here is my BaseActivity
abstract class BaseActivity<DB : ViewDataBinding, VM : BaseViewModel> : DaggerAppCompatActivity() {
private lateinit var mCustomDialog: CustomDialog
private lateinit var mViewDataBinding: DB
private lateinit var mViewModel : VM
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set Custom Dialog
mCustomDialog = CustomDialog(this, R.style.LoadingDialogStyle)
// Set ViewModel
mViewModel = ViewModelProviders.of(this, viewModelFactory).get(getViewModelClass().java)
// Set DataBinding
mViewDataBinding = DataBindingUtil.setContentView(this, getLayoutId())
mViewDataBinding.lifecycleOwner = this
mViewDataBinding.setVariable(getBindingVariable(), mViewModel)
mViewDataBinding.executePendingBindings()
// Initialize UI
prepareView(savedInstanceState)
}
#LayoutRes
abstract fun getLayoutId(): Int
protected abstract fun getViewModelClass(): KClass<VM>
abstract fun getBindingVariable(): Int
fun getViewModel(): VM {
return mViewModel
}
fun getViewDataBinding() : DB {
return mViewDataBinding
}
I am using protected abstract fun getViewModelClass(): KClass<VM> function for initializing ViewModel class in the function below
ViewModelProviders.of(this, viewModelFactory).get(getViewModelClass().java)
I use ViewModel in activities by this way
class SplashActivity : BaseActivity<ActivitySplashBinding, SplashViewModel>() {
override fun getViewModelClass(): KClass<SplashViewModel> {
return SplashViewModel::class
}
override fun getLayoutId(): Int {
return R.layout.activity_splash
}
override fun getBindingVariable(): Int {
return BR.vm
}
override fun prepareView(savedInstanceState: Bundle?) {
getViewModel().testLog()
}
}
But when I run the project, I got this error
error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.example.example.MyApp> {
^
java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
com.example.example.utils.ViewModelFactory(viewModels)
com.example.example.utils.ViewModelFactory is injected at
com.example.example.base.BaseActivity.viewModelFactory
com.example.example.ui.splash.SplashActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.example.example.di.AppComponent ? com.example.example.di.ActivityBindingsModule_SplashActivityInjector$app_debug.SplashActivitySubcomponent]
So I made some research and find out it is about KClass in my ViewModelKey
Here is ViewModelKey
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
If I do not change my code to Kotlin and use old Java class like this it works properly
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#MapKey
public #interface ViewModelKey {
Class<? extends ViewModel> value();
}
This is my ViewModelFactory class
#Suppress("UNCHECKED_CAST")
class ViewModelFactory #Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModels[modelClass]
?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
My SplashActivityModule
#Module
abstract class SplashActivityModule {
#Binds
#IntoMap
#ViewModelKey(SplashViewModel::class)
internal abstract fun provideSplashViewModel(splashViewModel: SplashViewModel) : ViewModel
}
So how can I use ViewModelKey properly with Kotlin and what is main cause of this error, any help will be appreciated

Your ViewModelKey be like
#MustBeDocumented
#kotlin.annotation.Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)

As mentioned this question problem is related to Kotlin version. Using higher than 1.3.30 version solves the problem.

Related

ViewModel Junit Test fails in case when i use Junit5 and Rxjava. Method getMainLooper in android.os.Looper not mocked

I am trying to wrote test case for my view model and I am using junit5 along with mockito. The issue is my test case is always failing it gives me this error
at android.os.Looper.getMainLooper(Looper.java)
at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder.<clinit>
Now I did a lot of research on this issue and found out that this is due to Rxjava scheduler as it is not able to run on the background thread I followed this link and did the required step so after that my view model looks like this
MainViewModelTest.kt
#ExtendWith(value = [InstantExecutorExtension::class,TestSchedulerExtension::class])
class MainViewModelTest {
#Mock
private lateinit var repository: GithubRepository
private lateinit var viewModel: MainViewModel
#Before
fun init() {
MockitoAnnotations.initMocks(this)
viewModel = MainViewModel(repository)
}
#Test
fun testNull(){
assertThat(viewModel.observeTrendingRepository(), notNullValue())
verify(repository, never()).makeRequestForTrendingRepo(anyBoolean())
}
#Test
fun fetchTrendingGitHubRepository_emptyDbFirstTimeCase(){
`when`(repository.makeRequestForTrendingRepo(anyBoolean()))
.thenReturn(Flowable.just(Resource.Loading(listOf())))
viewModel.fetchTrendingGitHubRepository(false) // this is where it fails
assertEquals(Resource.Loading<List<Repository>>(listOf()),LiveDataTestUtil.getValue(viewModel.observeTrendingRepository()));
}
}
Here are the InstantExecutorExtension and TestSchedulerExtension that take care of live data and Rxjava respectively.
package com.rajat.zomatotest.utils
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
package com.rajat.zomatotest.utils
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback
import org.junit.jupiter.api.extension.ExtensionContext
class TestSchedulerExtension : BeforeTestExecutionCallback, AfterTestExecutionCallback {
override fun beforeTestExecution(context: ExtensionContext?) {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
}
override fun afterTestExecution(context: ExtensionContext?) {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
I couldn't understand why is it giving me error, I have correctly added TestSchedulerExtension.kt so this should work!!
I run into the same problem, and I found that the way to fix this was by using
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
instead of
RxAndroidPlugins.setMainThreadSchedulerHandler { scheduler }
In this link you will find more information.

Adding Multiple Adapter Into Fragment

Hello all can you advice me how to add multiple different adapter in one fragment using dagger ?
#Module
class HomeFragmentModule {
#Provides
internal fun provideHomeInteractor(interactor: HomeInteractor): HomeMVPInteractor = interactor
#Provides
internal fun provideRegisterPresenter(presenter: HomePresenter<HomeView, HomeMVPInteractor>) : HomeMVPPresenter<HomeView, HomeMVPInteractor> = presenter
#Provides
#Named("ppobAdapter")
fun providePpobAdapter(): PpobAdapter = PpobAdapter(ArrayList())
#Provides
#Named("sliderAdapter")
internal fun provideSliderAdapter(): SliderAdapter = SliderAdapter(ArrayList())
} ```
Try this:
HomeFragment : Fragment() {
#Inject
#Named("ppobAdapter")
lateinit var ppobAdapter: PpobAdapter
#Inject
#Name("sliderAdapter")
lateinit var sliderAdapter: SliderAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
DaggerAppComponent
.builder()
.build()
.inject(this)
}
}
With a component that looks like this:
#Component(modules = [HomeFragmentModule::class])
interface AppComponent {
fun inject(fragment: HomeFragment)
}
For a really good intro to dagger I always advise this tutorial series

Reactive repository does not save an object

I think I don't understand well how a Reactive repository and handlers using it work. I have written a special test class only to test the simpliest handler using a repository
#SpringBootTest
class TestRepository() {
#Autowired
lateinit var myRepo: myRepo
#Autowired
lateinit var myHandler: MyHandler
#Test
fun `save with a handler`() {
val myObject = MyObject()
myHandler.save(request).subscribe()
StepVerifier.create (myRepository.count() ) <--this does not work
.expectNext (1L )
.expectComplete().verify()
}
#Test
fun `test only database saving`() {
val object = MyObject()
myRepo.save(myRepo).subscribe()
StepVerifier.create (myRepo.count() ) <-- this works
.expectNext (1L )
.expectComplete().verify()
}
}
my handler and repository are defined in the following way:
#Service
class MyHandler(private val myRepository: MyRepository) {
fun save(object: MyObject): Mono<MyObject> {
return myRepository.save(request)
}
}
#Repository
interface MyRepo : ReactiveMongoRepository<MyObject, String> {
fun save(request: MyObject): Mono<MyObject>
}
I also tried to play with subscribe method but it still does not see the results.
What should I correct?
Use Mono.then function to chain save and count functions and get a resulting Mono:
#Test
fun `save with a handler`() {
val countAfterSave = myHandler.save(MyObject()).then(myRepository.count());
StepVerifier.create(countAfterSave)
.expectNext(1L)
.expectComplete()
.verify()
}

Cannot access 'TAG': it is invisible (private in supertype) in 'AppCompatActivity'

I am a beginner in android programming. I'm currently using android studio 3.2.1.
I am trying to monitor the different states of an android activity in log. I have written the code shown below, but I keep receiving the error message:
Cannot access 'TAG': it is invisible (private in supertype) in 'AppCompatActivity'.
Even after searching, I cannot figure the error. Can some one help?
Code:
package com.cooperation.bestech.test1
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log;
class MainActivity : AppCompatActivity() {
private static final String TAG = "MyMessage";
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i(TAG, "onCreate");
}
override fun onStart() {
super.onStart()
Log.i(TAG, "onStart");
}
override fun onPause() {
super.onPause()
Log.i(TAG, "onPause");
}
override fun onResume() {
super.onResume()
Log.i(TAG, "onResume");
}
override fun onStop() {
super.onStop()
Log.i(TAG, "onStop");
}
override fun onRestart() {
super.onRestart()
Log.i(TAG, "onRestart");
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy");
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
Log.i(TAG, "onSaveInstanceState");
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
Log.i(TAG, "onRestoreInstanceState");
}
}
Since the original question is written in Kotlin (from comment), ignore the request of "launch Java project instead of the default Kotlin" may be a better solution.
And, applying the correct way to declare and assign a variable, your code segment should be like this:
class MainActivity : AppCompatActivity() {
val TAG = "MyMessage"
import android.content.ContentValues.TAG
Seems you are using Kotlin.So you need to define TAG in kotlin way:
In Kotlin constants located in the companion object:
class MyClass {
companion object {
private val TAG = "ClassName"
}
}

I can't reach any class member from a nested class in Kotlin

I want to access a member of the MainFragment class from PersonAdapter class but none of them are available. I tried making both the classes and the members public and private also but so far nothing worked.
I guess I'm missing something obvious but I just can't figure it out.
class MainFragment : Fragment() {
lateinit var personAdapter: PersonAdapter
lateinit var personListener: OnPersonSelected
private var realm: Realm by Delegates.notNull()
lateinit var realmListener: RealmChangeListener<Realm>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val v = inflater.inflate(R.layout.fragment_main, container, false)
return v
}
class PersonAdapter() : RecyclerView.Adapter<ViewHolder>() {
var localPersonList = personList
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindItems(localPersonList[position])
holder.itemView.setOnClickListener {
Toast.makeText(context, "click", Toast.LENGTH_SHORT).show()
//I want to reach personListener from here
}
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent!!.context).inflate(R.layout.person_list_item, parent, false)
return ViewHolder(v)
}
}}
In Kotlin, nested classes cannot access the outer class instance by default, just like nested static classes can't in Java.
To do that, add the inner modifier to the nested class:
class MainFragment : Fragment() {
// ...
inner class PersonAdapter() : RecyclerView.Adapter<ViewHolder>() {
// ...
}
}
Note that an inner class holds a reference to its containing class instance, which may affect the lifetime of the latter and potentially lead to a memory leak if the inner class instance is stored globally.
See: Nested classes in the language reference
In Kotlin, there are 2 types of the nested classes.
Nested Class
inner Class
Nested class are not allowed to access the member of the outer class.
If you want to access the member of outer class in the nested class then you need to define that nested class as inner class.
class OuterClass{
var name="john"
inner class InnerClass{
//....
}
}
Add inner
Note that Android Studio's Code completion(IntelliSense) doesn't work right inside the inner class
class OuterClass {
val outerVariable = "Hello, World!"
inner class InnerClass {
// Code completion doesn't work here
val innerVariable = outerVariable // Code completion work
fun innerFunction() {
// Code completion work
}
}
}