I am uning MVVM pattern when i call api in activity from ViewModel its always throwing me with Error
Smart cast to 'MainActivityViewModel' is impossible, because 'binding.mainModel' is a complex expression
Following is the my ViewMode:
class MainActivityViewModel(private val api: SearchAPI) : BaseViewModel() {
private var query: String = ""
get() = if (field.isEmpty()) "MVVM" else field
private val _refreshing: NotNullMutableLiveData<Boolean> = NotNullMutableLiveData(false)
val refreshing: NotNullMutableLiveData<Boolean>
get() = _refreshing
lateinit var _items: NotNullMutableLiveData<RetrofitWrapper>
val items: NotNullMutableLiveData<RetrofitWrapper>
get() = _items
fun getMainPageData() {
val params = mutableMapOf<String, String>().apply {
this["version"] = "v1"
this["locale"] = "en"
this["platform"] = "android"
}
addToDisposable(api.getHomePageDetail(params).with()
.doOnSubscribe { _refreshing.value = true }
.doOnSuccess { _refreshing.value = false }
.doOnError { _refreshing.value = false }
.subscribe({
_items.value = it
}, {
// handle errors
})
)
}
}
And following is my MainActivity:
class MainActivity : BindingActivity<ActivityMain2Binding>() {
override fun getLayoutResId(): Int {
return R.layout.activity_main2
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.mainModel = getViewModel()
binding.setLifecycleOwner(this)
binding.mainModel.getMainPageData()
}
}
Your Help on this matter highly appreciated, do let me know why this is happening to me.
binding.mainModel is a mutable, nullable variable.
Generated binding setter method code will look something like this :
public void setViewModel(#Nullable MainActivityViewModel viewModel) { ... }
When you call binding.mainModel.getMainPageData() it cannot infer that the variable is not null.
You can either do :
binding.mainModel!!.getMainPageData()
or more safely :
binding.mainModel?.getMainPageData()
Related
I am learning Android development, and as I saw in many topics, people were talking about that LiveData is not recommended to use anymore. I mean it's not up-to-date, and we should use Flows instead.
I am trying to get data from ROOM database with Flows and then convert them to StateFlow because as I know they are observables, and I also want to add UI states to them. Like when I get data successfully, state would change to Success or if it fails, it changes to Error.
I have a simple app for practicing. It stores subscribers with name and email, and show them in a recyclerview.
I've checked a lot of sites, how to use stateIn method, how to use StateFlows and Flows but didn't succeed. What's the most optimal way to do this?
And also what's the proper way of updating recyclerview adapter? Is it okay to change it all the time in MainActivity to a new adapter?
Here is the project (SubscriberViewModel.kt - line 30):
Project link
If I am doing other stuff wrong, please tell me, I want to learn. I appreciate any kind of help.
DAO:
import androidx.room.*
import kotlinx.coroutines.flow.Flow
#Dao
interface SubscriberDAO {
#Insert
suspend fun insertSubscriber(subscriber : Subscriber) : Long
#Update
suspend fun updateSubscriber(subscriber: Subscriber) : Int
#Delete
suspend fun deleteSubscriber(subscriber: Subscriber) : Int
#Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll() : Int
#Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers() : Flow<List<Subscriber>>
#Query("SELECT * FROM subscriber_data_table WHERE :id=subscriber_id")
fun getSubscriberById(id : Int) : Flow<Subscriber>
}
ViewModel:
class SubscriberViewModel(private val repository: SubscriberRepository) : ViewModel() {
private var isUpdateOrDelete = false
private lateinit var subscriberToUpdateOrDelete: Subscriber
val inputName = MutableStateFlow("")
val inputEmail = MutableStateFlow("")
private val _isDataAvailable = MutableStateFlow(false)
val isDataAvailable : StateFlow<Boolean>
get() = _isDataAvailable
val saveOrUpdateButtonText = MutableStateFlow("Save")
val deleteOrDeleteAllButtonText = MutableStateFlow("Delete all")
/*
//TODO - How to implement this as StateFlow<SubscriberListUiState> ??
//private val _subscribers : MutableStateFlow<SubscriberListUiState>
//val subscribers : StateFlow<SubscriberListUiState>
get() = _subscribers
*/
private fun clearInput() {
inputName.value = ""
inputEmail.value = ""
isUpdateOrDelete = false
saveOrUpdateButtonText.value = "Save"
deleteOrDeleteAllButtonText.value = "Delete all"
}
fun initUpdateAndDelete(subscriber: Subscriber) {
inputName.value = subscriber.name
inputEmail.value = subscriber.email
isUpdateOrDelete = true
subscriberToUpdateOrDelete = subscriber
saveOrUpdateButtonText.value = "Update"
deleteOrDeleteAllButtonText.value = "Delete"
}
fun saveOrUpdate() {
if (isUpdateOrDelete) {
subscriberToUpdateOrDelete.name = inputName.value
subscriberToUpdateOrDelete.email = inputEmail.value
update(subscriberToUpdateOrDelete)
} else {
val name = inputName.value
val email = inputEmail.value
if (name.isNotBlank() && email.isNotBlank()) {
insert(Subscriber(0, name, email))
}
inputName.value = ""
inputEmail.value = ""
}
}
fun deleteOrDeleteAll() {
if (isUpdateOrDelete) {
delete(subscriberToUpdateOrDelete)
} else {
deleteAll()
}
}
private fun insert(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(subscriber)
_isDataAvailable.value = true
}
private fun update(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.update(subscriber)
clearInput()
}
private fun delete(subscriber: Subscriber) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(subscriber)
clearInput()
}
private fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
//_subscribers.value = SubscriberListUiState.Success(emptyList())
_isDataAvailable.value = false
}
sealed class SubscriberListUiState {
data class Success(val list : List<Subscriber>) : SubscriberListUiState()
data class Error(val msg : String) : SubscriberListUiState()
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: SubscriberViewModel
private lateinit var viewModelFactory: SubscriberViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dao = SubscriberDatabase.getInstance(application).subscriberDAO
viewModelFactory = SubscriberViewModelFactory(SubscriberRepository(dao))
viewModel = ViewModelProvider(this, viewModelFactory)[SubscriberViewModel::class.java]
binding.viewModel = viewModel
binding.lifecycleOwner = this
initRecycleView()
}
private fun initRecycleView() {
binding.recyclerViewSubscribers.layoutManager = LinearLayoutManager(
this#MainActivity,
LinearLayoutManager.VERTICAL, false
)
displaySubscribersList()
}
private fun displaySubscribersList() {
/*
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.subscribers.collect { uiState ->
when (uiState) {
is SubscriberViewModel.SubscriberListUiState.Success -> {
binding.recyclerViewSubscribers.adapter = SubscriberRecyclerViewAdapter(uiState.list) {
subscriber: Subscriber -> listItemClicked(subscriber)
}
}
is SubscriberViewModel.SubscriberListUiState.Error -> {
Toast.makeText(applicationContext,uiState.msg, Toast.LENGTH_LONG).show()
}
}
}
}
}*/
}
private fun listItemClicked(subscriber: Subscriber) {
Toast.makeText(this, "${subscriber.name} is selected", Toast.LENGTH_SHORT).show()
viewModel.initUpdateAndDelete(subscriber)
}
}
You can convert a Flow type into a StateFlow by using stateIn method.
private val coroutineScope = CoroutineScope(Job())
private val flow: Flow<CustomType>
val stateFlow = flow.stateIn(scope = coroutineScope)
In order to transform the CustomType into UIState, you can use the transformLatest method on Flow. It will be something like below:
stateFlow.transformLatest { customType ->
customType.toUiState()
}
Where you can create an extension function to convert CustomType to UiState like this:
fun CustomType.toUiState() = UiState(
x = x,
y = y... and so on.
)
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 }
}
My DataStore keeps returning null even though I've set a default value on the preferences manager using the elvis operator. Also, my edit function to set a preference on a key-value pair isn't being called so I'm not even sure my datastore is properly setup in general. I'm pretty sure the class is properly injected though, because I can see it as a variable while using breakpoints.
Basically val countryCode = viewModel.countrySettings.value on the ViewModel always returns null
PreferencesManager class
const val TAG = "PreferencesManager"
const val DEFAULT_COUNTRY_PREFERENCE = "us"
const val DEFAULT_CATEGORY_PREFERENCE = "general"
private val Context.dataStore by preferencesDataStore(name = PREFERENCES_NAME)
#Singleton
class PreferencesManager #Inject constructor(#ApplicationContext appContext: Context) {
private val preferencesDataStore = appContext.dataStore
//Pairs are separated but I'll create an appropriate data class later.
val countrySettings = preferencesDataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error while trying to read user preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
val country = preference[PreferenceKeys.COUNTRY] ?: DEFAULT_COUNTRY_PREFERENCE
country
}
val categorySettings = preferencesDataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error while trying to read user preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val category = preferences[PreferenceKeys.CATEGORY] ?: DEFAULT_CATEGORY_PREFERENCE
category
}
suspend fun setCountryPreference(country: String) {
preferencesDataStore.edit { preference ->
preference[PreferenceKeys.COUNTRY] = country
}
}
suspend fun setCategoryPreference(category: String) {
preferencesDataStore.edit { preference ->
preference[PreferenceKeys.CATEGORY] = category
}
}
private object PreferenceKeys {
val COUNTRY = stringPreferencesKey("country")
val CATEGORY = stringPreferencesKey("category")
}
}
ViewModel
class MainViewModel #Inject constructor(
private val repository: Repository,
private val preferencesManager: PreferencesManager
): ViewModel() {
val countrySettings = preferencesManager.countrySettings.asLiveData()
val categorySettings = preferencesManager.categorySettings.asLiveData()
/* .... */
fun setCountryPreference(country: String) {
viewModelScope.launch {
preferencesManager.setCountryPreference(country)
}
}
fun setCategoryPreference(category: String) {
viewModelScope.launch {
preferencesManager.setCategoryPreference(category)
}
}
}
Fragment
val viewModel: MainViewModel by activityViewModels()
private var _binding: FragmentSettingsCountryScreenBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentSettingsCountryScreenBinding.bind(view)
//Using breakpoints I've noticed this function isn't even called on the preferences manager to set the value, which is weird
viewModel.setCountryPreference("us")
val countryCode = viewModel.countrySettings.value
binding.radiogroup.check(adaptPreferenceFromDataStore(countryCode!!))
binding.radiogroup.setOnCheckedChangeListener { _, checkedId ->
viewModel.setCountryPreference(adaptPreferenceToDataStore(checkedId))
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Yes, ianhanniballake is correct, it was a newbie mistake - I completely forgot I had to observe the livedata value and just then set the UI parameters. I was trying to set the preferences based on the value of a few switches (and vice-versa). Here's the proper function for setting it up:
fun setupSwitches() {
viewModel.countrySettings.observe(viewLifecycleOwner, { preference ->
binding.radioGroup.check(adaptPreferenceFromDataStore(preference))
})
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
viewModel.setCountryPreference(adaptPreferenceToDataStore(checkedId))
}
}
Then called setupSwitches() on onViewCreated.
I am following the MVVM pattern Activity-->ViewModel ---> Repository . Repository is calling api and updated the LiveData. The value is of LiveData is also updated in ViewModel but its not reflecting on Activity. Please guide me where i am missing, Code is given below
Activity code:
class LoginWithEmailActivity : AppCompatActivity() {
private var loginViewModel: LoginViewModel? = null
private var binding: ActivityLoginWithEmailBinding? = null
private var btnLogin : Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginViewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
binding = DataBindingUtil.setContentView(this#LoginWithEmailActivity, R.layout.activity_login_with_email)
binding!!.setLifecycleOwner(this)
binding!!.setLoginViewModel(loginViewModel)
btnLogin = findViewById(R.id.btn_login)
loginViewModel!!.servicesLiveData!!.observe(this, Observer<LoginDataModel?> { serviceSetterGetter ->
val msg = serviceSetterGetter.success
Toast.makeText(this#LoginWithEmailActivity, ""+msg, Toast.LENGTH_SHORT).show()
Log.v("///LOGIN SUCCESS////",""+msg);
})
btnLogin!!.setOnClickListener {
loginViewModel!!.getUser()
}
}
ViewModel.kt
class LoginViewModel : ViewModel() {
var servicesLiveData: MutableLiveData<LoginDataModel>? = MutableLiveData()
fun getUser() {
servicesLiveData = MainActivityRepository.getServicesApiCall()
}
}
Repository.kt
object MainActivityRepository {
val serviceSetterGetter = MutableLiveData<LoginDataModel>()
fun getServicesApiCall(): MutableLiveData<LoginDataModel> {
val params = JsonObject()
params.addProperty("email", "xyz#gmail.com")
val call: Call<LoginDataModel> = ApiClient.getClient.getPhotos(params)
call.enqueue(object : Callback<LoginDataModel> {
#RequiresApi(Build.VERSION_CODES.N)
override fun onResponse(call: Call<LoginDataModel>?, response: Response<LoginDataModel>?) {
if (response != null) {
val data = response.body()
serviceSetterGetter?.postValue(data);
}
}
override fun onFailure(call: Call<LoginDataModel>?, t: Throwable?) {
}
})
return serviceSetterGetter
}
}
You subscribe to the LiveData in onCreate
loginViewModel!!.servicesLiveData!!.observe(this, Observer<LoginDataModel?> { serviceSetterGetter ->
val msg = serviceSetterGetter.success
Toast.makeText(this#LoginWithEmailActivity, ""+msg, Toast.LENGTH_SHORT).show()
Log.v("///LOGIN SUCCESS////",""+msg);
})
but then getUser creates a new reference
fun getUser() {
servicesLiveData = MainActivityRepository.getServicesApiCall()
}
The one you are subscribed to is not the same as the getUser liveData.
If you want to keep what you have mostly the same you need to use MediatorLiveData
Or just do
getUser().observe(this, Observer<LoginDataModel?> { serviceSetterGetter ->
val msg = serviceSetterGetter.success
Toast.makeText(this#LoginWithEmailActivity, ""+msg, Toast.LENGTH_SHORT).show()
Log.v("///LOGIN SUCCESS////",""+msg);
})
fun getUser(): LiveData<LoginDataModel> {
return MainActivityRepository.getServicesApiCall()
}
I have an activity to perform rest API everytime it opened and i use MVVM pattern for this project. But with this snippet code i failed to get updated everytime i open activity. So i debug all my parameters in every line, they all fine the suspect problem might when apiService.readNewsAsync(param1,param2) execute, my postValue did not update my resulRead parameter. There were no crash here, but i got result which not updated from result (postValue). Can someone explain to me why this happened?
Here what activity looks like
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivityReadBinding>(this,
R.layout.activity_read).apply {
this.viewModel = readViewModel
this.lifecycleOwner = this#ReadActivity
}
readViewModel.observerRead.observe(this, Observer {
val sukses = it.isSuccess
when{
sukses -> {
val data = it.data as Read
val article = data.article
//Log.d("-->", "${article.toString()}")
}
else -> {
toast("ada error ${it.msg}")
Timber.d("ERROR : ${it.msg}")
}
}
})
readViewModel.getReadNews()
}
Viewmodel
var observerRead = MutableLiveData<AppResponse>()
init {
observerRead = readRepository.observerReadNews()
}
fun getReadNews() {
// kanal and guid i fetch from intent and these value are valid
loadingVisibility = View.VISIBLE
val ok = readRepository.getReadNews(kanal!!, guid!!)
if(ok){
loadingVisibility = View.GONE
}
}
REPOSITORY
class ReadRepositoryImpl private constructor(private val newsdataDao: NewsdataDao) : ReadRepository{
override fun observerReadNews(): MutableLiveData<AppResponse> {
return newsdataDao.resultRead
}
override fun getReadNews(channel: String, guid: Int) = newsdataDao.readNews(channel, guid)
companion object{
#Volatile private var instance: ReadRepositoryImpl? = null
fun getInstance(newsdataDao: NewsdataDao) = instance ?: synchronized(this){
instance ?: ReadRepositoryImpl(newsdataDao).also {
instance = it
}
}
}
}
MODEL / DATA SOURCE
class NewsdataDao {
private val apiService = ApiClient.getClient().create(ApiService::class.java)
var resultRead = MutableLiveData<AppResponse>()
fun readNews(channel: String, guid: Int): Boolean{
GlobalScope.launch {
val response = apiService.readNewsAsync(Constants.API_TOKEN, channel, guid.toString()).await()
when{
response.isSuccessful -> {
val res = response.body()
val appRes = AppResponse(true, "ok", res!!)
resultRead.postValue(appRes)
}
else -> {
val appRes = AppResponse(false, "Error: ${response.message()}", null)
resultRead.postValue(appRes)
}
}
}
return true
}
}
Perhaps this activity is not getting stopped.
Check this out:
When you call readViewModel.getReadNews() in onCreate() your activity is created once, only if onStop is called will it be created again.