I dont understand where in mvvm model object should be store.
For e.g i have app
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
viewModel.userScore.observe(this, Observer { it->
score_view.text = it.toString()
})
score_bt.setOnClickListener {
viewModel.scorePoint()
}
}
}
class MyViewModel: ViewModel() {
val _userScore = MutableLiveData<Int>()
val userScore: LiveData<Int>
get() = _userScore
init {
_userScore.value = 1
}
fun scorePoint(){
_userScore.value = (_userScore.value)?.plus(1)
}
}
class Game {
val score = 0
}
when user click button the score increase. I want to store the score in object class Game. Where should the object be stored and how to connect the object with viewmodel, because I think that viewmodel shouldn't contain the object. To be clear I don't expect to stored that object when user turn off app.
Object Game
class Game {
var score = 0
}
The ViewModel
class MyViewModel: ViewModel() {
val game = Game() //init the object
val _userScore = MutableLiveData<Game>()
val userScore: LiveData<Game>
get() = _userScore
init {
_userScore.value = game.apply {
score = 1
}
}
fun scorePoint(){
_userScore.value = game.apply {
score++
}
}
}
Related
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()
}
}
So I created MVVM app in kotlin to fetch movies from TMDB api, using injections and coroutines.
My problem is that I cannot copy the list of returned movies into a new list I created or reassign any variables inside the livedata observer from the MainActivity the values of variables stays the same as they were after exit the scope.
MainActivity class:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val viewModel:MoviesViewModel by lazy {
ViewModelProvider(this)[MoviesViewModel::class.java]
}
private lateinit var list: MutableList<Movies>
private var number:Int=1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
getData()
list
number
}
private fun getData(){
list= mutableListOf()
viewModel.getData(page = "1").observe(this#MainActivity,Observer{ item ->
item?.let { res ->
when (res.status) {
Status.SUCCESS -> {
var tmpList = item.data?.body()?.results
list= tmpList!!.toMutableList()
number+=1
}
Status.ERROR -> {
res.message?.let { Log.e("Error", it) }
}}}
})}}
ViewModel class:
class MoviesViewModel #ViewModelInject constructor(var repository: MoviesRepository): ViewModel() {
fun getData(page:String)= liveData(Dispatchers.IO){
emit(Resource.loading(data = null))
try {
emit(Resource.success(data=repository.getMovies(api_key = Constants.API_KEY,
start_year=Constants.START_YEAR, end_year = Constants.END_YEAR,page = page)))
}catch (e:Exception){
emit(e.message?.let { Resource.error(message = it, data = null) })
}
}
}
As you can see I tried to change the value of number and load the list into my new list but outside the scope the values returned to be what they were before.
Very thankful for anyone who can assist.
Update:
So I tried to initialized all the items inside the success case and it worked I guess there is no other way to change the values outside the scope.
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.
)
How to pass value from Activity to View Model? I try to find anything on web but I failed. What I want is this: I have two recyclerviews in one activity. If user click on item A in recyclerview 1 I want to send ID of this item to View Model and return something by this ID. There is an error with dokladId parameter in testToShow variable.
What is the easy way to handle it?
This is my ViewModel:
#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
}
This is the activity
#AndroidEntryPoint
class DokladActivity : AppCompatActivity(), SkladAdapter.OnItemClickListener, DokladAdapter.OnItemClickListener {
private val skladViewModel: SkladViewModel by viewModels()
//private val dokladViewModel: DokladViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDokladBinding.inflate(layoutInflater)
//setContentView(R.layout.activity_doklad)
setContentView(binding.root)
binding.btVybratDoklad.setOnClickListener{
openActivity(binding.root)
}
val skladAdapter = SkladAdapter (this)
val dokladAdapter = DokladAdapter(this)
binding.apply {
recyclerViewSklady.apply {
adapter = skladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.skladyPolozky.observe(this#DokladActivity) {
skladAdapter.submitList(it)
Log.d("Doklad", skladAdapter.currentList.toString())
}
recyclerViewDoklady.apply {
adapter = dokladAdapter
layoutManager = LinearLayoutManager(this#DokladActivity)
}
skladViewModel.dokladyPolozky.observe(this#DokladActivity){
dokladAdapter.submitList(it)
Log.d("Doklad", dokladAdapter.currentList.toString())
}
}
}
fun openActivity(view: View){
val intent = Intent(this,PolozkaActivity::class.java )
startActivity(intent)
}
override fun onItemClick(polozkaSklad: SkladTuple) {
val action = polozkaSklad.reg
}
override fun onItemClick(polozkaHlavicka: DokladTuple) {
val intent = Intent(this, PolozkaActivity::class.java)
intent.putExtra("doklad", polozkaHlavicka.doklad)
//intent.putExtra("polozkaHlavicka", polozkaHlavicka as Serializable)
startActivity(intent)
}
}
Repository with some function:
fun getSelectedDokladyBySklad(sklad: Int) : Flow<List<SkladDokladTuple>>{
return sybaseDao.getAllDokladFromPolozkaBySklad(sklad)
}
and DAO:
#Query("SELECT distinct doklad FROM cis06zebrap where sklad=:skladId")
fun getAllDokladFromPolozkaBySklad(skladId:Int?=null): Flow<List<SkladDokladTuple>>
#HiltViewModel
class SkladViewModel #Inject constructor(
repository: SybaseRepository
): ViewModel(){
val skladyPolozky = repository.getAllSkladFromPolozka().asLiveData()
val dokladyPolozky = repository.getAllHlavickyToDoklad().asLiveData()
val testToShow = repository.getSelectedDokladyBySklad(dokladId).asLiveData()
fun someNameYouFindUseful(id: String) {
// do something with the id
...
// notify the UI
someLiveDataYouShoudlBeObservingFromTheUI.value = SomeSealedClassWrappingTheStates.SomeStateYouFindDescriptive
}
}
Then in your Activity/Fragment you'd do:
viewModel.someNameYouFindUseful("the Id")
Since you will likely have a viewModel reference there.
To complete the missing pieces, please take a look at the Google official documentation about ViewModels including how to expose a "state" and react to it.
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