Thoughts Required ? Am i cleaning my ViewModel class properly? - android

I want to clean my viewmodel class by using raywenderlich approach.
So in this blog they have written that we there is a binder component which is not for user. but i want to use this class as helper class for viewmodel.
Here I am creating a class which i termed as Binder class for ViewModel, which my view will use to make contact with ViewModel class.
Now my Binder class is helping me in cleaning my ViewModel class like,
Edited :
So my View class will be like :
class UserDetailView : AppCompatActivity(), UiCallbacks {
lateinit var screenCallbacks: ScreenCallbacks;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
screenCallbacks = UserDetailBinder(this).bindLayout(R.layout.activity_learn_binding)
screenCallbacks.loadData()
}
override fun onUserNameChanged(userName: String) {
}
}
And my Binder class will be like :
class UserDetailBinder(private val userDetailView: UserDetailView) : ScreenCallbacks, ViewModelController {
private val userDetailObservers = UserDetailViewState(userDetailView)
init {
userDetailObservers.observe(userDetailView)
}
fun bindLayout(layout: Int): ScreenCallbacks {
binding<com.bold.job.databinding.ActivityLearnBindingBinding, OwnViewModel>(userDetailView, layout).let {
var viewModel = viewModel(userDetailView, OwnViewModel::class.java)
it.viewModel = viewModel
}
return this
}
override fun loadData() {
}
}

Related

How to pass arguments from Activity to ViewModel using Hilt (without a ViewModel Factory)

In my activity, I have multiple variables being initiated from Intent Extras. As of now I am using ViewModelFactory to pass these variables as arguments to my viewModel.
How do I eliminate the need for ViewModelFacotory with hilt
Here are two variables in my Activity class
class CommentsActivity : AppCompatActivity() {
private lateinit var viewModel: CommentsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
val contentId = intent.getStringExtra(CONTENT_ID_FIELD) //nullable strings
val highlightedCommentId = intent.getStringExtra(HIGHLIGHTED_COMMENT_ID_RF) //nullable strings
val commentsViewModelFactory = CommentsViewModelFactory(
contentId,
highlightedCommentId
)
viewModel = ViewModelProvider(this, commentsViewModelFactory[CommentsViewModel::class.java]
}
}
Here is my viewModel
class CommentsViewMode(
contentId : String?,
highlightedCo;mmentId : String?,
) : ViewModel() {
//logic code here
}
My app is already set up to use hilt but in this case How can I pass these 2 variables and eliminate the viewModelFactory entirely
The trick is to initialize those variables only once, while the activity can be created multiple times. In my apps, I use a flag.
View model:
class CommentsViewModel : ViewModel() {
private var initialized = false
private var contentId : String? = null
private var highlightedCommentId : String? = null
fun initialize(contentId : String?, highlightedCommentId : String?) {
if (!initialized) {
initialized = true
this.contentId = contentId
this.highlightedCommentId = highlightedCommentId
}
}
//logic code here
}
Also, you should know that there is an open issue in dagger project exactly for this capability:
https://github.com/google/dagger/issues/2287
You're welcome to follow the progress.
If you want to use hilt effectively u can follow this steps
Use #HiltViewModel in your view model
#HiltViewModel
class MyViewModel #inject constructor(private val yrParameter): ViewModel {}
Also you no longer need any ViewModelFactory! All is done for you! In your activity or fragment, you can now just use KTX viewModels() directly.
private val viewModel: MyViewModel by viewModels()
Or if you want to use base classes for fragment and activity you can use this code to pass viewModel class
abstract class BaseFragment<V: ViewModel, T: ViewDataBinding>(#LayoutRes val layout: Int, viewModelClass: Class<V>) : Fragment() {
private val mViewModel by lazy {
ViewModelProvider(this).get(viewModelClass)
}
}

Binding is missing in Dagger2?

I have made a simple app that tells the car is a two-wheeler or four-wheeler with the help of dagger2 but an error will occur if I run the app and the error is binding is missing for this I have also used #Named annotation but the error is coming again and again.
MainActivity.kt
class MainActivity : AppCompatActivity() {
#Inject
lateinit var car: Car
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
DaggerCarComp.builder().build().inject(this)
car.tellCarTypes()
}
}
Car.kt
class Car #Inject constructor(
#Named("two") val tw: TW,
val fw: FW
) {
fun tellCarTypes() {
tw.whichCar()
}
}
CarComp.kt
#Component(modules = [CarModule::class])
interface CarComp {
fun inject (mainActivity: MainActivity)
}
CarType.kt
interface CarType {
fun whichCar()
}
class TW #Inject constructor() : CarType {
override fun whichCar() {
Log.d("Type", "whichCar: Two wheeler")
}
}
class FW : CarType {
override fun whichCar() {
Log.d("Type", "whichCar: Four wheeler")
}
}
CarModule.kt
#Module
class CarModule {
#Provides
#Named("two")
fun tellCar(tw: TW) : CarType {
return tw
}
#Provides
#Named("four")
fun tellCar2() : CarType {
return FW()
}
}
Error image
Your class FW does not have the #Inject annotation alongside his constructor. I think it has to be defined this way so Dagger know what to do
class FW #Inject constructor() : CarType {
override fun whichCar() {
Log.d("Type", "whichCar: Four wheeler")
}
}
Also your function tellCar2 should probably be defined in this way :
#Provides
#Named("four"
fun tellCar2(fw: FW) : CartType {
return fw
}
Since you are using an interface, you could also use #Binds in an abstract module and so you can directly bind the correct implementation of the interface regarding your needs. Check this article : https://medium.com/mobile-app-development-publication/dagger-2-binds-vs-provides-cbf3c10511b5 or this : https://dagger.dev/api/2.21/dagger/Binds.html

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

Is there away to mock ViewModel that is inside of a fragment

Is there a way to mock ViewModel that's built is inside of a fragment? I'm trying to run some tests on a fragment, one of the fragment functions interacts with the ViewModel, I would like to run the test function and provided a mocked result for the ViewModel. Is this even possilbe?
MyFragment
class MyFragment : Fragment() {
#Inject
lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
(requireActivity().application as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RoboeltricTestRunner::) {
#Before
fun setup() {
FragmentScenario.Companion.launchIncontainer(MyFragment::class.java)
}
}
Yeah, just mark your ViewModel open and then you can create a mock implementation on top of it.
open class MyViewModel: ViewModel() {
fun myMethodINeedToMock() {
}
}
class MockMyViewModel: MyViewModel() {
override fun myMethodINeedToMock() {
// don't call super.myMethodINeedToMock()
}
}
So, register your MockMyViewModel to the DI framework when testing.
I thought I would post this for anyone else struggling to find a solution. You'll want to use a Fragment Factory, that has a dependency on the ViewModel. Injecting the ViewModel into the fragments constructor allows the ViewModel to easliy be mocked. There are a few steps that need to be completed for a FragmentFactory but it's not that complicated once you do a couple of them.
Fragment Add the ViewModel into the constructor.
class MyFragment(private val viewModel: ViewModel) : Fragment {
...
}
FragmentFactory, allows fragments to have dependencies in the constructor.
class MyFragmentFactory(private val viewModel: MyViewModel) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
MyFirstFragment::class.java.name -> {
MyFragment(viewModel)
}
// You could use this factory for multiple Fragments.
MySecondFragment::class.java.name -> {
MySecondFragment(viewModel)
}
// You also don't have to pass the dependency
MyThirdFragment::class.java.name -> {
MyThirdFragment()
}
else -> super.instantiate(classLoader, className)
}
}
}
Main Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create your ViewModel
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// create the FragmentFactory and the viewModel dependency.
supportFragmentManager.fragmentFactory = MainFragmentFactory(viewModel)
// FragmentFactory needs to be created before super in an activity.
super.onCreate(savedInstanceState)
}
}
Test
#RunWith(RobolectricTestRunner::class)
class MyFragmentUnitTest {
#Before
fun setup() {
val viewModel: MainViewModel = mock(MyViewModel::class.java)
...
}
}

Why a viewmodel factory is needed in Android?

We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
I just put a simple example of how I did it without Factory
here is the kodein module:
val heroesRepositoryModel = Kodein {
bind<HeroesRepository>() with singleton {
HeroesRepository()
}
bind<ApiDataSource>() with singleton {
DataModule.create()
}
bind<MainViewModel>() with provider {
MainViewModel()
}
}
The piece of the Activity where I instantiate the viewmodel without using the factory
class MainActivity : AppCompatActivity() {
private lateinit var heroesAdapter: HeroAdapter
private lateinit var viewModel: MainViewModel
private val heroesList = mutableListOf<Heroes.MapHero>()
private var page = 0
private var progressBarUpdated = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
initAdapter()
initObserver()
findHeroes()
}
The ViewModel where I instantiate the usecase directly without having it in the constructor
class MainViewModel : ViewModel(), CoroutineScope {
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
val data = MutableLiveData<List<Heroes.MapHero>>()
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
launch {
try {
val response = heroesRepository.getHeroes(page).await()
data.value = response.data.results.map { it.convertToMapHero() }
} catch (e: HttpException) {
data.value = null
} catch (e: Throwable) {
data.value = null
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
So here a example using factory
class ListFragment : Fragment(), KodeinAware, ContactsAdapter.OnContactListener {
override val kodein by closestKodein()
private lateinit var adapterContacts: ContactsAdapter
private val mainViewModelFactory: MainViewModelFactory by instance()
private val mainViewModel: MainViewModel by lazy {
activity?.run {
ViewModelProviders.of(this, mainViewModelFactory)
.get(MainViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_list, container, false)
}
The viewmodelfactory:
class MainViewModelFactory (private val getContacts: GetContacts) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return MainViewModel(getContacts) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And the viewmodel:
class MainViewModel(private val getContacts: GetContacts) : BaseViewModel() {
lateinit var gamesList: LiveData<PagedList<Contact>>
var contactsSelectedData: MutableLiveData<List<Contact>> = MutableLiveData()
var contactsSelected: ArrayList<Contact> = ArrayList()
private val pagedListConfig by lazy {
PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGES_CONTACTS_SIZE)
.setPageSize(PAGES_CONTACTS_SIZE)
.setPrefetchDistance(PAGES_CONTACTS_SIZE*2)
.build()
}
Here is the complete first example:
https://github.com/ibanarriolaIT/Marvel/tree/mvvm
And the complete second example:
https://github.com/AdrianMeizoso/Payment-App
We can not create ViewModel on our own. We need ViewModelProviders utility provided by Android to create ViewModels.
But ViewModelProviders can only instantiate ViewModels with no arg constructor.
So if I have a ViewModel with multiple arguments, then I need to use a Factory that I can pass to ViewModelProviders to use when an instance of MyViewModel is required.
For example -
public class MyViewModel extends ViewModel {
private final MyRepo myrepo;
public MyViewModel(MyRepo myrepo) {
this.myrepo = myrepo;
}
}
To instantiate this ViewModel, I need to have a factory which ViewModelProviders can use to create its instance.
ViewModelProviders Utility can not create instance of a ViewModel with argument constructor because it does not know how and what objects to pass in the constructor.
In short,
if we need to pass some input data to the constructor of the viewModel , we need to create a factory class for viewModel.
Like example :-
class MyViewModelFactory constructor(private val repository: DataRepository): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MyViewModel::class.java!!)) {
MyViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Reason
We cannot directly create the object of the ViewModel as it would not be aware of the lifecyclerOwner. So we use :-
ViewModelProviders.of(this, MyViewModelFactory(repository)).get(MyViewModel::class.java)
We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?
Because Android will only give you a new instance if it's not yet created for that specific given ViewModelStoreOwner.
Let's also not forget that ViewModels are kept alive across configuration changes, so if you rotate the phone, you're not supposed to create a new ViewModel.
If you are going back to a previous Activity and you re-open this Activity, then the previous ViewModel should receive onCleared() and the new Activity should have a new ViewModel.
Unless you're doing that yourself, you should probably just trust the ViewModelProviders.Factory to do its job.
(And you need the factory because you typically don't just have a no-arg constructor, your ViewModel has constructor arguments, and the ViewModelProvider must know how to fill out the constructor arguments when you're using a non-default constructor).
When we are simply using ViewModel, we cannot pass arguments to that ViewModel
class GameViewModel() : ViewModel() {
init {
Log.d(TAG, "GameViewModel created")
}
}
However, in some cases, we need to pass our own arguments to ViewModel. This can be done using ViewModelFactory.
class ScoreViewModel(finalScore: Int) : ViewModel() {
val score = finalScore
init {
Log.d(TAG, "Final score: $finalScore")
}
}
And to instantiate this ViewModel, we need a ViewModelProvider.Factory as simple ViewModel cannot instantiate it.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
When it comes to instantiating object of this ViewModel i.e with ViewModelProvider, we pass ViewModelFactory as an argument which contains information about our custom arguments which we want to pass. It goes like:
viewModelFactory = ScoreViewModelFactory(score)
viewModel = ViewModelProvider(this,viewModelFactory).get(ScoreViewModel::class.java)
That is why factory methods are there.

Categories

Resources