Basically, I am fetching the products list from this API using Retrofit into a MediatorLiveData inside ProductsRepository class. But, the problem is, when I try to observe the LiveData I get null.
Here is my code snippet:
ProductsRepository:
#MainScope
class ProductsRepository #Inject constructor(private val productsApi: ProductsApi) {
private val products: MediatorLiveData<ProductsResource<List<ProductsModel>>> =
MediatorLiveData()
fun getProducts(): LiveData<ProductsResource<List<ProductsModel>>> {
products.value = ProductsResource.loading(null)
val source: LiveData<ProductsResource<List<ProductsModel>>> =
LiveDataReactiveStreams.fromPublisher {
productsApi.getProducts()
.onErrorReturn {
val p = ProductsModel()
p.setId(-1)
val products = ArrayList<ProductsModel>()
products.add(p)
products
}.map {
if (it[0].getId() == -1) {
ProductsResource.error("Something went wrong", null)
}
ProductsResource.success(it)
}.observeOn(Schedulers.io())
}
products.addSource(source){
products.value = it
products.removeSource(source)
}
return products
}
}
MainViewModel
class MainViewModel #Inject constructor(private val repository: ProductsRepository): ViewModel() {
fun getProducts(): LiveData<ProductsResource<List<ProductsModel>>>{
return repository.getProducts()
}
}
MainActivity:
class MainActivity : DaggerAppCompatActivity() {
lateinit var binding: ActivityMainBinding
#Inject
lateinit var viewModelProviderFactory: ViewModelProviderFactory
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initViewModel()
subscribeToObservers()
}
private fun subscribeToObservers(){
mainViewModel.getProducts()
.observe(this){
Log.d("", "subscribeToObservers: "+ it.data?.size)
}
}
private fun initViewModel() {
mainViewModel = ViewModelProvider(this, viewModelProviderFactory).get(MainViewModel::class.java)
}
}
If I call hasActiveObservers(), it returns false although I am observing it from the MainActivity.
Now, let's say if I replace the MediatorLiveData with MutableLiveData and refactor my ProductsRepository like below, I get my expected output.
fun getProducts(): LiveData<ProductsResource<List<ProductsModel>>> {
val products: MutableLiveData<ProductsResource<List<ProductsModel>>> = MutableLiveData()
products.value = ProductsResource.loading(null)
productsApi.getProducts()
.onErrorReturn {
//Log.d("MyError", it.message.toString())
val p = ProductsModel()
p.setId(-1)
val product = ArrayList<ProductsModel>()
product.add(p)
product
}.map { product ->
if (product.isNotEmpty()) {
if (product[0].getId() == -1) {
// Log.d("Map", "Error: ${product}")
ProductsResource.error(
"Something went Wrong",
null
)
}
}
ProductsResource.success(product)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
products.value = it
}
return products
}
I don't know If I am successful in explaining my problem. Please, let me know If I need to provide more details or code snippets.
Thanks in advance
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 am new to dagger hilt (DI), And I Implement dagger hilt (DI) in my project. Now I am trying to inject the ViewModel. It works fine. But in the API result, I am using mutableLivedata for updating value from the view model to view with the observer. The observer listens and fetches the value works fine. But I read the observed data value; it cleared the value. I don't know why it happened. Can anyone help me to find this?
Login Fragment
#AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(FragmentLoginBinding::inflate) {
private val viewModel by viewModels<LoginViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewActionListeners()
setUpViewModelObserver()
}
private fun setUpViewModelObserver() {
viewModel.users.observe(viewLifecycleOwner) { response ->
AppLog.e("validateResponse", response.toString())
when (response.status) {
Status.SUCCESS -> {
binding.frmLayoutProgress.visible(false)
response.data?.let { users -> AppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }
}
Status.LOADING -> {
binding.frmLayoutProgress.visible(true)
}
Status.ERROR -> {
//Handle Error
binding.frmLayoutProgress.visible(false)
AppLog.e("Error:", response.message.toString())
}
}
}
}
}
If you comment the lineAppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }it return the value if uncomment response returns set to be null.
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val repository: LoginRepository,
private val networkHelper: NetworkHelper
) : ViewModel() {
val users: MutableLiveData<Resource<ResponseValidateUser>> = MutableLiveData()
fun loadValidateUser(email: String) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
if (networkHelper.isNetworkConnected()) {
repository.getValidateUser(email).let {
if (it.isSuccessful) {
users.postValue(Resource.success(it.body()))
} else users.postValue(Resource.error(it.errorBody().toString(), null))
}
} else users.postValue(Resource.error("No internet connection", null))
}
}
}
LoginRepository
class LoginRepository #Inject constructor(private val apiHelper: AuthApiHelper) {
suspend fun getValidateUser(email: String) = apiHelper.getValidateUser(email)
}
AuthApiHelper
interface AuthApiHelper {
suspend fun getValidateUser(email: String): Response<ResponseValidateUser>
}
ResponseValidateUser
data class ResponseValidateUser(
#SerializedName("clientResponse") val clientResponse: ClientResponse,
#SerializedName("status") val status: String,
#SerializedName("errorMessage") val errorMessage: String
)
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()
}
MainActivity
class MainActivity : AppCompatActivity() {
#Inject
lateinit var mainViewModelFactory: mainViewModelFactory
private lateinit var mainActivityBinding: ActivityMainBinding
private lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainActivityBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_main
)
mainActivityBinding.rvmainRepos.adapter = mainAdapter
AndroidInjection.inject(this)
mainViewModel =
ViewModelProviders.of(
this#MainActivity,
mainViewModelFactory
)[mainViewModel::class.java]
mainActivityBinding.viewmodel = mainViewModel
mainActivityBinding.lifecycleOwner = this
mainViewModel.mainRepoReponse.observe(this, Observer<Response> {
repoList.clear()
it.success?.let { response ->
if (!response.isEmpty()) {
// mainViewModel.saveDataToDb(response)
// mainViewModel.createWorkerForClearingDb()
}
}
})
}
}
MainViewModelFactory
class MainViewModelFactory #Inject constructor(
val mainRepository: mainRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>) =
with(modelClass) {
when {
isAssignableFrom(mainViewModel::class.java) -> mainViewModel(
mainRepository = mainRepository
)
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
MainViewModel
class MainViewModel(
val mainRepository: mainRepository
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
val mainRepoReponse = MutableLiveData<Response>()
val loadingProgress: MutableLiveData<Boolean> = MutableLiveData()
val _loadingProgress: LiveData<Boolean> = loadingProgress
val loadingFailed: MutableLiveData<Boolean> = MutableLiveData()
val _loadingFailed: LiveData<Boolean> = loadingFailed
var isConnected: Boolean = false
fun fetchmainRepos() {
if (isConnected) {
loadingProgress.value = true
compositeDisposable.add(
mainRepository.getmainRepos().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response ->
run {
saveDataToDb(response)
)
}
},
{ error ->
processResponse(Response(AppConstants.Status.SUCCESS, null, error))
}
)
)
} else {
fetchFromLocal()
}
}
private fun saveDataToDb(response: List<mainRepo>) {
mainRepository.insertmainUsers(response)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(object : DisposableCompletableObserver() {
override fun onComplete() {
Log.d("Status", "Save Success")
}
override fun onError(e: Throwable) {
Log.d("Status", "error ${e.localizedMessage}")
}
})
}
}
MainRepository
interface MainRepository {
fun getmainRepos(): Single<List<mainRepo>>
fun getAllLocalRecords(): Single<List<mainRepo>>
fun insertmainUsers(repoList: List<mainRepo>): Completable
}
MainRepositoryImpl
class mainRepositoryImpl #Inject constructor(
val apiService: GitHubApi,
val mainDao: AppDao
) : MainRepository {
override fun getAllLocalRecords(): Single<List<mainRepo>> = mainDao.getAllRepos()
override fun insertmainUsers(repoList: List<mainRepo>) :Completable{
return mainDao.insertAllRepos(repoList)
}
override fun getmainRepos(): Single<List<mainRepo>> {
return apiService.getmainGits()
}
}
I'm quite confused with the implementation of MVVM with LiveData and Rxjava, in my MainViewModel I am calling the interface method and implementing it inside ViewModel, also on the response I'm saving the response to db. However, that is a private method, which won't be testable in unit testing in a proper way (because it's private). What is the best practice to call other methods on the completion of one method or i have to implement all the methods inside the implementation class which uses the interface.
Your ViewModel should not care how you are getting the data if you are trying to follow the clean architecture pattern. The logic for fetching the data from local or remote sources should be in the repository in the worst case where you can also save the response. In that case, since you have a contact for the methods, you can easily test them. Ideally, you could break it down even more - adding Usecases/Interactors.
i'm newbie in android architecture component , this is my code for working with viewmodel, repository , retrofit 2 using kotlin
my activity code:
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainViewModel=ViewModelProviders.of(this).get(MainViewModel::class.java)
mainViewModel.getUsers().observe(this, Observer<MutableList<User>> {
it?.forEach {
Log.v("this", "name " + it.name + " " + it.famil)
}
})
}
my viewmodel :
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val mainRepository:MainReposioty=MainReposioty()
private lateinit var users: MutableLiveData<MutableList<User>>
fun getUsers(): MutableLiveData<MutableList<User>> {
if (!::users.isInitialized) {
users =mainRepository.getUsers()
}
return users
}
}
my repository:
class MainReposioty {
private lateinit var getUserList: getUsersApi
private lateinit var users: MutableLiveData<MutableList<User>>
fun getUsers():MutableLiveData<MutableList<User>> {
if(!::users.isInitialized){
users= MutableLiveData()
}
getUserList = ApiConnection.client.create(getUsersApi::class.java)
getUserList.getUsers()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
users += result.users
}, { error ->
Log.v("this", error.localizedMessage.toString())
}
)
return users;
}
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(values: List<T>) {
val value = this.value ?: arrayListOf()
value.addAll(values)
this.value = value
}
}
other codes are fine for fetching the data from webservice . I was wondering if my code is ok and in the right way