I'm in the process of wrapping my head around Architecture Components / MVVM.
Let's say I have a repository, a ViewModel and a Fragment. I'm using a Resource class as a wrapper to expose network status, like suggested in the Guide to architecture components.
My repository currently looks something like this (simplified for brevity):
class MyRepository {
fun getLists(organizationId: String) {
var data = MutableLiveData<Resource<List<Something>>>()
data.value = Resource.loading()
ApolloClient().query(query)
.enqueue(object : ApolloCall.Callback<Data>() {
override fun onResponse(response: Response<Data>) {
response.data()?.let {
data.postValue(Resource.success(it))
}
}
override fun onFailure(exception: ApolloException) {
data.postValue(Resource.exception(exception))
}
})
}
Then in the ViewModel, I also declare a MutableLiveData:
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myLiveData = myRepository.getLists(organizationId)
}
Finally, the Fragment:
viewModel.getLists.observe(this, Observer {
it?.let {
if (it.status.isLoading()) showLoading() else hideLoading()
if (it.status == Status.SUCCESS) {
it.data?.let {
adapter.replaceData(it)
setupViews()
}
}
if (it.status == Status.ERROR) {
// Show error
}
}
})
As you see, there will be an issue with the observer not being triggered, since the LiveData variable will be reset in the process (the Repository creates a new instance).
I'm trying to figure out the best way to make sure that the same LiveData variable is used between the Repository and ViewModel.
I thought about passing the LiveData from the ViewModel to the getLists method, so that the Repository would be using the object from the ViewModel, but even if it works, it seems wrong to do that.
What I mean is something like that:
ViewModel
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myRepository.getLists(myLiveData, organizationId)
}
Repository
fun getLists(data: MutableLiveData<Resource<List<Something>>>, organizationId: String) {
...
}
I think I figured out how to do it, thanks to #NSimon for the cue.
My repository stayed the same, and my ViewModel looks like this:
class MyViewModel : ViewModel() {
private val myRepository = MyRepository()
private val organizationIdLiveData = MutableLiveData<String>()
private val lists = Transformations.switchMap(organizationIdLiveData) { organizationId -> myRepository.getLists(organizationId) }
fun getLists() : LiveData<Resource<MutableList<Something>>> {
return lists
}
fun fetchLists(organizationId: String, forceRefresh: Boolean = false) {
if (organizationIdLiveData.value == null || forceRefresh) {
organizationIdLiveData.value = organizationId
}
}
}
I observe getLists() in my fragment, and call viewModel.fetchLists(id) when I want the data. Seems legit?
Related
I think my observable is set incorrectly here. I am using Retrofit2 + Moshi as the deserializer, and the API call from Retrofit is working.
But once I make the API call, I am trying to set up the Observable in my Activity and then use the API call data from the data class.
Here is my view model code:
class DealsViewModel(val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<DealItems>>()
var job: Job? = null
val loading = MutableLiveData<Boolean>()
val errorMessage = MutableLiveData<String>()
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getMovies() {
viewModelScope.launch{
// View Model Scope gives the Coroutine that will be canceled when the ViewModel is cleared.
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val items = repository.getProduct()
withContext(Dispatchers.Main) {
if (items.isNullOrEmpty()) {
loading.value = false
// put error message in here later
} else {
dealList.postValue(items)
return#withContext
}
}
}
}
}
private fun onError(message: String) {
errorMessage.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
And here is my MainActivity code.
I am using JetpackCompose in my activity, LiveData for the API response container. In my main repository is where I am validating a successful API response and then the coroutines for the call are inside of the view model.
My API call is successful, but I am not sure where to call the ViewModel.GetMovies() inside of the activity and I am not sure if the observables are set properly and/or where to pass the API's livedata into my composable function.
Thanks for any help you can provide. I am new to android and trying to use Coroutines for the first time.
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofitService = RetrofitService.getInstance()
val viewModel = ViewModelProvider(this,
MyViewModelFactory(MainRepository(retrofitService = retrofitService))).get(DealsViewModel::class.java)
// viewModel.getProducts()
setContent {
myApp {
MyScreenContent()
}
viewModel.movieList.observe(
this, { it ->
if( it != null) {
it.forEach {
var movieLocation = it.movieLocation
val description = it.description
val id = it.id
val title = it.title
val regularPrice = it.regularPrice
}
}
})
return#setContent
}
viewModel.errorMessage.observe(this, {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
})
viewModel.loading.observe(
this,
Observer {
if (it) {
}
})
}
}
I assume that it always depends when should you call especially in the activity we have many lifecycles; however, the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the getMovies is called only one time, and also guarantee the service itself is not called unless someone is listening to it.
You may check the full documentation in this link
Here is a code example
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call getMovies at all in the activity, you just listen to the observer.
I have this code in my view model
class SearchViewModel #ViewModelInject constructor(jobUseCase: JobUseCase) : ViewModel() {
val job = jobUseCase.getSearchJob(...).asLiveData()
}
but thegetSearchJob() need the parameter to fill. and this where I use the job in the activity
private fun getSearchData() {
searchViewModel.job.observe(this, { job ->
if (job != null) {
when (job) {
is Resource.Loading -> binding.progressBar.visibility = View.VISIBLE
is Resource.Success -> {
binding.progressBar.visibility = View.GONE
jobAdapter.setData(job.data)
}
is Resource.Error -> {
binding.progressBar.visibility = View.GONE
}
}
}
})
}
but I don't know how to fill the getSearchJob() parameter in the val. or if there is any idea please help me.
I think it would be simpler for you to have a simple MutableLiveData instance, from which you can expose a regular liveData that can be exposed to the activity, and a onTextChanged Listener simply updates the viewModel, jobUseCase.getSearchJob(...) gets the data, and then you can set the new value to MutableLiveData instance.
if you need something more elegant, then let jobUseCase.getSearchJob(...) take in a flow instance that you can pass in data to which in turn fires off your job livedata
Simple Mutable livedata based example
class SearchViewModel1 constructor(private val jobUseCase: JobUseCase) : ViewModel() {
private val _internalMutableLiveData = MutableLiveData<String>()
private val publicLiveData: LiveData<String> = _internalMutableLiveData
fun updateSearchTerm(term: String) {
viewModelScope.launch {
val result = jobUseCase.getSearchJob(term)
_internalMutableLiveData.value = result
}
}
}
class JobUseCase {
suspend fun getSearchJob(term: String): String {
// fetch Data here
return ""
}
}
In my viewmodel class
class ViewModel(application: Application) : AndroidViewModel(application) {
private val repository: Repository by lazy {
Repository.getInstance(getApplication<BaseApplication>().retrofitFactory)
}
private var _liveData = MutableLiveData<ItemState>()
val liveData: LiveData<ItemState> = _liveData
init {
fetchData()
}
private fun fetchData() {
repository.getLiveData().observeForever(liveDataObserver)
}
override fun onCleared() {
super.onCleared()
repository.getLiveData().removeObserver(liveDataObserver)
}
private val liveDataObserver = Observer<User> {
if (it != null) {
setData(it)
}
}
private fun setData(it: User) =viewModelScope.launch {
val list1 = mutableListOf<something1>()
val list2 = mutableListOf<something2>()
list1.add(it.data)
list2.add(it.data)
}
_liveData.value = ItemState.State1(list1)
delay(1)
_liveData.value = ItemState.State2(list2)
}
The ItemState is a sealed class with two data members
sealed class ItemState {
data class State1(val list: List<something1>) : ItemState()
data class State2(val list: List<something2>) : ItemState()
}
Activity Observer Code
viewModel.liveData.observe(this, Observer {
loadDataIntoUi(it)
})
private fun loadDataIntoUi(data: ItemState) {
when (data) {
is ItemState.State1 -> adaptr1.addItems(data.list)
is ItemState.State2 -> adaptr2.addItems(data.list)
}
Now if i don't use delay in my viewModel here like above the livedata first value that is Office doesn't get observed but it works fine with delay
I have done a lot of research didn't understand why this happening also I have many alternate solutions to this but my question is why delay make's it working
I Am using MVVM architecture to simple project. Then i stack in this case, when i have to return value from Model DataSource (Lambda function) to Repository then ViewModel will observe this repository. Please correct me if this not ideally and give me some advise for the true MVVM in android. i want to use LiveData only instead of RxJava in this case, because many sample in Github using RxJava.
In my Model i have class UserDaoImpl, code snippet like below
class UserDaoImpl : UserDao {
private val resultCreateUser = MutableLiveData<AppResponse>()
private val mAuth : FirebaseAuth by lazy {
FirebaseAuth.getInstance()
}
override fun createUser(user: User) {
mAuth.createUserWithEmailAndPassword(user.email, user.password)
.addOnCompleteListener {
//I DID NOT REACH THIS LINE
println("hasilnya ${it.isSuccessful} ")
if(it.isSuccessful){
val appResponse = AppResponse(true, "oke")
resultCreateUser.postValue(appResponse)
}else{
val appResponse = AppResponse(false, "not oke -> ${it.result.toString()}")
resultCreateUser.postValue(appResponse)
}
}
.addOnFailureListener {
println("hasilnya ${it.message}")
val appResponse = AppResponse(false, "not oke -> ${it.message}")
resultCreateUser.postValue(appResponse)
}
}
override fun getResultCreateUser() = resultCreateUser
}
And this is my Repository snippet code
class RegisterRepositoryImpl private constructor(private val userDao: UserDao) : RegisterRepository{
companion object{
#Volatile private var instance : RegisterRepositoryImpl? = null
fun getInstance(userDao: UserDao) = instance ?: synchronized(this){
instance ?: RegisterRepositoryImpl(userDao).also {
instance = it
}
}
}
override fun registerUser(user: User) : LiveData<AppResponse> {
userDao.createUser(user)
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
}
Then this is my ViewModel
class RegisterViewModel (private val registerRepository: RegisterRepository) : ViewModel() {
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
}
If i execute the snippet code above, the result always nullpointer in signUpResult
This is my Activity
lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
initializeUI()
}
private fun initializeUI() {
val factory = InjectorUtils.provideRegisterViewModelFactory()
viewModel = ViewModelProviders.of(this, factory).get(RegisterViewModel::class.java)
viewModel.signUpResult.observe(this, Observer {
//IT always null
if(it.success){
// to HomeActivity
Toast.makeText(this, "Success! ${it.msg}", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "FALSE! ${it.msg}", Toast.LENGTH_SHORT).show()
}
})
register_btn.setOnClickListener {
val username = name.text.toString()
val email = email.text.toString()
val password = password.text.toString()
val phone = number.text.toString()
val user = User(0, username,"disana", email, password, "disana")
viewModel.registerUser(user)
}
}
Crash occured when i press register button
I'm not 100% sure, but I think the problem is in your ViewModel, where you are trying to pass by reference MutableLiveData. Your Activity is observing signUpResult MutableLiveData, but you are never posting new value, you are trying to change reference of that LiveData to one in Repository.
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
I think that the solution here is to let your ViewModel return LiveData, which is returned from Repository.
fun registerUser(user: User): MutableLiveData<AppResponse> {
println(user.toString())
return registerRepository.registerUser(user)
}
And you need to observe function registerUser(user) in your Activity.
viewModel.registerUser(user).observe(this, Observer {
But now you encountered another problem. By this example you will trigger observe method every time your button is clicked. So you need to split in repository your function, you need to make one only for returning userDao.getResultCreateUser() as LiveData<AppResponse>, and the other to trigger userDao.create(user) .
So you can make two functions in your repository
override fun observeRegistrationResponse() : LiveData<AppResponse> {
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
override fun registerUser(user: User) {
userDao.createUser(user)
}
Now also in ViewModel you need to make separate function for observing result and for sending request for registration.
fun observeRegistrationResponse(): LiveData<AppResponse> {
return registerRepository.observeRegistrationResponse()
}
fun registerUser(user: User){
println(user.toString())
registerRepository.registerUser(user)
}
And finally you can observe in your function initializeUI
viewModel.observeRegistrationResponse().observe(this, Observer {
And send registration request on button click
viewModel.registerUser(user)
Sorry for long response, but I tried to explain why you need to change your approach. I hope I helped you a bit to understand how LiveData works.
I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}