How can I capture and handle errors gracefully, presenting error message in an alert and avoid app crashes.
Consider following example, where each function is scoped by viewModel and runs in a coroutine (asynchronous)
Consider a scenario where the methods (deleteUser, insertUser) would fail
UserViewModel
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val userDao: UserDao
private val users: LiveData<List<User>>
init {
val database = AppDatabase.getInstance(application)
userDao = database.userDao()
users = userDao.getAllUsers()
}
fun insertUser(user: User) = viewModelScope.launch {
userDao.insertUser(user)
}
fun deleteUser(id: Long) = viewModelScope.launch {
userDao.deleteUser(id)
}
fun getUsers(): LiveData<List<User>> {
return users
}
}
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var userViewModel: UserViewModel
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
recyclerView = findViewById(R.id.recycler_view)
adapter = UserAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
userViewModel.getUsers().observe(this, Observer {
adapter.setData(it)
})
// What if this fails? DBExceptions or others
userViewModel.insertUser(User(0, "John Doe", "johndoe#example.com"))
// What if this fails? DBExceptions or others
userViewModel.deleteUser(1)
}
fun alert(exception: Throwable): AlertDialog {
return AlertDialog.Builder(this)
.setTitle(exception.javaClass.simpleName)
.setMessage(exception.localizedMessage)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(R.string.okay, null)
.create().show()
}
}
Related
Error: can't create an instance of the view model class
here is how I'm trying to create it
class MainActivity : AppCompatActivity() {
lateinit var noteViewModel: NoteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noteViewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(this.application)
).get(
NoteViewModel::class.java
)
noteViewModel.getAllNotes().observe(this, object : Observer<List<Note>> {
override fun onChanged(t: List<Note>?) {
Toast.makeText(this#MainActivity, t.toString(), Toast.LENGTH_LONG).show()
}
})
}
}
And here is my view model class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository = NoteRepository(application)
private val allNotes: LiveData<List<Note>> = repository.getAllNotes()
fun insert(note: Note) {
repository.insert(note)
}
fun delete(note: Note) {
repository.delete(note)
}
fun update(note: Note) {
repository.update(note)
}
fun deleteAll() {
repository.deleteAllNotes()
}
fun getAllNotes(): LiveData<List<Note>> = allNotes
}
everything looks fine, I don't know what's causing the error
You can use the kotlin property delegate "viewModels()" to instantiate your viewmodel
class MainActivity : AppCompatActivity() {
var noteViewModel: NoteViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
noteViewModel.getAllNotes().observe(this, object : Observer<List<Note>> {
override fun onChanged(t: List<Note>?) {
Toast.makeText(this#MainActivity, t.toString(), Toast.LENGTH_LONG).show()
}
})
}
}
Try with the below dependency then you will be able to get kotlin property delegate "viewModels()" in your activity.
dependencies {
val lifecycle_version = "2.3.1"
val arch_version = "2.1.0"
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
// Saved state module for ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")
implementation "androidx.activity:activity-ktx:1.3.0-beta01"
}
//write below code in your activity for instantiate viewModel
private val noteViewModel: NoteViewModel by viewModels()
//For more detail go through below link
https://developer.android.com/jetpack/androidx/releases/lifecycle#declaring_dependencies
I had the same problem(in java). This solved it:
// old code
mWordViewModel = new ViewModelProvider(this).get(WordViewModel.class);
// new code
mWordViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(WordViewModel.class);
if anyone is having this problem one solution to fix this is just make ViewModel constructor public (Class that extends AndroidViewModel)
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
when I run this class, I'm always got test failed in method verify_on_success_is_called() with error,
Actually, there were zero interactions with this mock.
but if I run method only, test will passed.
#Mock
lateinit var mDummy: Dummy
private lateinit var mainViewModel: MainViewModel
#Mock
lateinit var main: MainViewModel.IMain
#Before
#Throws(Exception::class)
fun setup() {
MockitoAnnotations.initMocks(this)
MainViewModel.mIMain = main
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
mainViewModel = MainViewModel(mDummy)
}
#Test
fun verify_on_success_is_called() {
val mockList: ArrayList<Employee> = ArrayList()
mockList.add(Employee(1, "a", 20000.0, 22))
val list: List<Employee> = mockList
`when`(mDummy.getEmployees()).thenReturn(Observable.just(Response.success(list)))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onSuccess()
}
#Test
fun verify_on_onError_is_called() {
MainViewModel.mIMain = main
`when`(mDummy.getEmployees()).thenReturn(Observable.error(Throwable()))
mainViewModel.getEmployees()
Mockito.verify(main, times(1)).onError()
}
this the viewModel class I want to test
class MainViewModel(private val mDummy: Dummy) : ViewModel() {
companion object {
lateinit var mIMain: IMain
}
interface IMain {
fun onSuccess()
fun onError()
}
fun getEmployees() {
mDummy.getEmployees()
.observeOn(SchedulerProvides.main())
.subscribeOn(SchedulerProvides.io())
.subscribe({ response ->
if (response.isSuccessful) {
mIMain.onSuccess()
} else {
mIMain.onError()
}
}, {
mIMain.onError()
})
}
and this my mainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainViewModel.mIMain = mIMainViewModelIniliazed()
}
private fun mIMainViewModelIniliazed() = object :MainViewModel.IMain{
override fun onSuccess() {
}
override fun onError() {
}
}
Please correct me if am wrong but i think your problem is because you're setting
MainViewModel.mIMain = main
before creating your viewmodel instance, shouldn't be as below?
mainViewModel = MainViewModel(mDummy)
mainViewModel.mIMain = main
Trying to fetch the data from the server using Retrofit and caching it offline using Room Database.
Problem I have fetched the data from the server and inserted it into the room but could not populate it. After evaluating the expression, the LiveData list is null.
I am certain that the insertion is successful, as I went through some answers on StackOverflow which mentioned to return the list of IDs to find out if there has been an insert.
Below is my code:
User.kt
#Entity(tableName = "user_table")
data class User(
#PrimaryKey(autoGenerate = true)
val id:Long,
#Json(name = "name")
val uname:String,
val username:String,
val email:String,
#Embedded
val address: Address,
val phone:String,
val website:String,
#Embedded
val company: Company)
data class Address(
val street:String,
val suite:String,
val city:String,
val zipcode:String,
#Embedded
val geo: Geo)
data class Geo(
val lat:String,
val lng:String)
data class Company(
#Json(name = "name")
val companyName:String,
val catchPhrase:String,
val bs:String)
UserDao.kt
#Dao
interface UserDao{
#Query("select * from user_table")
fun getUserList():LiveData<List<User>>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(userList:List<User>):List<Long>
}
UserRepository.kt
class UserRepository(private val userDao: UserDao) {
val userList: LiveData<List<User>> = userDao.getUserList()
suspend fun refreshUserList() {
withContext(Dispatchers.IO) {
val userList = UserApi.retrofitService.getUserData().await()
val list = userDao.insertAll(userList)
Log.e("list","${list.size}")
}
}
}
MainActivityViewModel.kt
class MainActivityViewModel(database:UserDao, application: Application) : AndroidViewModel(application){
private val userRepository = UserRepository(database)
val usersList = userRepository.userList
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main)
init {
getUsersList()
}
private fun getUsersList() {
coroutineScope.launch {
try{
userRepository.refreshUserList()
}catch (networkError: IOException){
Toast.makeText(getApplication(),"Failure",Toast.LENGTH_SHORT).show()
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
//Main View Model Destroys
}
}
UserAdapter.kt
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>(){
var userDetailsList:List<User> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(UserCellLayoutBinding.inflate(
LayoutInflater.from(parent.context)
))
}
override fun getItemCount(): Int {
return userDetailsList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val user = userDetailsList[position]
holder.bind(user)
}
class ViewHolder(private var binding: UserCellLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(user: User){
binding.user = user
binding.executePendingBindings()
}
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var viewModelFactory: ViewModelFactory
private lateinit var viewModel: MainActivityViewModel
private var adapter:UserAdapter ?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dataSource = UserRoomDatabase.getInstance(application).userDao
viewModelFactory = ViewModelFactory(dataSource,application)
viewModel = ViewModelProviders.of(this,viewModelFactory).get(MainActivityViewModel::class.java)
binding.lifecycleOwner = this
binding.mainActivityViewModel = viewModel
binding.executePendingBindings()
binding.userListRecyclerView.adapter = adapter
viewModel.usersList.observe(this, Observer<List<User>> {users ->
users?.apply {
adapter?.userDetailsList = users
}
})
}
}
Any help would be greatly appreciated.
Thanks in advance.
I forgot to instantiate the adapter in MainActivity.kt
adapter = UserAdapter()
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val dataSource = UserRoomDatabase.getInstance(application).userDao
viewModelFactory = ViewModelFactory(dataSource,application)
viewModel = ViewModelProviders.of(this,viewModelFactory).get(MainActivityViewModel::class.java)
binding.lifecycleOwner = this
binding.mainActivityViewModel = viewModel
binding.executePendingBindings()
adapter = UserAdapter()
binding.userListRecyclerView.adapter = adapter
viewModel.usersList.observe(this, Observer<List<User>> {users ->
users?.apply {
adapter?.userDetailsList = users
}
})
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.