I'm just new with kotlin coroutines. I just created new project for testing livedata but i cant observe data changes. I didn't understand the concept of livedata. When it'll be trigger? Because when i observe ROOM database(Not the coroutines way. i used the MutableLiveData) it was working very well. Observer was always triggered whenever data changed.
I just wanted to clean and modern code. My expectations: when I click btnLogin button (when user login with another account or you can say when data changes) livedata must trigger.
Here is my example:
Retrofit interface:
interface RetroMainClient {
#POST("login.php")
suspend fun login(#Body model: UserLoginModel): Response<UserLoginModel>
companion object {
val getApi: RetroMainClient by lazy {
Retrofit.Builder().baseUrl("https://example.com/")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())).build()
.create(RetroMainClient::class.java)
}
}
}
My repository:
class Repository {
suspend fun getLoginApi(model: UserLoginModel) = RetroMainClient.getApi.login(model)
}
My viewModel:
class MainViewModel : ViewModel() {
fun login(model: UserLoginModel) = liveData(IO) {
try {
emit(Repository().getLoginApi(model))
} catch (e: Exception) {
Log.e("exception", "${e.message}")
}
}
}
and my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
var model = UserLoginModel("user1", "123456")
viewModel.login(model).observe(this, Observer {
if (it.isSuccessful) {
btnLogin.text = it.body()?.username
}
})
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
CoroutineScope(IO).launch {
try {
Repository().getLoginApi(model)
} catch (e: Exception) {
Log.e("exception:", "${e.message}")
}
}
}
}
}
When you call viewModel.login() method you create a new instance of LiveData class. In order to execute corresponding block in viewModel.login() after every click on btnLogin button you need call LiveData.observe() method for every viewModel.login() call.
In MainActivity's onCreate method:
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
viewModel.login(model).observe(this, Observer { data ->
if (it.isSuccessful) {
btnLogin.text = data.body()?.username
}
})
}
ANOTHER APPROACH:
is to launch a coroutine in MainViewModel class and update LiveData field manually:
class MainViewModel : ViewModel() {
val loginResponse: LiveData<Response<UserLoginModel>> = MutableLiveData<Response<UserLoginModel>>()
fun login(model: UserLoginModel) = viewModelScope.launch(IO) {
try {
(loginResponse as MutableLiveData).postValue(Repository().getLoginApi(model))
} catch (e: Exception) {
Log.e("exception", "${e.message}")
}
}
}
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
var model = UserLoginModel("user1", "123456")
viewModel.loginResponse.observe(this, Observer {
if (it.isSuccessful) {
btnLogin.text = it.body()?.username
}
})
btnLogin.setOnClickListener {
model = UserLoginModel("user2", "123456")
viewModel.login(model)
}
}
}
To use viewModelScope in MainViewModel class add dependency to build.gradle file:
final LIFECYCLE_VERSION = "2.2.0-rc03" // add most recent version
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION"
Related
I'd love to observe changes of a shared preference. Here is how I Use Kotlin Flow to do it:
Data source.
interface DataSource {
fun bestTime(): Flow<Long>
fun setBestTime(time: Long)
}
class LocalDataSource #Inject constructor(
#ActivityContext context: Context
) : DataSource {
private val preferences = context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE)
#ExperimentalCoroutinesApi
override fun bestTime() = callbackFlow {
trySendBlocking(preferences, PREF_KEY_BEST_TIME)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == PREF_KEY_BEST_TIME) {
trySendBlocking(sharedPreferences, key)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { // NEVER CALLED
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
#ExperimentalCoroutinesApi
private fun ProducerScope<Long>.trySendBlocking(
sharedPreferences: SharedPreferences,
key: String?
) {
trySendBlocking(sharedPreferences.getLong(key, 0L))
.onSuccess { }
.onFailure {
Log.e(TAG, "", it)
}
}
override fun setBestTime(time: Long) = preferences.edit {
putLong(PREF_KEY_BEST_TIME, time)
}
companion object {
private const val TAG = "LocalDataSource"
private const val PREFS_FILE_NAME = "PREFS_FILE_NAME"
private const val PREF_KEY_BEST_TIME = "PREF_KEY_BEST_TIME"
}
}
Repository
interface Repository {
fun observeBestTime(): Flow<Long>
fun setBestTime(bestTime: Long)
}
class RepositoryImpl #Inject constructor(
private val dataSource: DataSource
) : Repository {
override fun observeBestTime() = dataSource.bestTime()
override fun setBestTime(bestTime: Long) = dataSource.setBestTime(bestTime)
}
ViewModel
class BestTimeViewModel #Inject constructor(
private val repository: Repository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(0L)
val uiState: StateFlow<Long> = _uiState
init {
viewModelScope.launch {
repository.observeBestTime()
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("myTag", "viewModelScope onCompletion")
}
.collect { bestTime ->
_uiState.value = bestTime
}
}
}
fun setBestTime(time: Long) = repository.setBestTime(time)
}
Fragment.
#AndroidEntryPoint
class MetaDataFragment : Fragment(R.layout.fragment_meta_data) {
#Inject
lateinit var timeFormatter: TimeFormatter
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bestTimeView = view.findViewById<TextView>(R.id.best_time_value)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
bestTimeViewModel.uiState
.map { millis ->
timeFormatter.format(millis)
}
.onCompletion { // CALLED WHEN THE SCREEN IS ROTATED OR HOME BUTTON PRESSED
Log.d("MyApp", "onCompletion")
}
.collect {
bestTimeView.text = it
}
}
}
}
}
I've noticed that awaitClose is never called. But this is where my clean-up code is. Please advise. If it's not a good idea to use callbackFlow in the first place, please let me know (as you can see some functions are ExperimentalCoroutinesApi meaning their behaviour can change)
I found a solution that allows me to save a simple dataset such as a preference and observe its changes using Kotlin Flow. It's Preferences DataStore.
This is the code lab and guide I used:
https://developer.android.com/codelabs/android-preferences-datastore#0
https://developer.android.com/topic/libraries/architecture/datastore
and this is my code:
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
data class UserPreferences(val bestTime: Long)
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
interface DataSource {
fun userPreferencesFlow(): Flow<UserPreferences>
suspend fun updateBestTime(newBestTime: Long)
}
class LocalDataSource(
#ApplicationContext private val context: Context,
) : DataSource {
override fun userPreferencesFlow(): Flow<UserPreferences> =
context.dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val bestTime = preferences[PreferencesKeys.BEST_TIME] ?: 0L
UserPreferences(bestTime)
}
override suspend fun updateBestTime(newBestTime: Long) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.BEST_TIME] = newBestTime
}
}
}
private object PreferencesKeys {
val BEST_TIME = longPreferencesKey("BEST_TIME")
}
and the dependency to add to build.gradle:
implementation "androidx.datastore:datastore-preferences:1.0.0"
The problem is, that you are injecting your ViewModel as if it was just a regular class, by using
#Inject
lateinit var bestTimeViewModel: BestTimeViewModel
Because of this, the ViewModel's viewModelScope is never cancelled, and therefor the Flow is collected forever.
Per Documentation, you should use
privat val bestTimeViewModel: BestTimeViewModel by viewModels()
This ensures that the ViewModel's onCleared method, which in turn will cancel the viewModelScope, is called when your Fragment is destroyed.
Also make sure your ViewModel is annotated with #HiltViewModel:
#HiltViewModel
class BestTimeViewModel #Inject constructor(...) : ViewModel()
I load data in recycleView in advance. In order to do that I have following code in onCreate() of Activity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
observeViewModel()
if (savedInstanceState == null) {
mainViewModel.userIntent.offer(MainIntent.FetchUser)
}
}
As you see I offer() when savedInstanceState is null, The problem is when we have process death ( you can simply create it by activating Do not keep activities in developer option), reload of data will not be triggered.
another option is to use it inside init block of ViewModel, but problem is I want to have bellow unit test which I can verify all three states :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runBlockingTest {
`when`(apiService.getUsers()).thenReturn(emptyList())
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository)
viewModel.state.asLiveData().observeForever(observer)
viewModel.userIntent.send(MainIntent.FetchUser)
}
verify(observer, times(3)).onChanged(captor.capture())
verify(observer).onChanged(MainState.Idle)
verify(observer).onChanged(MainState.Loading)
verify(observer).onChanged(MainState.Users(emptyList()))
}
If I use the init block option as soon as ViewModel initialized, send or offer will be called while observeForever did not be used for LiveData in the above unit test.
Here is my ViewModel class :
class MainViewModel(
private val repository: MainRepository
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
What could be the solution for the above scenarios?
The only solution that I found is moving fetchUser method and another _state as MutableStateFlow to Repository layer and observeForever it in Repository for local unit test, as a result I can send or offer userIntent in init block off ViewModel.
I will have following _state in ViewModel :
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = repository.state
val state: StateFlow<MainState>
get() = _state
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
}
}
I have a AmbassadorDAO that has a getAll() : List<Ambassador> that return correctly the list of Ambassadors.
The problem becomes when I refactory my existent code to use DataSource.Factory to paginate my list
Here is the code
Presation Module
Activity
class AmbassadorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val viewModel by viewModel<AmbassadorViewModel>()
val adapter = AmbassadorAdapter(this)
list_of_ambassadors.adapter = adapter
viewModel.ambassadors.observe(this, Observer { adapter.submitList(it) })
viewModel.listAmbassadors()
...
}
...
}
Viewmodel
class AmbassadorViewModel(
...,
private val getAllAmbassadorInteractor: GetAllAmbassadorInteractor
) : ViewModel() {
...
// not working
private val _ambassadors = MutableLiveData<PagedList<Ambassador>>()
// it's working
//private val _ambassadors = MutableLiveData<List<Ambassador>>()
...
// not working
val ambassadors : LiveData<PagedList<Ambassador>>
get() = _ambassadors
// it's working
//val ambassadors : LiveData<List<Ambassador>>
// get() = _ambassadors
...
fun listAmbassadors() {
viewModelScope.launch {
try {
...
// not working
// the data not return anything
// the livedata is notified with null
val data = getAllAmbassadorInteractor.exec()
_ambassadors.value = LivePagedListBuilder(data, 20).build().value
// it's working
//_ambassadors.value = getAllAmbassadorInteractor.exec()
} catch (e: Exception) {
e.printStackTrace()
} finally {
...
}
}
}
}
Domain Module
Boundary between PRESENTATION (my usecase interface)
interface GetAllAmbassadorInteractor {
//suspend fun exec() : List<Ambassador>
suspend fun exec() : DataSource.Factory<Int, Ambassador>
}
Usecase implementation
class GetAllAmbassadorInteractorImpl(
private val repository: AmbassadorRepository
) : GetAllAmbassadorInteractor {
override suspend fun exec() = withContext(Dispatchers.IO) { repository.getAll() }
}
Boundary between DATA (my repository interface)
interface AmbassadorRepository {
...
//suspend fun getAll() : List<Ambassador>
suspend fun getAll() : DataSource.Factory<Int, Ambassador>
...
}
Data Module
Repository implementation
class AmbassadorRepositoryImpl(
private val ambassadorDAO: AmbassadorDAO
) : AmbassadorRepository {
...
override suspend fun getAll() = ambassadorDAO.getAll().map { it.toDomain() }
...
}
My DAO
#Dao
interface AmbassadorDAO {
...
#Query("SELECT * FROM ${AmbassadorEntity.TABLE_NAME} ORDER BY name DESC")
fun getAll(): DataSource.Factory<Int, AmbassadorEntity>
//fun getAll(): List<AmbassadorEntity>
...
}
Where am I doign wrong?
I guess your mistake is on this line in AmbassadorViewModel class:
_ambassadors.value = LivePagedListBuilder(data, 20).build().value
Instead of that use:
_ambassadors.value = LivePagedListBuilder(data, 20).build()
Also refer to this post, maybe it will help.
With the support of Kotlin extension (LifecycleScope) we can easily connect LiveData with Coroutine and you don't need to use backing properties like _ambassadors and make it MutableLiveData.
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 or higher.
Like this is a function which is using Coroutine and returning a LiveData
/**
* Get all news rows livedata pageList from DB using Coroutine.
*/
suspend fun getAllNewsLiveData(): LiveData<PagedList<News>> {
return withContext(Dispatchers.IO) {
val data = mDao.getAllNews()
LivePagedListBuilder(data, Constants.PAGINATION_SIZE).build()
}
}
Now in UI class we can simply call this function using lifescope extension
lifecycleScope.launchWhenStarted {
newsViewModel.getNews()?.observe(this#NewsActivity, Observer { pagedNewsList -> pagedNewsList.let { newsAdapter.submitList(pagedNewsList) } })
}
I have a LoginActivity, where ViewModel is injected using dagger. LoginActivity calls an API through ViewModel upon click of a button. Meanwhile, if the screen rotates, it triggers onDestroy of LoginActivity and there, I dispose that API call. After this, in onCreate(), new instance of ViewModel is injected and because of this, my state is lost & I need to tap again in order to make API call.
Here's my LoginActivity:
class LoginActivity : AppCompatActivity() {
#Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
val loginBinding = DataBindingUtil.setContentView<LoginBindings>(this, R.layout.activity_login)
loginBinding.loginVm = loginViewModel
loginBinding.executePendingBindings()
}
override fun onDestroy() {
super.onDestroy()
loginViewModel.onDestroy()
}
}
Here's my LoginViewModel:
class LoginViewModel(private val validator: Validator,
private val resourceProvider: ResourceProvider,
private val authenticationDataModel: AuthenticationDataModel) : BaseViewModel() {
val userName = ObservableField("")
val password = ObservableField("")
val userNameError = ObservableField("")
val passwordError = ObservableField("")
fun onLoginTapped() {
// Validations
if (!validator.isValidUsername(userName.get())) {
userNameError.set(resourceProvider.getString(R.string.invalid_username_error))
return
}
if (!validator.isValidPassword(password.get())) {
passwordError.set(resourceProvider.getString(R.string.invalid_password_error))
return
}
val loginRequest = LoginRequest(userName.get(), password.get())
addToDisposable(authenticationDataModel.loginUser(loginRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { isApiCallInProgress.set(true) }
.doOnDispose {
LogManager.e("LoginViewModel", "I am disposed, save me!!")
isApiCallInProgress.set(false) }
.subscribe({ user ->
isApiCallInProgress.set(false)
LogManager.e("LoginViewModel", user.name)
}, { error ->
isApiCallInProgress.set(false)
error.printStackTrace()
}))
}
}
My BaseViewModel:
open class BaseViewModel {
private val disposables = CompositeDisposable()
val isApiCallInProgress = ObservableBoolean(false)
fun addToDisposable(disposable: Disposable) {
disposables.add(disposable)
}
fun onDestroy() {
disposables.clear()
}
}
Here's the module which provides the ViewModel:
#Module
class AuthenticationModule {
#Provides
fun provideLoginViewModel(validator: Validator, resourceProvider: ResourceProvider,
authenticationDataModel: AuthenticationDataModel): LoginViewModel {
return LoginViewModel(validator, resourceProvider, authenticationDataModel)
}
#Provides
fun provideAuthenticationRepo(): IAuthenticationRepo {
return AuthRepoApiImpl()
}
}
How can I retain my ViewModel through orientation changes. (Note: I am NOT using Architecture Components' ViewModel). Should I make my ViewModel Singleton? Or is there any other way of doing it?