I am working on a android project with MVVM structure. I want to use LiveData as recommended.
In the samples there are always just simple objecttypes e.g. String.
But I want to put an more complex/nested objecttype into LiveData.
For example an objectstructure like this:
class ClassA {
private var testVarB = ClassB()
fun getTestVarB(): ClassB {
return this.testVarB
}
fun setTestVarB(classB: ClassB) {
this.testVarB = classB
}
fun setTxt(str: String) {
this.testVarB.getTestVarC().setStr(str)
}
}
class ClassB {
private var testVarC = ClassC()
fun getTestVarC(): ClassC {
return this.testVarC
}
fun setTestVarB(classC: ClassC) {
this.testVarC = classC
}
}
class ClassC {
private var str: String = "class C"
fun getStr(): String {
return this.str
}
fun setStr(str: String) {
if (str != this.str) {
this.str = str
}
}
}
and my ViewModel looks like this:
class MyViewModel : ViewModel() {
var classAObj= ClassA()
private var _obj: MutableLiveData<ClassA> = MutableLiveData()
val myLiveData: LiveData<ClassA> = _obj
init {
_obj.value = classAObj
}
}
and the LiveDataObject is observed in the fragment:
class FirstFragment : Fragment() {
private var viewModel = MyViewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.myLiveData.observe(
requireActivity(),
Observer<ClassA>() {
// should get fired!
Log.d("TAG", "update view")
})
}
}
So if the variable str of ClassC changes the callback should get executed.
I am looking for a smart and simple solution.
I just found this similar post:
LiveData update on object field change
This example got a depth of 1. But I am looking for a solution with arbitrarily depth.
The fact that I can not find a sample of the solution for my problem makes me suspicious.
So I guess my approach is kind of wrong or bad practice anyway.
Maybe I should look for a way breaking things down and observe just simple objects.
Has anyone a solution or opinion to this?
Thanks for your help!
Here is the solution i have worked out:
I am using the PropertyAwareMutableLiveData class from here: LiveData update on object field change
class PropertyAwareMutableLiveData<T : BaseObservable> : MutableLiveData<T>() {
private val callback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
Based on this I extended the model with an iterface/abstract class.
abstract class InterfaceObservable : BaseObservable() {
open fun setNewString(s: String) {
notifyPropertyChanged(BR.str)
}
}
class ClassA : InterfaceObservable() {
private var testVarB = ClassB()
fun getTestVarB(): ClassB {
return this.testVarB
}
fun setTestVarB(classB: ClassB) {
this.testVarB = classB
}
override fun setNewString(s: String) {
super.setNewString(s)
this.testVarB.setNewString(s)
}
}
class ClassB {
private var testVarC = ClassC()
fun getTestVarC(): ClassC {
return this.testVarC
}
fun setTestVarB(classC: ClassC) {
this.testVarC = classC
}
fun setNewString(s: String) {
this.testVarC.setStr(s)
}
}
class ClassC : BaseObservable() {
#Bindable
private var str: String = "class C"
fun getStr(): String {
return this.str
}
fun setStr(str: String) {
if (str != this.str) {
this.str = str
}
}
}
In my ViewModel I use the PropertyAwareMutableLiveData class.
class MyViewModel() : ViewModel() {
var classAObj: ClassA = ClassA()
val myLiveData = PropertyAwareMutableLiveData<ClassA>()
init {
myLiveData.value = classAObj
}
}
In the Fragment I can observe the LiveData object. If ClassC.str changes the observer will get notified and can change the UI.
class MyFragment : Fragment() {
private lateinit var viewModel: MyViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.myLiveData.observe(
viewLifecycleOwner,
Observer<ClassA> {
Log.d("TAG", "change your UI here")
})
}
}
Every property which is relevant in your UI, should only be changeable over the interface given by the class InterfaceObservable.
Thats the reason why this is not a perfect solution.
But maybe it is reasonable in your case.
The issue is from the way you create your ViewModel. You can't directly instantiate it. If you use fragment-ktx artifact you can do like that :
private val model: SharedViewModel by activityViewModels()
The fragment has his own lifecycle. So you should replace requireActivity() by viewLifeCycleOwner
viewModel.myLiveData.observe(
viewLifeCycleOwner,
Observer<ClassA>() {
// should get fired!
Log.d("TAG", "update view")
})
More information here: https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
Related
I'm using the Epoxy library on Android.
What I'm curious about is why the parameter of the lambda expression doesn't get an error when the type doesn't match.
The listener is a lambda expression that takes an Int type as a parameter.
But listener(addDetailClicked) works normally.
Shouldn't it be listener(Int)? or listener({ i -> addDetailClicked(i) }).
Actually, I don't know why it works even after I write the code.
How is this possible?
Model
#EpoxyModelClass(layout = R.layout.item_routine)
abstract class EpoxyRoutineModel() : EpoxyModelWithHolder<EpoxyRoutineModel.Holder>() {
#EpoxyAttribute
var workout: String = "see"
#EpoxyAttribute
var curPos: Int = 0
#EpoxyAttribute
lateinit var listener: (Int) -> Unit // this
override fun bind(holder: Holder) {
holder.workout.text = workout
holder.add_btn.setOnClickListener {
listener(curPos)
}
}
}
Controller
class RoutineItemController(
private val addDetailClicked: (Int) -> Unit)
: EpoxyController() {
private var routineItem : List<RoutineItem>? = emptyList()
override fun buildModels() {
var i:Int =0
routineItem?.forEach {
when(it) {
is RoutineItem.RoutineModel ->
EpoxyRoutineModel_()
.id(i++)
.curPos(i++)
.workout("d")
.listener(addDetailClicked) // why? listener(Int) or listener({ i -> addDetailClicked(i) })
.addTo(this)
}
}
}
}
Fragment
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var epoxyController : RoutineItemController
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
epoxyController = RoutineItemController(::addDetail)
binding.rv.adapter = epoxyController.adapter
binding.rv.itemAnimator = null
return binding.root
}
private fun addDetail(pos: Int) {
vm.addDetail2(pos)
}
}
I believe you missed the fact that EpoxyRoutineModel_ contains setters for data types found in EpoxyRoutineModel. For example, EpoxyRoutineModel.curPos is of type Int, so EpoxyRoutineModel_.curPos() is a function declared as:
fun curPos(Int): EpoxyRoutineModel_
(or similar)
Similarly, EpoxyRoutineModel.listener is of type (Int) -> Unit, so EpoxyRoutineModel_.listener() is declared as:
fun listener((Int) -> Unit): EpoxyRoutineModel_
So listener() is a function that receives another function (which itself receives Int). So we can provide addDetailClicked there.
I am trying to use the Firebase API in my project but Transformations.map for the variable authenticationState in the View Model does not run. I have been following Google's tutorial here (link goes to the ViewModel of that project).
I want to be able to add the Transformations.map code to the FirebaseUserLiveData file later but I cant seem to figure out why it doesn't run.
FirebaseUserLiveData
class FirebaseUserLiveData: LiveData<FirebaseUser?>() {
private val firebaseAuth = FirebaseAuth.getInstance()
private val authStateListener = FirebaseAuth.AuthStateListener { firebaseAuth ->
value = firebaseAuth.currentUser
}
override fun onActive() {
firebaseAuth.addAuthStateListener { authStateListener }
}
override fun onInactive() {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
SearchMovieFragmentViewModel
class SearchMovieFragmentViewModel : ViewModel() {
enum class AuthenticationState {
AUTHENTICATED, UNAUTHENTICATED, INVALID_AUTHENTICATION
}
var authenticationState = Transformations.map(FirebaseUserLiveData()) { user ->
Log.d("TEST", "in the state function")
if (user != null) {
AuthenticationState.AUTHENTICATED
} else {
AuthenticationState.UNAUTHENTICATED
}
}
SearchMovieFragment
class SearchMovieFragment : Fragment(), MovieSearchItemViewModel {
companion object {
fun newInstance() = SearchMovieFragment()
}
private lateinit var searchMovieFragmentViewModel: SearchMovieFragmentViewModel
private lateinit var binding: SearchMovieFragmentBinding
private lateinit var movieRecyclerView: RecyclerView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.search_movie_fragment, container, false)
searchMovieFragmentViewModel = ViewModelProvider(this).get(SearchMovieFragmentViewModel::class.java)
binding.lifecycleOwner = this
binding.viewmodel = searchMovieFragmentViewModel
binding.signOutButton.setOnClickListener {
AuthUI.getInstance().signOut(requireContext())
}
searchMovieFragmentViewModel.authenticationState.observe(viewLifecycleOwner, Observer { state ->
when (state) {
AUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.VISIBLE
UNAUTHENTICATED -> searchMovieFragmentViewModel.signedIn = View.GONE
}
})
return binding.root
}
}
Should be .addAuthStateListener(authStateListener) instead of { authStateListener }
That is because you are not keeping the reference of FirebaseUserLiveData() once you start observing it like Transformations.map(FirebaseUserLiveData()) { user ->.
You have to have the reference of the Livedata you are mapping or transferring to another form of Livedata.
It is like a chain of observation, All LiveData in the chain should be observed or should have some kind of observer down the line, The main use-case is to transform some form of livedata to something you want, For Example:
class YourRepository{ // your repo, that connected to a network that keeps up to date some data
val IntegerResource: LiveData<Int> = SomeRetrofitInstance.fetchFromNetwork() //updating some resource from network
}
class YourViewModel{
val repository = YourRepository()
//this will start observe the repository livedata and map it to string resource
var StringResource: Livedata<String> = Transformations.map( repository.IntegerResource ) { integerValue ->
integerValue.toString()
}
My Point is you have to keep alive the LiveData you are transforming. Hope helped.
I have two LiveData, aMVoice1, and aMVoice2.
I hope to check if they are equal.
I know I need to use observe to get the value of a LiveData.
so I think isEqual = (mDetailViewModel.aMVoice1.value==mDetailViewMode2.aMVoice1.value ) is wrong.
But I think there are some problems with fun observeVoice(), how can I fix it?
class FragmentDetail : Fragment() {
private lateinit var binding: LayoutDetailBinding
private val mDetailViewModel by lazy {
...
}
var isEqual=false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.lifecycleOwner = this.viewLifecycleOwner
binding.aDetailViewModel=mDetailViewModel
isEqual = (mDetailViewModel.aMVoice1.value==mDetailViewMode2.aMVoice1.value ) // I don't think it's correct.
observeVoice()
return binding.root
}
fun observeVoice() {
mDetailViewModel.aMVoice1.observe(viewLifecycleOwner){value1->
isEqual = (value1==mDetailViewModel.aMVoice2.value) // mDetailViewModel.aMVoice2.value maybe null
}
}
}
class DetailViewModel(private val mDBVoiceRepository: DBVoiceRepository, private val voiceId1:Int,private val voiceId2:Int) : ViewModel() {
val aMVoice1=mDBVoiceRepository.getVoiceById(voiceId1)
val aMVoice2=mDBVoiceRepository.getVoiceById(voiceId2)
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
fun getVoiceById(id:Int)=mDBVoiceDao.getVoiceById(id)
}
#Dao
interface DBVoiceDao{
#Query("SELECT * FROM voice_table where id=:id")
fun getVoiceById(id:Int):LiveData<MVoice>
}
data class MVoice(
#PrimaryKey (autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
var name: String = "",
var path: String = ""
)
Added Content
Is it Ok for the following code?
fun observeVoice() {
mDetailViewModel.aMVoice1.observe(viewLifecycleOwner){value1->
mDetailViewModel.aMVoice2.observe(viewLifecycleOwner){value2->
isEqual = (value1==value2)
}
}
}
According to the official documents, the best way to achieve a solution for such cases is to use MediatorLiveData as a LiveData merger. Using it, you can check the equality of values when a new value is posted on either of LiveDatas:
class DetailViewModel(...) : ViewModel() {
val areMVoicesEqual = MediatorLiveData<Boolean>().apply {
addSource(aMVoice1) { postValue(it == aMVoice2.value) }
addSource(aMVoice2) { postValue(it == aMVoice1.value) }
}
}
Then:
fun observeVoice() {
mDetailViewModel.areMVoicesEqual.observe(viewLifecycleOwner){ equality ->
// do whatever you want with `equality`
}
}
Note that Added Content snippet you mentioned is not correct. In fact, in this case, every time a value is being observed on aMVoice1, a new Observer starts to observe on aMVoice2 which is not right.
I have one activity with unspecified orientation and there is one fragment attached to that activity that has different layouts for portrait and landscape mode and on that fragment, multiple API calls on a conditional basis, my problem is that when the screen rotates all data was lost and there is a lot of data on that fragment by which I don't want to save each data on saveInstance method. I tried android:configChanges="keyboardHidden|orientation|screenSize", but this didn't solve my problem. I want to handle this problem using viewModel. Please help, Thanks in advance.
Here is my code
Repository
class GetDataRepository {
val TAG = GetDataRepository::class.java.canonicalName
var job: CompletableJob = Job()
fun getData(
token: String?,
sslContext: SSLContext,
matchId: Int
): LiveData<ResponseModel> {
job = Job()
return object : LiveData<ResponseModel>() {
override fun onActive() {
super.onActive()
job.let { thejob ->
CoroutineScope(thejob).launch {
try {
val apiResponse = ApiService(sslContext).getData(
token
)
LogUtil.debugLog(TAG, "apiResponse ${apiResponse}")
withContext(Dispatchers.Main) {
value = apiResponse
}
} catch (e: Throwable) {
LogUtil.errorLog(TAG, "error: ${e.message}")
withContext(Dispatchers.Main) {
when (e) {
is HttpException -> {
value =
Gson().fromJson<ResponseModel>(
(e as HttpException).response()?.errorBody()
?.string(),
ResponseModel::class.java
)
}
else -> value = ResponseModel(error = e)
}
}
} finally {
thejob.complete()
}
}
}
}
}
}
fun cancelJob() {
job.cancel()
}
}
ViewMode:
class DataViewModel : ViewModel() {
val TAG = DataViewModel::class.java.canonicalName
var mListener: DataListener? = null
private val mGetDataRepository: GetDataRepository = GetDataRepository()
fun getData() {
LogUtil.debugLog(TAG, "getData")
if (mListener?.isInternetAvailable()!!) {
mListener?.onStartAPI()
val context = mListener?.getContext()
val token: String? = String.format(
context?.resources!!.getString(R.string.user_token),
PreferenceUtil.getUserData(context).token
)
val sslContext = mListener?.getSSlContext()
if (sslContext != null) {
val getData =
mGetDataRepository.getData(
token
)
LogUtil.debugLog(TAG, "getData ${getData}")
mListener?.onApiCall(getData)
} else {
LogUtil.debugLog(TAG, "getData Invalid certificate")
mListener?.onError("Invalid certificate")
}
} else {
LogUtil.debugLog(TAG, "getData No internet")
mListener?.onError("Please check your internet connectivity!!!")
}
LogUtil.debugLog(TAG, "Exit getData()")
}
}
Activity:
class DataActivity : AppCompatActivity() {
val TAG = DataActivity::class.java.canonicalName
lateinit var fragment: DataFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LogUtil.debugLog(TAG, "onCreate: Enter")
var binding: ActivityDataBinding =
DataBindingUtil.setContentView(this, R.layout.activity_data)
if (savedInstanceState == null) {
fragment = DataFragment.newInstance()
supportFragmentManager.beginTransaction().add(R.id.container, fragment, DataFragment.TAG)
} else {
fragment = supportFragmentManager.findFragmentByTag(DataFragment.TAG) as DataFragment
}
LogUtil.debugLog(TAG, "onCreate: Exit")
}
}
Fragment:
class DataFragment : Fragment(), DataListener {
private var mBinding: FragmentDataBinding? = null
private lateinit var mViewModel: DataViewModel
companion object {
val TAG = DataFragment::class.java.canonicalName
fun newInstance(): DataFragment {
return DataFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding =
DataBindingUtil.inflate(inflater, R.layout.fragment_data, container, false)
mViewModel = ViewModelProvider(this).get(DataViewModel::class.java)
mViewModel.mListener = this
getData()
return mBinding?.root
}
private fun getData() {
LogUtil.debugLog(TAG, "Enter getMatchScore()")
mViewModel.getData()
LogUtil.debugLog(TAG, "Exit getMatchScore()")
}
override fun <T> onApiCall(response: LiveData<T>) {
response.observe(this, Observer {
it as DataResponseModel
//
})
}
}
The lifecycle of viewModel by default is longer than your activity (in your case, screen rotation).
ViewModel will not be destroyed as soon as activity destroyed for configuration change, you can see this link.
You seem to have made a mistake elsewhere in your activity/fragment, please put your activity/fragment code here.
In your fragment you call mViewModel.getData() in your onCreateView, and every time you rotate your activity, this method call and all store data reset and fetched again!, simply you can check data of ViewModel in your fragment and if it's empty call getData(), it also seems your ViewModel reference to your view(Fragment) (you pass a listener from your fragment to your ViewModel) and it is also an anti-pattern (This article is recommended)
I have a fragment with updateToolbar() function. When i try to get my ViewModel, calles UserViewModel on this method i get error:
Can't create ViewModelProvider for detached fragment
When i try to get UserViewModel inside onViewCreated(), all works fine. Why it happens? I call updateToolbar() after onCreateView() and I'm not create any fragment transactions before function called.
I'm started to learn Clean Architecture, and intuitively i think the reason of error can be on it, so i add this code too. I think problem about presenter, but i can't understand where exactly.
PacksFragment:
class PacksFragment : BaseCompatFragment() {
#Inject
lateinit var presenter: PacksFragmentPresenter
private var userViewModel: UserViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
userViewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
LibApp.get().injector.inject(this)
...
presenter.openNewPack(packId)
}
fun updateToolbar() {
Timber.e((userViewModel == null).toString())
Timber.e(userViewModel?.getData()?.value?.coins.toString())
}
}
PacksFragmentPresenter:
class PacksFragmentPresenter #Inject constructor(
private val packsFragment: PacksFragment,
private val getCoinsFromUserCase: GetCoinsFromUserCase
) {
fun openNewPack(packId: Int) {
if (getCoinsFromUserCase.getCoinsFromUser()){
packsFragment.updateToolbar()
}
}
}
GetCoinsFromUserCase:
class GetCoinsFromUserCase {
fun getCoinsFromUser(): Boolean {
val userViewModel = UserViewModel()
userViewModel.takeCoins(10)
return true
}
}
userViewModel:
class UserViewModel : ViewModel(), UserApi {
private val data = MutableLiveData<User>()
fun setData(user: User) {
//Logged fine
Timber.e("User setted")
data.value = user
}
fun getData(): LiveData<User> {
if (data.value == null) {
val user = User()
user.coins = 200
data.value = user
}
//Logged fine, "false"
Timber.e("User getted")
Timber.e("Is user == null? %s", (data.value == null).toString())
return data
}
override fun takeCoins(value: Int) {
//Specially commented it
// getCoins(value)
}
}
UPD:
I make some changes to prevent crush - make userViewModel nullable(update PacksFragment code on the top).
But userViewModel is always null when i call updateToolbar(). Main thing fragment not removed/deleted/invisible/..., it's active fragment with button which called updateToolbar() function.