I am following this video to use Kotlin coroutine : https://www.youtube.com/watch?v=BOHK_w09pVA&t=1326s
, at 9:30 in the video.
But when I write the similar code, it doesn't behave like i expected:
WalletDao.kt
interface WalletDao {
#Query("SELECT * FROM wallet LIMIT 1")
suspend fun getFirstWallet(): Wallet
}
Repository.kt
class Repository private constructor(appContext: Context) {
private val appDatabase = AppDatabase.getInstance(appContext)
//current wallet
private var _currentWallet : MutableLiveData<Wallet> = MutableLiveData()
var currentWallet : LiveData<Wallet> = _currentWallet
suspend fun initFirstWallet()
{
val fW = withContext(Dispatchers.IO)
{
appDatabase.walletDao.getFirstWallet()
}
_currentWallet.value = fW
}
}
MainViewModel.kt
class MainActivityViewModel(private val repository: Repository) : ViewModel()
{
var currentWallet: LiveData<Wallet> = repository.currentWallet
init {
viewModelScope.launch {
repository.initFirstWallet()
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainActivityViewModel
private fun registerObservers() {
...
viewModel.currentWallet.observe(this)
{
if (it != null) {
binding.walletName.text = it.name
binding.walletBalance.text = it.amount.toString()
viewModel.onCurrentWalletChange()
}
}
}
}
When i run the code in MainActivity, sometimes the lambda of viewModel.currentWallet get called and the name of the current wallet is dislpayed, BUT most of the time it doesn't get called.
I have change the code to the folloiwing but not thing has changed:
Repository.kt
class Repository private constructor(appContext: Context) {
suspend fun initFirstWallet()
{
withContext(Dispatchers.IO)
{
_currentWallet.postValue(appDatabase.walletDao.getFirstWallet())
}
}
}
Can you tell me what is wrong with the above code??? Thank you very much!
You don't need to use coroutines for this.
WalletDao.kt
interface WalletDao {
#Query("SELECT * FROM wallet LIMIT 1")
fun getFirstWallet(): LiveData<Wallet>
}
Repository.kt
class Repository private constructor(appContext: Context) {
private val appDatabase = AppDatabase.getInstance(appContext)
fun initFirstWallet() = appDatabase.walletDao.getFirstWallet()
}
MainActivityViewModel.kt
class MainActivityViewModel(private val repository: Repository) : ViewModel()
{
var currentWallet: LiveData<Wallet> = repository.initFirstWallet()
}
This is will work fine as intended without any threading issue.
Related
I followed this tutorial link
But I met a problem that "kotlin.UninitializedPropertyAccessException: lateinit property splashViewModel has not been initialized"
Here is my code
#Module
#InstallIn(SingletonComponent::class)
object MainModule {
#Provides
#Singleton
fun provideDataStoreRepository(
#ApplicationContext context: Context
) = DataStoreRepository(context = context)
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "on_boarding_pref")
class DataStoreRepository(context: Context) {
private object PreferencesKey {
val onBoardingKey = booleanPreferencesKey(name = "on_boarding_completed")
}
private val dataStore = context.dataStore
suspend fun saveOnBoardingState(completed: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKey.onBoardingKey] = completed
}
}
fun readOnBoardingState(): Flow<Boolean> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false
onBoardingState
}
}
}
class SplashViewModel #Inject constructor(
private val repository: DataStoreRepository
) : ViewModel() {
private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
private val _startDestination: MutableState<String> = mutableStateOf(Screen.OnboardingFirstScreen.route)
val startDestination: State<String> = _startDestination
init {
viewModelScope.launch {
repository.readOnBoardingState().collect { completed ->
if (completed) {
_startDestination.value = Screen.MainScreen.route
} else {
_startDestination.value = Screen.OnboardingFirstScreen.route
}
}
_isLoading.value = false
}
}
}
And in my main activity
class MainActivity : ComponentActivity() {
#Inject
lateinit var splashViewModel: SplashViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen().setKeepOnScreenCondition {
!splashViewModel.isLoading.value
}
setContent{
BottomNavWithBadgesTheme {
val screen by splashViewModel.startDestination
....
}
}
It turned out MainModule object have never been used. Is that problem? I'm new to jetpack data store, I just followed it, so I don't know where is the problem and how to fix it. Thank you in advance.
Firstly, it's not about data store. It is about dependency injection. You are trying to get the data from viewmodel when it is not initialized.
To solve the problem:
Mark your viewmodel class with #HiltViewModel annotation
Remove lateinit var keyword and #Inject annotation from viewmodel in your MainActivity
Your viewmodel must be initialized in onCreate function like that:
viewModel: SplashViewModel = hiltViewModel()
I already create some unit test for my view model. but when I println() the result it always return State Loading.. I have tried to read some article and cek in other source code but I'm still not found the answer.
Here is my code from ViewModel :
class PredefineViewModel() : ViewModel() {
private var predefineRepository: PredefineRepository? = PredefineRepository()
private val _predefined = MutableLiveData<String>()
val predefined: LiveData<Resource<Payload<Predefine>>> =
Transformations.switchMap(_predefined) {
predefineRepository?.predefine()
}
fun predefined() {
_predefined.value = "predefined".random().toString()
}
}
Here is my Repository
class PredefineRepository() {
private val api: PredefineApi? = PredefineApi.init()
fun predefine(): BaseMutableLiveData<Predefine> {
val predefine: BaseMutableLiveData<Predefine> = BaseMutableLiveData()
api?.let { api ->
predefine.isLoading()
api.predefined().observe()?.subscribe({ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
}, { error ->
predefine.isError(error)
})
}
return predefine
}
}
Here is my Resources State :
data class Resource<T>(var status: Status? = null, var meta: Meta? = null, var payload: T? =null) {
companion object {
fun <T> success(data: T?, meta: Meta): Resource<T> {
return Resource(Status.SUCCESS, meta, data)
}
fun <T> error(data: T?, meta: Meta): Resource<T> {
return Resource(Status.ERROR, meta, data)
}
fun <T> loading(data: T?, meta: Meta): Resource<T> {
return Resource(Status.LOADING, null, null)
}
}
}
UPDATE TEST CLASS
And, This is sample I try to print and check value from my live data view model :
class PredefineViewModelTest {
#get:Rule
val taskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: PredefineViewModel
private lateinit var repository: PredefineRepository
private lateinit var api: Api
#Before
fun setUp() {
api = Networks().bridge().create(Api::class.java)
repository = PredefineRepository()
viewModel = PredefineViewModel()
}
#Test
fun test_predefined(){
val data = BaseMutableLiveData<Predefine>()
val result = api.predefined()
result.test().await().assertComplete()
result.subscribe {
data.isSuccess(it)
}
`when`(repository.predefine()).thenReturn(data)
viewModel.predefined()
viewModel.predefined.observeForever {
println("value: $it")
println("data: ${data.value}")
}
}
}
UPDATE LOG Results
Why the result from my predefined always:
value: Resource(status=LOADING, meta=null, payload=null, errorData=[])
data: Resource(status=SUCCESS, meta=Meta(code=200, message=success, error=null), payload= Data(code=200, message=success, errorDara =[])
Thank You..
You would require to mock your API response. The unit test won't run your API actually you have to mock that. Please have a look at the attached snippet, It will give you a basic idea of how you can achieve that.
ViewModel:
class MainViewModel(val repository: Repository) : ViewModel() {
fun fetchData(): LiveData<Boolean> {
return Transformations.map(repository.getData()) {
if (it.status == 200) {
true
} else {
false
}
}
}
}
Repo:
open class Repository {
open fun getData() : LiveData<MainModel> {
return MutableLiveData(MainModel(10, 200))
}
}
Test Class:
#RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {
lateinit var mainModel: MainViewModel
#Rule
#JvmField
var rule: TestRule = InstantTaskExecutorRule()
#Mock
lateinit var repo: Repository
init {
MockitoAnnotations.initMocks(this)
}
#Before
fun setup() {
mainModel = MainViewModel(repo)
}
#Test
fun fetchData_success() {
val mainModelData = MainModel(10, 200)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertTrue(it)
}
}
#Test
fun fetchData_failure() {
val mainModelData = MainModel(10, 404)
`when`(repo.getData()).thenReturn(MutableLiveData(mainModelData))
mainModel.fetchData().observeForever {
Assert.assertFalse(it)
}
}
}
I couldn't see your API mock. Your initial status is loading inside LiveData.
{ response ->
response?.let { resource ->
predefine.isSuccess(resource)
}
block is not executing during the test.
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'm trying to use the new liveData builder referenced here to retrieve my data, then transform it into view models. However, my repository code isn't being invoked (at least I'm not able to see it being triggered when I use my debugger). Am I not supposed to use two liveData{ ... } builders? (one in my repository, one in my view model)?
class MyRepository #Inject constructor() : Repository {
override fun getMyContentLiveData(params: MyParams): LiveData<MyContent> =
liveData {
val myContent = networkRequest(params) // send network request with params
emit(myContent)
}
}
class MyViewModel #Inject constructor(
private val repository: MyRepository
) : ViewModel() {
val viewModelList = liveData(Dispatchers.IO) {
val contentLiveData = repository.getContentLiveData(keyParams)
val viewModelLiveData = contentToViewModels(contentLiveData)
emit(viewModelLiveData)
}
private fun contentToViewModels(contentLiveData: LiveData<MyContent>): LiveData<List<ViewModel>> {
return Transformations.map(contentLiveData) { content ->
//perform some transformation and return List<ViewModel>
}
}
}
class MyFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val myViewModel: MyViewModel by lazy {
ViewModelProviders.of(this, viewModelFactory).get(MyViewModel::class.java)
}
lateinit var params: MyParams
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
myViewModel.params = params
myViewModel.viewModelList.observe(this, Observer {
onListChanged(it)
})
}
You could try with the emitSource:
val viewModelList = liveData(Dispatchers.IO) {
emitSource(
repository.getContentLiveData(keyParams).map {
contentToViewModels(it)
}
}
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.