I have one pretty big complicated ViewModel and I want to split it or build it with few smaller ViewModels.
Below I want to show how I make my ViewModels in general (please do not laugh, this is my first Android ViewModel). I'm not using DataBinding, just ViewBinding.
class AssignUserTagToInventoryItemViewModel() : ViewModel() {
private val UserTag = "MyApp" + this.javaClass.simpleName
init {
Log.d(UserTag, "Class init called")
loadInventoryItems()
loadRandomUserTags() // todo: replace with real implementation
}
private var allItems = ArrayList<InventoryItemDto?>()
//<editor-fold desc="FilterByName">
private val _filterByName = MutableLiveData<String>("")
val filterByName: LiveData<String> get() = _filterByName
fun setFilterByName(t : String) { _filterByName.value = t; applyFilters();}
//</editor-fold>
//<editor-fold desc="FilterByAssignedToMe">
private val _filterByAssignedToMe = MutableLiveData<Boolean>(false)
val filterByAssignedToMe: LiveData<Boolean> get() = _filterByAssignedToMe
fun setFilterByAssignedToMe(t : Boolean) { _filterByAssignedToMe.value = t; applyFilters(); }
//</editor-fold>
//<editor-fold desc="SelectedInventoryItem">
private val _selectedInventoryItem = MutableLiveData<InventoryItemDto?>(null)
fun getSelectedInventoryItem() : LiveData<InventoryItemDto?> = _selectedInventoryItem
fun setSelectedInventoryItem(itemDto: InventoryItemDto?) {
_selectedInventoryItem.value = itemDto
selectedItemOrUserTagChanged()
}
//</editor-fold>
// <editor-fold desc="FilteredItems">
val _displayedItems = MutableLiveData<ArrayList<InventoryItemDto?>>(ArrayList())
val displayedItems: LiveData<ArrayList<InventoryItemDto?>> get() = _displayedItems
// </editor-fold>
// <editor-fold desc="ItemsListError">
val _itemsListError = MutableLiveData<String>("")
val itemsListError :LiveData<String> get() = _itemsListError
fun setItemsListError(s : String) { _itemsListError.value = s }
// </editor-fold>
//<editor-fold desc="UserTag list">
val _UserTags = MutableLiveData<ArrayList<UserTag>>(ArrayList())
val UserTags : LiveData<ArrayList<UserTag>> get() = _UserTags
fun setUserTags(a : ArrayList<UserTag>) { _UserTags.value = a }
//</editor-fold>
//<editor-fold desc="SelectedUserTagItem">
private val _selectedUserTag = MutableLiveData<UserTag?>(null)
fun getSelectedUserTag() : LiveData<UserTag?> = _selectedUserTag
fun setSelectedUserTag(UserTag : UserTag?) {
_selectedUserTag.value = UserTag
selectedItemOrUserTagChanged()
}
//</editor-fold>
//<editor-fold desc="CanSubmit">
private val _canSubmit = MutableLiveData<Boolean>(false)
val canSubmit: LiveData<Boolean> get() = _canSubmit
//</editor-fold>
private fun selectedItemOrUserTagChanged() {
_canSubmit.value = true
}
private fun loadInventoryItems(){
Log.d(UserTag, "Loading inventory items...")
viewModelScope.launch {
try {
val apiResponse = ApiResponse(ApiAdapter.apiClient.findAllInventoryItems())
if (apiResponse.code == 200 && apiResponse.body != null) {
allItems = apiResponse.body
applyFilters()
Log.d(UserTag, "Loading inventory items done.")
}
else {
setItemsListError(apiResponse.code.toString())
Log.d(UserTag, "Loading inventory items error.")
}
} catch (t : Throwable) {
setItemsListError(t.message.toString())
}
}
}
private fun applyFilters(){
Log.d(UserTag, "ViewModel apply filters called. Current name filter: ${filterByName.value}")
val tempResults = ArrayList<InventoryItemDto?>()
val nameFilterLowercase = filterByName.value.toString().lowercase()
if (!filterByName.value.isNullOrEmpty()) {
for (item in allItems) {
val itemNameLowercase = item?.name?.lowercase()?:""
if (itemNameLowercase.contains(nameFilterLowercase))
tempResults.add(item)
}
_displayedItems.value = tempResults
} else {
_displayedItems.value = allItems
}
}
private fun loadRandomUserTags(){
val temp = ArrayList<UserTag>()
for (i in 1..50){
val epc = getRandomHexString(24).uppercase()
val UserTag = UserTag(epc, 0, "0")
temp.add(UserTag)
}
viewModelScope.launch {
delay(100)
_UserTags.value = temp
}
}
private fun getRandomHexString(numchars: Int): String {
val r = Random()
val sb = StringBuffer()
while (sb.length < numchars) {
sb.append(Integer.toHexString(r.nextInt()))
}
return sb.toString().substring(0, numchars)
}
}
Simply create multiple view models according to the task they are performing.
There are several problems here :
Your ViewModel name is too long
You can create an object of the getRandomHexString and this way you can use it inside any other classes or ViewModels you may need in future. It also saves space inside ViewModel.
Learn about the clean architecture and follow its practices. Here, you can create a separate view model or helper class for filtering your results. If you create another view model, you can simply retrieve results from your current view model to the activity and call filter view model inside your activity. This way you can separate code blocks according to the role they play or the function they perform.
Related
I have two stateflow in my viewmodel
private val _peopleList = MutableStateFlow(emptyList<People>())
val peopleList: StateFlow<List<People>> = _peopleList
val _peopleListLoader = MutableStateFlow(false)
val peopleListLoader: StateFlow<Boolean> = _peopleListLoader
peopleList is used to display list and peopleListLoader is used to display progress indicator in UI. All of these are working fine in app as expected. But in my Unit test when I check the peopleListLoader using peopleListLoader.toList(values) functionality its dosent have the values i assigned to it during the peoplelist loading.
Following is my implementation
PeopleListViewModel
#HiltViewModel
class PeopleListViewModel #Inject constructor(
val repository: PeopleRepository,
#MainDispatcher private val dispatcher: CoroutineDispatcher
) : ViewModel() {
private val _peopleList = MutableStateFlow(emptyList<People>())
val peopleList: StateFlow<List<People>> = _peopleList
val _peopleListLoader = MutableStateFlow(false)
val peopleListLoader: StateFlow<Boolean> = _peopleListLoader
private val _peopleListErrorMessage = MutableStateFlow("")
var peopleListErrorMessage: StateFlow<String> = _peopleListErrorMessage
fun loadPeoplesList() {
viewModelScope.launch(dispatcher) {
_peopleListLoader.value = true // set the loader visibility true
repository.getPeopleList().collect() {
try {
when {
it.isSuccess -> {
_peopleList.value = it.getOrNull()!!
_peopleListErrorMessage.value = ""
}
it.isFailure -> {
_peopleListErrorMessage.value = it.exceptionOrNull().toString()
}
}
}finally {
_peopleListLoader.value = false // set the loader visibility false
}
}
}
}
}
Unit Test
class PeopleListViewModelShould {
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
//SUT - PeopleListViewModel
val repository: PeopleRepository = mock()
val peoplesList = mock<List<People>>()
val expected = Result.success(peoplesList)
#Test
fun turnLoaderVisibilityFalseAfterFetchingPeoplesList()= runTest{
whenever(repository.getPeopleList() ).thenReturn(
flow {
emit(expected)
}
)
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
val viewmodel = PeopleListViewModel(repository,testDispatcher)
val values = mutableListOf<Boolean>()
val collectJob= launch(testDispatcher){
viewmodel.peopleListLoader.toList(values)
}
viewmodel.loadPeoplesList()
System.out.println("Result is : "+values.toString()) //This prints ony [false] iam expecting [false,true,false]
assertEquals(true, values[0]) // Assert on the list contents
collectJob.cancel()
}
}
WHats is wrong with my unittest, I appreciate all suggestions and advance thanks to all..
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'm struggling with a ViewModel issue, I'm quite new to them.
I need to be able to access a Json file in my assets folder, so I'm using androidViewModel.
But I also need to pass a category into the viewModel, so I can filter the json file by that category.
What I've tried so far.
building a viewModeFactory
Creating a ResourcesProvider
creating a seperate viewModel for each category
Whats the best way to achieve this?
Here's my current viewModel
class GuideViewModel(application: Application) : AndroidViewModel(application) {
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
private val _difficulty = MutableLiveData(3)
val difficulty: LiveData<Int>
get() = _difficulty
private val _date = MutableLiveData<Long>(3010)
val date: LiveData<Long>
get() = _date
private val _graphic = MutableLiveData<String>()
val graphic: LiveData<String>
get() = _graphic
private var brewerDetails: MutableLiveData<MovieObject>? = null
fun getDetails(CurrentMovie: String): MutableLiveData<MovieObject> {
brewerDetails = MutableLiveData<MovieObject>()
loadJsonData(CurrentMovie)
return brewerDetails as MutableLiveData<MovieObject>
}
private fun loadJsonData(CurrentMovie: String) {
var CurrentMovieObject = MovieObject()
try {
val jsonString = loadJsonDataFromFile()
val json = JSONObject(jsonString)
val jsonBrewers = json.getJSONArray("movies")
for (index in 0 until jsonBrewers.length()) {
if (jsonBrewers.getJSONObject(index).getString(KEY_NAME) == "VARIABLE") { << need to pass a variable into viewModel somehow
val name = jsonBrewers.getJSONObject(index).getString(KEY_NAME)
val difficulty = jsonBrewers.getJSONObject(index).getInt(KEY_DIFFICULTY)
val date = jsonBrewers.getJSONObject(index).getLong(KEY_DATE)
val graphic = jsonBrewers.getJSONObject(index).getString(KEY_GRAPHIC)
_name.value = name
_difficulty.value = difficulty
_date.value = date
_graphic.value = graphic
}
}
} catch (e: JSONException) {
}
}
private fun loadJsonDataFromFile(): String {
var json = ""
try {
val input = getApplication<Application>().assets.open("movies.json") << need application to open the json file
val size = input.available()
val buffer = ByteArray(size)
input.read(buffer)
input.close()
json = buffer.toString(Charsets.UTF_8)
} catch (e: IOException) {
e.printStackTrace()
}
return json
}
}
I'm currently writing an app that displays a list of movies. The app has 8 fragments that contain the recyclerview: Trending Movies, Action, Comedy, Horror, Romance, Scifi, Search, and Favorites.
The items in the recyclerview contain a checkbox that adds the movie to the favorites. When I scroll or exit the app, the checkbox state resets. I'm trying to save the state of the checkbox using savestate but it's not working.
Can anyone please tell me what I'm doing wrong? Below is the viewmodel.
Thank you.
MoviesListViewModel.kt
package com.example.moviesapp.ui
import androidx.lifecycle.*
import com.example.moviesapp.network.MoviesRepository
import com.example.moviesapp.network.MoviesResults
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
const val DEFAULT_QUERY = " "
const val ACTION_MOVIES = "moviesAction"
const val COMEDY_MOVIES = "moviesComedy"
const val HORROR_MOVIES = "moviesHorror"
const val ROMANCE_MOVIES = "moviesRomance"
const val SCIFI_MOVIES = "moviesScifi"
const val TRENDING_MOVIES = "moviesTrending"
enum class MovieApiStatus {LOADING, ERROR, DONE}
#HiltViewModel
class MoviesListViewModel #Inject constructor(
private val repository: MoviesRepository,
private var state: SavedStateHandle
): ViewModel() {
private val _moviesAction: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(ACTION_MOVIES)
val moviesAction: LiveData<List<MoviesResults.Movies>> = _moviesAction
private val _moviesComedy: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(COMEDY_MOVIES)
val moviesComedy: LiveData<List<MoviesResults.Movies>> = _moviesComedy
private val _moviesHorror: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(HORROR_MOVIES)
val moviesHorror: LiveData<List<MoviesResults.Movies>> = _moviesHorror
private val _moviesRomance: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(
ROMANCE_MOVIES)
val moviesRomance: LiveData<List<MoviesResults.Movies>> = _moviesRomance
private val _moviesScifi: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(SCIFI_MOVIES)
val moviesScifi: LiveData<List<MoviesResults.Movies>> = _moviesScifi
private val _moviesTrending: MutableLiveData<List<MoviesResults.Movies>> = state.getLiveData(TRENDING_MOVIES)
val moviesTrending: LiveData<List<MoviesResults.Movies>> = _moviesTrending
private val _networkState = MutableLiveData<MovieApiStatus>()
val networkState: LiveData<MovieApiStatus> = _networkState
init {
getMovies()
}
fun getAction() {
viewModelScope.launch {
_moviesAction.value = repository.getActionMovies()
}
}
fun getComedy() {
viewModelScope.launch {
_moviesComedy.value = repository.getComedyMovies()
}
}
fun getHorror() {
viewModelScope.launch {
_moviesHorror.value = repository.getHorrorMovies()
}
}
fun getRomance() {
viewModelScope.launch {
_moviesRomance.value = repository.getRomanceMovies()
}
}
fun getScifi() {
viewModelScope.launch {
_moviesScifi.value = repository.getScifiMovies()
}
}
fun getTrending() {
viewModelScope.launch {
_moviesTrending.value = repository.getTrendingMovies()
}
}
private var currentQuery = MutableLiveData(DEFAULT_QUERY)
val movies = currentQuery.switchMap {
queryString ->
liveData {
emit(repository.getSearchResults(queryString))
}
}
fun searchMovies(query: String) {
currentQuery.value = query
}
private fun getMovies() {
viewModelScope. launch {
_networkState.value = MovieApiStatus.LOADING
try {
_networkState.value = MovieApiStatus.DONE
}
catch (e: Exception) {
_networkState.value = MovieApiStatus.ERROR
}
}
}
class MoviesListViewModelFactory #Inject constructor(private val repository: MoviesRepository, private val state: SavedStateHandle): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MoviesListViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return MoviesListViewModel(repository, state) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
Interface for accessing and modifying preference data returned by Context.getSharedPreferences(String, int). For any particular set of preferences, there is a single instance of this class that all clients share. Modifications to the preferences must go through an Editor object to ensure the preference values remain in a consistent state and control when they are committed to storage. Objects that are returned from the various get methods must be treated as immutable by the application.
Note: This class provides strong consistency guarantees. It is using expensive operations which might slow down an app. Frequently changing properties or properties where loss can be tolerated should use other mechanisms. For more details read the comments on Editor.commit() and Editor.apply().
Note: This class does not support use across multiple processes.
in your case you can simply store the key-value pair to record the relevant selection.
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.