lateinit var for LiveData - android

In my ViewModel I have a lateinit var to hold some LiveData. The way this variable is initialized depends on the data and the current date. Can't do it in SQL. This is the ViewModel:
class MainViewModel {
lateinit var timeStamps: LiveData<List<TimeStamp>>
init {
viewModelScope.launch {
val db = RoomDB.getInstance(application).timeStampDao()
val lastTimeStamp = db.getLast()
if (lastTimeStamp == null
|| (lastTimeStamp.instant < setToStartOfDay(Calendar.getInstance()).timeInMillis)
&& lastTimeStamp.action == ACTION.END_WORK) {
timeStamps = db.getAllAfterLive(Calendar.getInstance().timeInMillis)
} else {
db.getLastAction(ACTION.START_WORK)?.let { lastStartWork ->
val startOfDay = setToStartOfDay(initCalendar(lastStartWork.instant)).timeInMillis
db.getFirstActionAfter(ACTION.START_WORK, startOfDay)?.let {
timeStamps = db.getAllAfterLive(it.instant)
}
}
}
Here I access timeStamps in my Activity:
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }
This leads to a UninitializedPropertyAccessException: onCreate runs faster than the timeStamps initialization launched in parallel.
I fixed this by introducing another lateinit var for a callback:
class MainViewModel {
lateinit var timeStamps: LiveData<List<TimeStamp>>
lateinit var timeStampsInitializedCallback: () -> Unit
init {
viewModelScope.launch {
// inspect the data and initialize timeStamps
timeStampsInitializedCallback()
}
which I initialize in onCreate:
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.timeStampsInitializedCallback = {
viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }
}
This works, but it introduces a race condition. Should the initialization for timeStamps unexpectedly finish before the callback is initialized, I'd get another UninitializedPropertyAccessException and be back where I started.
How can I improve this code?

You can also use liveData builder function:
class MainViewModel {
val timeStamps: LiveData<List<TimeStamp>> = liveData {
// inspect the data and initialize timeStamps
emit(timeStamps) // emit list of TimeStamps
emitSource(liveData) // emit another LiveData
}
}
// in Activity
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.timeStamps.observe(this) { list -> recordsAdapter.submitList(list) }
}
The liveData code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive.

The simplest option seems like MutableLiveData:
class MainViewModel {
private val _timeStamps = MutableLiveData<List<TimeStamp>>()
val timeStamps: LiveData<List<TimeStamp>> = _timeStamps
init {
viewModelScope.launch {
// inspect the data and set a value on _timeStamps
}
Depending on what the coroutine is doing, there may be other options (e.g., asLiveData() on a Flow, MediatorLiveData).

Related

Cannot reassign variable inside observer viewmodel

So I created MVVM app in kotlin to fetch movies from TMDB api, using injections and coroutines.
My problem is that I cannot copy the list of returned movies into a new list I created or reassign any variables inside the livedata observer from the MainActivity the values of variables stays the same as they were after exit the scope.
MainActivity class:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val viewModel:MoviesViewModel by lazy {
ViewModelProvider(this)[MoviesViewModel::class.java]
}
private lateinit var list: MutableList<Movies>
private var number:Int=1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
getData()
list
number
}
private fun getData(){
list= mutableListOf()
viewModel.getData(page = "1").observe(this#MainActivity,Observer{ item ->
item?.let { res ->
when (res.status) {
Status.SUCCESS -> {
var tmpList = item.data?.body()?.results
list= tmpList!!.toMutableList()
number+=1
}
Status.ERROR -> {
res.message?.let { Log.e("Error", it) }
}}}
})}}
ViewModel class:
class MoviesViewModel #ViewModelInject constructor(var repository: MoviesRepository): ViewModel() {
fun getData(page:String)= liveData(Dispatchers.IO){
emit(Resource.loading(data = null))
try {
emit(Resource.success(data=repository.getMovies(api_key = Constants.API_KEY,
start_year=Constants.START_YEAR, end_year = Constants.END_YEAR,page = page)))
}catch (e:Exception){
emit(e.message?.let { Resource.error(message = it, data = null) })
}
}
}
As you can see I tried to change the value of number and load the list into my new list but outside the scope the values returned to be what they were before.
Very thankful for anyone who can assist.
Update:
So I tried to initialized all the items inside the success case and it worked I guess there is no other way to change the values outside the scope.

How to properly set Observable in the Activity to Pass data from API call in view model into Activity + Data Class for the list. Android Compose

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.

LiveData lazy init with coroutines not working

I want to load data from an API when activity is started. Currently, I call a view model's method from the activity to load data and it's working fine, but I don't know if it's the best way to do it:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData.observe(this) {
//do things with the data
}
lifeCycleScope.launch { viewModel.loadData() }
}
ViewModel
class MyViewModel : ViewModel() {
val myData = MutableLiveData<MyData>()
suspend fun loadData() = withContext(Dispatchers.IO) {
val data = api.getData()
withContext(Dispatchers.Main) {
myData.value = data
}
}
}
I have seen some examples using lazy initialization, but I don't know how to implement it with coroutines. I have tried this:
Activity
override fun onCreate(savedInstanceState: Bundle?) {
//initialize stuff...
viewModel.myData().observe(this) {
//do things with the data
}
}
ViewModel
private val myData : MutableLiveData<MyData> by lazy {
MutableLiveData<MyData>().also {
viewModelScope.launch {
loadData()
}
}
}
fun myData() = myData
suspend fun loadData() = // same as above
But data is not fetched and nothing is displayed.
If you've added dependency livedata-ktx then you can use livedata builder to also have API call in same block and emit. Checkout how you can do it:
class MyViewModel : ViewModel() {
val myData: LiveData<MyData> = liveData {
val data = api.getData() // suspended call
emit(data) // emit data once available
}
}

ViewModel Fragment Recreates On Screen Rotation

I'm building an application with latest android architecture components. I'm using firebase firestore as a database with jetpack navigation(Bottom nav). I'm successfully able to display data from DB. But Whenever I rotate mt screen the store fragment recreates & makes request to DB.
Repo
override fun getAllStores() = callbackFlow<State<List<Store>>> {
// Emit loading state
send(State.loading())
val listener = remoteDB.collection(Constants.COLLECTION_STORES)
.addSnapshotListener { querySnapshot, exception ->
querySnapshot?.toObjects(Store::class.java)?.let { store ->
// Emit success state with data
offer(State.success(store))
}
exception?.let {
// emit exception with message
offer(State.failed(it.message!!))
cancel()
}
}
awaitClose {
listener.remove()
cancel()
}
}.catch {
// Thrown exception on State Failed
emit(State.failed(it.message.toString()))
}.flowOn(Dispatchers.IO)
ViewModel
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores()
}
Store Fragment
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreFragment : Fragment(R.layout.fragment_store) {
private lateinit var storeAdapter: StoreAdapter
private val viewModel: StoreViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as MainActivity).supportActionBar?.title = getString(R.string.store_title)
setUpRV()
// get all stores
lifecycleScope.launch {
getAllStores()
}
}
private suspend fun getAllStores() {
viewModel.getAllStores().collect { state ->
when (state) {
is State.Loading -> {
store_progress.show()
}
is State.Success -> {
storeAdapter.differ.submitList(state.data)
store_progress.animate().alpha(0f)
.withEndAction {
store_rv.animate().alpha(1f)
store_progress.hide()
}
}
is State.Failed -> {
store_progress.hide()
activity?.toast("Failed! ${state.message}")
}
}
}
}
private fun setUpRV() {
storeAdapter = StoreAdapter()
store_rv.apply {
adapter = storeAdapter
addItemDecoration(SpacesItemDecorator(16))
}
}
}
Main activity(Nav graph)
#InternalCoroutinesApi
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// init bottom navigation
bottom_navigation.setupWithNavController(nav_host_fragment.findNavController())
}
}
Every time it recreates my fragment. I don't want to save or retain any views using methods. Because ViewModel used to protect view on screen rotation. Kindly let me know any tips & tricks. Thanks in advance ;)
Flow in itself is not stateful - that is a key difference between it and LiveData. That means that after your collect completes, the next collect starts the callbackFlow from scratch.
This is precisely why the lifecycle-livedata-ktx artifact contains the asLiveData() extension that allows you to continue to use a Flow at the repository layer while maintaining the stateful (and Lifecycle) properties of LiveData for your UI:
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
class StoreViewModel(private val repository: DBInterface = Repo()) : ViewModel() {
fun getAllStores() = repository.getAllStores().asLiveData()
}
You'd change your UI code to continue to use LiveData and observe().
Kotlin is working on a shareIn operation that would allow your ViewModel to save the state of a Flow. That would allow you to use Flow at all layers of your app without requerying information from scratch when the Fragment/Activity that is calling collect gets destroyed and recreated.
you can add android:configChanges="orientation|screenSize|screenLayout" to your manifest for the activity. this should prevents restarts when orientation changes.
check this site and also here are som infos.

Is there a way to achieve this rx flow in Kotlin with coroutines/Flow/Channels?

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.

Categories

Resources