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
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 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 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.
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
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.