SharedViewModel
class SharedViewModel : ViewModel() {
private val feedback: MutableLiveData<Feedback> = MutableLiveData()
fun setType(type: TYPE?) {
this.feedback.value?.type = type
}
fun setPart(part: PART?) {
this.feedback.value?.part = part
}
fun setStatus(status: STATUS?) {
this.feedback.value?.status = status
}
fun setSoftware(software: SOFTWARE?) {
this.feedback.value?.software = software
}
fun setHardware(hardware: HARDWARE?) {
this.feedback.value?.hardware = hardware
}
fun getFeedback() : MutableLiveData<Feedback> = this.feedback
}
Setting value in Fragment A
val model: SharedViewModel by activityViewModels()
screen.setOnClickListener {
model.setHardware(HARDWARE.SCREEN) // ENUM CLASS
}
Trying to print value in Fragment B
val model: SharedViewModel by activityViewModels()
print.setOnClickListener { println(model.getFeedback().value) } // NULL ;(
What I've tried so far:
Replace code with an observer pattern in fragment B, still get null
Tried to use navigator component shared data like this
val viewModel: SharedViewModel by navGraphViewModels(R.id.checkout_graph)
Any help is much appreciated !
Problem is LiveData value will be null for the first time, so kotlin statement
this.feedback.value?.hardware = hardware wont execute after value?. please change the value so it will invoke changes to subscribers
fun setHardware(hardware: HARDWARE?) {
this.feedback.value = this.feedback.value?.apply{ this.hardware = hardware }?: Feedback(hardware = hardware) //if null set default
}
Same applies for other methods in viewmodel
Related
I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
I have a property with late initialization.
Now I want to provide a live data which does not emit anything until the property is initialized completely.
How to do this in proper Kotlin way?
class SomeConnection {
val data: Flow<SomeData>
...
class MyViewModel {
private lateinit var _connection: SomeConnection
// private val _connection: CompletableDeferred<SomeConnection>()
val data = _coonection.ensureInitilized().data.toLiveData()
fun connect(){
viewModelScope.launch {
val conn = establishConnection()
// Here I have to do something for the call ensureInitilized to proceed
}
}
private suspend fun establishConnection(){
...
}
Declare a MutableLiveData emitting values of type SomeConnection and a corresponding LiveData.
private val _connectionLiveData = MutableLiveData<SomeConnection>()
val connectionLiveData: LiveData<SomeConnection> = _connectionLiveData
Then assign value to _connectionLiveData when _connection is initialized:
if (::_connection.isInitialized()) _connectionLiveData.value = _connection
(or _connectionLiveData.postValue(_connection) if your code works concurrently)
Now observe this LiveData in another place in code, I'll use fragment here for the sake of example:
override fun firstOnViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.connectionLiveData.observe(this, ::sendData)
}
Then send the desired data via the corresponding view model method
I have two fragments.
One activity which is ideal, I use nav Host to navigate from 1st fragment to another. I do not have a database as of now. I make a call in the repository using retrofit which returns the result as OutPut which is sealed class, I got a result of API call into first fragment ViewModel and I can observe it in first fragment. Now how do I send that to Second fragment or second ViewModel. what is the best solution here? I do not want to implement the database. I do not want to make another call by creating a repository for the second ViewModel and call the same method. I also want to observe any changes in list which I can do by DiffUtil if am I correct? What is the best solution in this case? Below is my code. How can I send wordResponse live data in the second fragment adapter and also observe changes.
My Repository
class DictionaryRepository internal constructor(private val networkService: NetworkService) {
companion object {
#Volatile
private var dictionaryRepoInstance: DictionaryRepository? = null
fun getInstance(dictionaryService: NetworkService) =
dictionaryRepoInstance ?: synchronized(this) {
dictionaryRepoInstance
?: DictionaryRepository(dictionaryService).also { dictionaryRepoInstance = it }
}
}
/**
* Fetch a new searched word from the network
*/
suspend fun fetchRecentSearchedWord(term: CharSequence) = try {
val response = networkService.retrofitClient().makeCallForWordDefinition(term)
OutputResult.Success(response.list)
} catch (t: Throwable) {
OutputResult.Error(t)
}
}
MyViewModel
class SearchFragmentViewModel internal constructor(
private val dictionaryRepository: DictionaryRepository) : ViewModel() {
/** Show a loading spinner if true*/
private val _spinner = MutableLiveData(false)
val spinner: LiveData<Boolean> get() = _spinner
/**take the data into the result live data*/
private val _wordResponse = MutableLiveData<OutputResult>()
val wordResponse: LiveData<OutputResult> get() = _wordResponse
fun makeAPICallWithSuspendFunction(term: CharSequence) {
_spinner.value = true
viewModelScope.launch(Dispatchers.Main) {
val result = dictionaryRepository.fetchRecentSearchedWord(term)
_wordResponse.value = when (result) {
is OutputResult.Success -> {
OutputResult.Success(result.output)
}
is OutputResult.Error -> {
OutputResult.Error(result.throwable)
}
}
}
_spinner.value = false
}
}
I am now stuck and currently wondering why my mutable arraylist returns null even if it is being updated with postvalue(). I tried to display it using Toast and it displayed [] which I think is null. It had no space in between so it looked like a box. I did toString() it as well in order to show the text. How would I be able to solve this problem?
Here is my Main Activity:
val list = ArrayList<String>()
list.add("text1")
list.add("text2")
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
viewmodel.Testlist.postValue(list)
ViewModel:
class viewmodel: ViewModel() {
val Testlist: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
Testlist.value = arrayListOf()
}
}
Fragment:
Top area:
activity?.let {
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
observeInput(viewmodel)
}
Bottom area:
private fun observeInput(viewmodel: viewmodel) {
viewmodel.Testlist.observe(viewLifecycleOwner, Observer {
it?.let {
Toast.makeText(context, it.toString(), Toast.LENGTH_LONG).show()
}
})
}
You post the value to the LiveData object in the activity's viewmodel, which isn't the same instance as the fragment's viewmodel. Let's take look at the way you instantiate the viewmodel in your fragment:
activity?.let {
// activity can be refered by the implicit parameter `it`
// `this` refers to the current fragment hence it's the owner of the view model
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
observeInput(viewmodel)
}
To get a viewmodel that is shared between your activity and fragment you have to pass the activity as its owner:
activity?.let { val viewmodel = ViewModelProviders.of(it).get(viewmodel::class.java) }
Probably you can see developer guide example to resolve your problem
https://developer.android.com/topic/libraries/architecture/viewmodel.html#kotlin
// shared viewmodel
class SharedViewModel : ViewModel() {
private val usersList: MutableLiveData<List<String>>()
fun getUsers(): LiveData<List<String>> {
return usersList
}
fun setUsers(users: List<String>) {
usersList.value = users
}
}
// Attach ViewModel In Activity onCreate()
val model = ViewModelProviders.of(this)[SharedViewModel::class.java]
val list = arrayListOf<String>()
list.add("user1")
list.add("user2")
model.setUsers(list)
// Get same ViewModel instance In fragment onCreateView()
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
You can use this :
fun <T : Any?> MutableLiveData<ArrayList<T>>.default(initialValue: ArrayList<T>) = apply { setValue(initialValue) }
and then use this function as below:
viewmodel.Testlist.default(ArrayList())
For me, I have a BaseActivity that other activities extend from it :
class UAppCompatActivity : AppCompatActivity() {
protected fun <T : Any?> MutableLiveData<ArrayList<T>>.default(initialValue: ArrayList<T>) = apply { setValue(initialValue) }
protected fun <T> MutableLiveData<ArrayList<T>>.addItem(item: T) {
val updatedItems = this.value as ArrayList
updatedItems.add(item)
this.value = updatedItems
}
protected fun <T> MutableLiveData<ArrayList<T>>.deleteItem(item: T) {
val updatedItems = this.value as ArrayList
updatedItems.remove(item)
this.value = updatedItems
}
...
have you used the same instance of your view model? or have you defined another view model in the fragment class? The issue could be that you're accessing a different instance of the view model and not the one were the MutableLiveData was updated
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()
}