I have an abstract class, with a MediatorLiveData object in it. This object has a number of sources, one of which depends on the childs class, and is abstract in the parent class.
Adding the sources in an init block causes a NullPointerException at runtime, because at the time the init block adds the source, it is still abstract (or so I have been led to believe).
Is there a way to use an abstract LiveData as a source for a MediatorLiveData without having to set that source in a child class? I just want to override val and be done with it, since I definitely will forget to call the addSources() function at some time in the future.
(I am aware that this example is not the most useful way to do this exact thing, but I didn't want to add unneccesary complexity)
Example:
abstract class MyClass: ViewModel(){
private val _myMediator = MediatorLiveData<String>()
protected abstract val mySource: LiveData<String>
val myObservable: LiveData<String>
get() = _myMediator
// This will cause a NullPointerException at runtime
init{
_myMediator.addSource(mySource){ _myMediator.value = it }
}
//This should work, but requires this to be called in child class
protected fun addSources(){
_myMediator.addSource(mySource){ _myMediator.value = it }
}
}
class myChild: MyClass(){
override val mySource = Transformations.map(myRepository.someData) { it.toString() }
// This is where init { addSources() } would be called
}
After reading Stachu's anwser, I decided to go with this, which I didn't test butI think should work:
abstract class MyFixedClass: ViewModel(){
private val _myMediator: MediatorLiveData<String> by lazy{
MediatorLiveData<String>().apply{
addSource(mySource){ this.value = it }
}
}
protected abstract val mySource: LiveData<String>
val myObservable: LiveData<String>
get() = _myMediator
}
class MyChild: MyFixedClass(){
override val mySource = Transformations.map(myRepository.someData) { it.toString() }
}
how about using lazy evaluation, e.g. something like this
abstract class MyClass : ViewModel() {
private val _myMediator = MediatorLiveData<String>()
private val _mySource: LiveData<String> by lazy { mySource() }
protected abstract fun mySource(): LiveData<String>
val myObservable: LiveData<String>
get() = _myMediator
init {
_myMediator.addSource(_mySource) { _myMediator.value = it }
}
}
class myChild : MyClass() {
override fun mySource() = Transformations.map(myRepository.someData) { it.toString() }
}
Related
The following class has a method than can load a list of users and store it in a LiveData wrapper:
class UserLoader {
...
val loadedUsersLiveData: MutableLiveData<List<User>> = MutableLiveData()
fun loadUsers() {
...
val userRequest: Call<UserResponse> = userApi.loadUsers()
userRequest.enqueue(object : Callback<UserResponse> {
...
override fun onResponse(call: Call<UserResponse>, response: Response<UserResponse>) {
...
loadedUsersLiveData.value = ... // assigns the list of users returned
}
...
}
}
How would I initialize the LiveData variable in my class below to the value of the one that's fetched in the class above?
class UserTableViewModel : ViewModel() {
// TODO: initialize usersLiveData to UserLoader's loadedUsersLiveData
val usersLiveData: LiveData<List<User>> // ??
fun loadUsers() {
UserLoader().loadUsers()
}
}
class UserTableViewModel : ViewModel() {
private val userLoader = UserLoader()
// TODO: initialize usersLiveData to UserLoader's loadedUsersLiveData
val usersLiveData: LiveData<List<User>> = userLoader.loadedUsersLiveData
fun loadUsers() {
userLoader.loadUsers()
}
}
Don't directly expose mutable properties always expose immutable properties for reading the values.
on side note: you can make class UserLoader() to object if you want to safe to call it from everywhere or don't have constructor values.
LiveData is an interface but what you can do is initialize it through MutableLiveData or another LiveData of same type which in your case from UserLoader.
You can do:
class UserTableViewModel : ViewModel() {
private val _userLiveData = MutableLiveData<List<User>>()
val usersLiveData: LiveData<List<User>> get() = _userLiveData // observe it in the view..
// don't forget to call this function...
fun updateUserList() {
_userLiveData.value = UserLoader().loadedUsersLiveData
}
fun loadUsers() {
UserLoader().loadUsers()
}
}
or
class UserTableViewModel : ViewModel() {
//observe it in view..
val usersLiveData: LiveData<List<User>> = UserLoader().loadedUsersLiveData
}
fun loadUsers() {
UserLoader().loadUsers()
}
I would like to assign one property either lazy or in a "normal way", but the problem is, that my value is always cast to "Any". I cannot use the "by" keyword, when I assign a property conditionally. Here is my current approach
abstract class IWorkerContract(private val isLazy: Boolean = false) {
private val workRequest = if (isLazy) {
// Type mismatch. Required: OneTimeWorkRequest Found: Lazy<OneTimeWorkRequest>
lazy {
OneTimeWorkRequestBuilder<Worker>.build()
}
} else {
OneTimeWorkRequestBuilder<Worker>.build()
}
}
Edit Testing
abstract class IWorkerContract(private val isLazy: Boolean = false) {
private val lazyMgr = ResettableLazyManager()
private val workRequest by if (isLazy) {
// Type 'TypeVariable(<TYPE-PARAMETER-FOR-IF-RESOLVE>)' has no method 'getValue(Test, KProperty<*>)' and thus it cannot serve as a delegate
resettableLazy(lazyMgr) {
OneTimeWorkRequestBuilder<Worker>.build()
}
} else {
OneTimeWorkRequestBuilder<Worker>.build()
}
Lazy Delegate
class ResettableLazy<PROPTYPE>(
private val manager: ResettableLazyManager,
private val init: () -> PROPTYPE,
) : Resettable {
#Volatile
private var lazyHolder = initBlock()
operator fun getValue(thisRef: Any?, property: KProperty<*>): PROPTYPE = lazyHolder.value
override fun reset() {
lazyHolder = initBlock()
}
private fun initBlock(): Lazy<PROPTYPE> = lazy {
manager.register(this)
init()
}
}
fun <PROPTYPE> resettableLazy(
manager: ResettableLazyManager,
init: () -> PROPTYPE,
): ResettableLazy<PROPTYPE> = ResettableLazy(manager, init)
value is always cast to "Any"
Yes, because function lazy { } creates a new instance of Lazy<OneTimeWorkRequest>, not OneTimeWorkRequest, those types are incompatible. I don't understand your requirement exactly, but problem can be solved by providing a custom Lazy implementation, e.g.
class InitializedLazy<T>(override val value: T) : Lazy<T> {
override fun isInitialized(): Boolean = true
}
Usage:
abstract class IWorkerContract(private val isLazy: Boolean = false) {
private val workRequest by if (isLazy) {
lazy { OneTimeWorkRequestBuilder<Worker>().build() }
} else {
InitializedLazy(OneTimeWorkRequestBuilder<Worker>().build())
}
}
You could split it up in 2 separate variables:
abstract class IWorkerContract(private val isLazy: Boolean = false) {
private val lazyWorkRequest by lazy {
OneTimeWorkRequestBuilder<Worker>.build()
}
private val workRequest
get() = when {
isLazy -> lazyWorkRequest
else -> OneTimeWorkRequestBuilder<Worker>.build()
}
}
Because of get(), lazyWorkRequest will not be initialised immediately but only when needed.
But more importantly: why is this behaviour needed, what is the harm of always using lazy?
Also, what is the intended purpose of ResettableLazy? It looks like all you want to have a var and this is the solution to solve the missing getValue() or Type mismatch. Is that correct?
It feels to me your question is too specific, too technical. Could you explain without using Kotlin what kind of behaviour you need?
If you access your property in the constructor, if will be computed at instantiation time.
class Foo(val isLazy: Boolean){
val bar: Int by lazy { computeValue() }
init { if (!isLazy) bar }
}
LeakCanary is telling me that one of my ViewModels is leaking but after playing around for 2 days I can't get the leak to go away.
Here is why LeakCanary shows
Here is the Fragment getting the ViewModel
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
getStrains(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
Here is the ViewModel
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
val strainList = MutableLiveData<List<MinimalStrain>>()
fun getStrains(breederId: String) {
viewModelScope.launch {
breederRepository.getMinimalStrains(breederId).observeForever {
strainList.value = it
}
}
}
}
Here is the BreederRepository:
class BreederRepository(context: Context) {
private val dao: BreederDao
private val breederApi = RetrofitClientInstance.getInstance(context).breederAndStrainIdsApi
init {
val database: Db = Db.getInstance(
context
)!!
dao = database.breederDao()
}
suspend fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
withContext(Dispatchers.IO) {
dao.getMinimalStrains(breederId)
}
}
Here is the Db class
#Database(
entities = [Breeder::class, Strain::class],
version = 1,
exportSchema = true)
#TypeConverters(RoomDateConverter::class)
abstract class Db : RoomDatabase() {
abstract fun breederDao(): BreederDao
companion object {
private var instance: Db? = null
#JvmStatic
fun getInstance(context: Context): Db? {
if (instance == null) {
synchronized(Db::class) {
instance = Room.databaseBuilder(
context.applicationContext,
Db::class.java, "seedfinder_db"
)
.build()
}
}
return instance
}
}
}
You're using observeForever, which, as the name suggest, will keep observing forever, even after your ViewModel is cleared. Room does not require using a suspend method for DAO methods that return a LiveData and that is never the right approach in any case - LiveData is already asynchronous.
Instead, you should be transforming your LiveData, using your breederId as the input to your strainList LiveData:
class ViewBreederViewModel(application: Application) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
private val currentBreederId = MutableLiveData<String>()
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = currentBreederId.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
private fun setBreederId(breederId: String) {
currentBreederId.value = breederId
}
}
Where your getMinimalStrains becomes:
fun getMinimalStrains(breederId: String): LiveData<List<MinimalStrain>> =
dao.getMinimalStrains(breederId)
And you use it by setting your breederId in your UI and observing your strainList as before:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java).apply {
setBreederId(arguments?.getString(BREEDER_ID_KEY, "")!!)
}
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
If you're using Saved State module for ViewModels (which is the default if you're using the latest stable Fragments / Activity libraries), then you can use SavedStateHandle, which is automatically populated from your Fragment's arguments and skip the setBreederId() entirely:
class ViewBreederViewModel(
application: Application,
savedStateHandle: SavedStateHandle
) : AndroidViewModel(application) {
private val breederRepository = BreederRepository(application)
// Here we use the switchMap method from the lifecycle-livedata-ktx artifact
val strainList: LiveData<String> = savedStateHandle
.getLiveData(BREEDER_ID_KEY) // Automatically populated from arguments
.switchMap {
breederId -> breederRepository.getMinimalStrains(breederId)
}
}
Which means your code can simply become:
viewModel = ViewModelProvider(this).get(ViewBreederViewModel::class.java)
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
And if you use the fragment-ktx artifact, you can simplify this further to:
// Move this to where you declare viewModel
val viewModel: ViewBreederViewModel by viewModels()
viewModel.strainList.observe(viewLifecycleOwner) { strainList ->
// use your updated list
}
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 trying out Kotlin Coroutines and Flow for the first time and I am trying to reproduce a certain flow I use on Android with RxJava with an MVI-ish approach, but I am having difficulties getting it right and I am essentially stuck at this point.
The RxJava app looks essentially like this:
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.None
)
sealed class RenderEvent {
object None : RenderEvent()
class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
MainActivity has an instance of a PublishSubject with a Event type. Ie MainActivityView.Event.OnViewInitialised, MainActivityView.Event.OnError etc. The initial Event is sent in onCreate() via the subjects's .onNext(Event) call.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedSubject: PublishSubject<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
Observable.mergeArray(
onViewInitialisedSubject
.toFlowable(BackpressureStrategy.BUFFER)
.toObservable()
).observeOn(
Schedulers.io()
).compose(
viewModel()
).observeOn(
AndroidSchedulers.mainThread()
).subscribe(
::render
).addTo(
subscriptions
)
onViewInitialisedSubject
.onNext(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
MainActivityViewModel.kt
These Event's are then picked up by a MainActivityViewModel class which is invoked by .compose(viewModel()) which then transform the received Event into a sort of a new State via ObservableTransformer<Event, State>. The viewmodel returns a new state with a renderEvent in it, which can then be acted upon in the MainActivity again via render(state: MainActivityView.State)function.
#MainActivityScope
class MainActivityViewModel #Inject constructor(
private var state: MainActivityView.State
) {
operator fun invoke(): ObservableTransformer<MainActivityView.Event, MainActivityView.State> = onEvent
private val onEvent = ObservableTransformer<MainActivityView.Event,
MainActivityView.State> { upstream: Observable<MainActivityView.Event> ->
upstream.publish { shared: Observable<MainActivityView.Event> ->
Observable.mergeArray(
shared.ofType(MainActivityView.Event.OnViewInitialised::class.java)
).compose(
eventToViewState
)
}
}
private val eventToViewState = ObservableTransformer<MainActivityView.Event, MainActivityView.State> { upstream ->
upstream.flatMap { event ->
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
}
}
}
private fun onViewInitialisedEvent(): Observable<MainActivityView.State> {
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
return state.asObservable()
}
}
Could I achieve sort of the same flow with coroutines/Flow/Channels? Possibly a bit simplified even?
EDIT:
I have since found a solution that works for me, I haven't found any issues thus far. However this solution uses ConflatedBroadcastChannel<T> which eventually will be deprecated, it will likely be possible to replace it with (at the time of writing) not yet released SharedFlow api (more on that here.
The way it works is that the Activity and viewmodel shares
a ConflatedBroadcastChannel<MainActivity.Event> which is used to send or offer events from the Activity (or an adapter). The viewmodel reduce the event to a new State which is then emitted. The Activity is collecting on the Flow<State> returned by viewModel.invoke(), and ultimately renders the emitted State.
MainActivityView.kt
object MainActivityView {
sealed class Event {
object OnViewInitialised : Event()
data class OnButtonClicked(val idOfItemClicked: Int) : Event()
}
data class State(
val renderEvent: RenderEvent = RenderEvent.Idle
)
sealed class RenderEvent {
object Idle : RenderEvent()
data class DisplayText(val text: String) : RenderEvent()
}
}
MainActivity.kt
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>
private var isInitialised: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
init()
}
private fun init() {
if (!isInitialised) {
lifecycleScope.launch {
viewModel()
.flowOn(
Dispatchers.IO
).collect(::render)
}
eventChannel
.offer(
MainActivityView.Event.OnViewInitialised
)
isInitialised = true
}
}
private suspend fun render(state: MainActivityView.State): Unit =
when (state.renderEvent) {
MainActivityView.RenderEvent.Idle -> Unit
is MainActivityView.RenderEvent.DisplayText ->
renderDisplayText(text = state.renderEvent.text)
}
private val renderDisplayText(text: String) {
// render text
}
}
MainActivityViewModel.kt
class MainActivityViewModel constructor(
private var state: MainActivityView.State = MainActivityView.State(),
private val eventChannel: ConflatedBroadcastChannel<MainActivityView.Event>,
) {
suspend fun invoke(): Flow<MainActivityView.State> =
eventChannel
.asFlow()
.flatMapLatest { event: MainActivityView.Event ->
reduce(event)
}
private fun reduce(event: MainActivityView.Event): Flow<MainActivityView.State> =
when (event) {
MainActivityView.Event.OnViewInitialised -> onViewInitialisedEvent()
MainActivityView.Event.OnButtonClicked -> onButtonClickedEvent(event.idOfItemClicked)
}
private fun onViewInitialisedEvent(): Flow<MainActivityView.State> = flow
val renderEvent = MainActivityView.RenderEvent.DisplayText(text = "hello world")
state = state.copy(renderEvent = renderEvent)
emit(state)
}
private fun onButtonClickedEvent(idOfItemClicked: Int): Flow<MainActivityView.State> = flow
// do something to handle click
println("item clicked: $idOfItemClicked")
emit(state)
}
}
Similiar questions:
publishsubject-with-kotlin-coroutines-flow
Your MainActivity can look something like this.
#MainActivityScope
class MainActivity : AppCompatActivity(R.layout.activity_main) {
#Inject
lateinit var subscriptions: CompositeDisposable
#Inject
lateinit var viewModel: MainActivityViewModel
#Inject
lateinit var onViewInitialisedChannel: BroadcastChannel<MainActivityView.Event.OnViewInitialised>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupEvents()
}
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
}
private fun setupEvents() {
if (subscriptions.size() == 0) {
onViewInitialisedChannel.asFlow()
.buffer()
.flowOn(Dispatchers.IO)
.onEach(::render)
.launchIn(GlobalScope)
onViewInitialisedChannel
.offer(
MainActivityView
.Event
.OnViewInitialised
)
}
}
private fun render(state: MainActivityView.State) {
when (state.renderEvent) {
MainActivityView.RenderEvent.None -> Unit
is MainActivityView.RenderEvent.DisplayText -> {
mainActivityTextField.text = state.renderEvent.text
}
}
}
}
I think what you're looking for is the Flow version of compose and ObservableTransformer and as far as I can tell there isn't one. What you can use instead is the let operator and do something like this:
MainActivity:
yourFlow
.let(viewModel::invoke)
.onEach(::render)
.launchIn(lifecycleScope) // or viewLifecycleOwner.lifecycleScope if you're in a fragment
ViewModel:
operator fun invoke(viewEventFlow: Flow<Event>): Flow<State> = viewEventFlow.flatMapLatest { event ->
when (event) {
Event.OnViewInitialised -> flowOf(onViewInitialisedEvent())
}
}
As far as sharing a flow I would watch these issues:
https://github.com/Kotlin/kotlinx.coroutines/issues/2034
https://github.com/Kotlin/kotlinx.coroutines/issues/2047
Dominic's answer might work for replacing the publish subjects but I think the coroutines team is moving away from BroadcastChannel and intends to deprecate it in the near future.
kotlinx-coroutines-core provides a transform function.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/transform.html
it isn't quite the same as what we are used to in RxJava but should be usable for achieving the same result.