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
}
}
Related
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.
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 want to use coroutines in my project only when I use coroutines I get the error :Unable to invoke no-args constructor. I don't know why it's given this error. I am also new to coroutines.
here is my apiclient class:
class ApiClient {
val retro = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Here is my endpoint class:
#GET("v2/venues/search")
suspend fun get(
#Query("near") city: String,
#Query("limit") limit: String = Constants.limit,
#Query("radius") radius: String = Constants.radius,
#Query("client_id") id: String = Constants.clientId,
#Query("client_secret") secret: String = Constants.clientSecret,
#Query("v") date: String
): Call<VenuesMainResponse>
my Repository class:
class VenuesRepository() {
private val _data: MutableLiveData<VenuesMainResponse?> = MutableLiveData(null)
val data: LiveData<VenuesMainResponse?> get() = _data
suspend fun fetch(city: String, date: String) {
val retrofit = ApiClient()
val api = retrofit.retro.create(VenuesEndpoint::class.java)
api.get(
city = city,
date = date
).enqueue(object : Callback<VenuesMainResponse>{
override fun onResponse(call: Call<VenuesMainResponse>, response: Response<VenuesMainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<VenuesMainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
my ViewModel class:
class VenueViewModel( ) : ViewModel() {
private val repository = VenuesRepository()
fun getData(city: String, date: String): LiveData<VenuesMainResponse?> {
viewModelScope.launch {
try {
repository.fetch(city, date)
} catch (e: Exception) {
Log.d("Hallo", "Exception: " + e.message)
}
}
return repository.data
}
}
part of activity class:
class MainActivity : AppCompatActivity(){
private lateinit var venuesViewModel: VenueViewModel
private lateinit var adapter: HomeAdapter
private var searchData: List<Venue>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.main_search)
venuesViewModel = ViewModelProvider(this)[VenueViewModel::class.java]
venuesViewModel.getData(
city = "",
date = ""
).observe(this, Observer {
it?.let { res ->
initAdapter()
rv_home.visibility = View.VISIBLE
adapter.setData(it.response.venues)
searchData = it.response.venues
println(it.response.venues)
}
})
this is my VenuesMainResponse data class
data class VenuesMainResponse(
val response: VenuesResponse
)
I think the no-args constructor warning should be related to your VenuesMainResponse, is it a data class? You should add the code for it as well and the complete Log details
Also, with Coroutines you should the change return value of the get() from Call<VenuesMainResponse> to VenuesMainResponse. You can then use a try-catch block to get the value instead of using enqueue on the Call.
Check this answer for knowing about it and feel free to ask if this doesn't solve the issue yet :)
UPDATE
Ok so I just noticed that it seems that you are trying to use the foursquare API. I recently helped out someone on StackOverFlow with the foursquare API so I kinda recognize those Query parameters and the Venue response in the code you provided above.
I guided the person on how to fetch the Venues from the Response using the MVVM architecture as well. You can find the complete code for getting the response after the UPDATE block in the answer here.
This answer by me has code with detailed explanation for ViewModel, Repository, MainActivity, and all the Model classes that you will need for fetching Venues from the foursquare API.
Let me know if you are unable to understand it, I'll help you out! :)
RE: UPDATE
So here is the change that will allow you to use this code with Coroutines as well.
Repository.kt
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
suspend fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
try {
val response = api.get(
longLat = longlat,
date = date
)
_data.value = response
} catch (e: Exception) {
_data.value = null
}
}
}
ViewModel.kt
class ViewModel : ViewModel() {
private val repository = Repository()
val data: LiveData<mainResponse?> = repository.data
fun getData(longLat: String, date: String) {
viewModelScope.launch {
repository.fetch(longLat, date)
}
}
}
api.kt
interface api {
#GET("v2/venues/search")
suspend fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): mainResponse
}
MainActivity.kt
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74",
date = "20210718" // date format is: YYYYMMDD
)
viewModel.data
.observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
Log.d("name ",name)
}
}
})
}
}
I have two tables in my Room DB - Events and Notes. For each event I have displayed in the RecycleView - I have a link to launch a note for that event. On first click - Note is created. On the second time the note is clicked, I would like to retrieve the previous note and then edit. Also, I am using the same activity to already edit/create new notes by passing on appropriate values, which works but uses parcelized note.
For editing an existing event Note - I am sending across the event ID (which is also stored in the Note table - not as a Foreign key) using the putExtra method. DB structure below (assocId refers to eventId)
ViewModel
fun setNotesByAssocEventId(assocEventId: String): Note {
return dao.getByAssocEventId(assocEventId)
}
DAO
#Query("SELECT * FROM notes WHERE assocEventId = :assocEventId")
fun getByAssocEventId(assocEventId: String): Note
NoteEntity
#Entity(tableName = "notes")
#Parcelize
data class Note(
//PrimaryKey annotation to declare primary key with auto increment value
//ColumnInfo annotation to specify the column's name
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "id") var id: Int = 0,
#ColumnInfo(name = "assocEventId") var assocEventId: String = "",
#ColumnInfo(name = "title") var title: String = "",
#ColumnInfo(name = "label") var label: String = "",
#ColumnInfo(name = "date") var date: String = "",
#ColumnInfo(name = "time") var time: String = "",
#ColumnInfo(name = "updatedDate") var updatedDate: String = "",
#ColumnInfo(name = "updatedTime") var updatedTime: String = "",
#ColumnInfo(name = "body") var body: String = ""
) : Parcelable
I am using the below code to edit/create new notes. While I am able to create/Edit notes. I am unable to retrieve a node for a particular event using the eventId. One of the errors I am getting is Note object has not been initialized when I am assigning the note object returned from the ViewModel. What could be the issue?
assocID is the event ID obtained using putExtra and the corresponding event note is to be retrieved...
private lateinit var binding: ActivityEditNoteBinding
private lateinit var notesViewModel: NotesViewModel
private lateinit var note: Note
private var assocId: String? = ""
private var isUpdate = false
private val dateChange = DateChange()
var refUsers: DatabaseReference? = null
var firebaseUser: FirebaseUser? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditNoteBinding.inflate(layoutInflater)
setContentView(binding.root)
assocId = intent.getStringExtra("eventId").toString()
initView()
initListener()
}
private fun initView() {
firebaseUser = FirebaseAuth.getInstance().currentUser
initViewModel()
if (assocId != null) {
findViewById<TextView>(R.id.editNote).text = "Edit Event Note"
Toast.makeText(this, "EvetnId received", Toast.LENGTH_SHORT).show()
isUpdate = true
binding.editNoteDelete.visibility = View.VISIBLE
notesViewModel.getNotes()
note = notesViewModel.setNotesByAssocEventId("%${assocId}%")
binding.editTextTitle.setText(note.title)
binding.editTextBody.setText(note.body)
binding.editTextTitle.setSelection(note.title.length)
//set spinner position
val compareValue = note.label
val adapter = ArrayAdapter.createFromResource(
this, R.array.NoteSpinnerVals,
android.R.layout.simple_spinner_item
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spLabel.adapter = adapter
val spinnerPosition = adapter.getPosition(compareValue)
binding.spLabel.setSelection(spinnerPosition)
}
}
private fun initViewModel() {
notesViewModel = ViewModelProvider(this).get(NotesViewModel::class.java)
}
private fun initListener() {
// binding.editNoteBack.setOnClickListener(this)
binding.editNoteSave.setOnClickListener(this)
binding.editNoteDelete.setOnClickListener(this)
}
private fun deleteNote(note: Note) {
notesViewModel.deleteNote(note)
Toast.makeText(this#EditNote, "Note removed", Toast.LENGTH_SHORT).show()
}
private fun showDialog() {
AwesomeDialog.build(this)
.position(AwesomeDialog.POSITIONS.CENTER)
.title("Delete the note?")
.icon(R.drawable.ic_delete_black)
.background(R.drawable.background_dialog)
.onPositive(
"Yes, delete",
buttonBackgroundColor = R.drawable.button_bg,
textColor = ContextCompat.getColor(this, R.color.white)
) {
deleteNote(note)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
.onNegative(
"Cancel",
buttonBackgroundColor = R.drawable.button_bg,
textColor = ContextCompat.getColor(this, R.color.white)
) {
}
}
Code of the ViewModel
class NotesViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
private val listNotes = MutableLiveData<ArrayList<Note>>()
private var dao: NoteDao
init {
val database = AppDatabase.getDatabase(context)
dao = database.getNoteDao()
}
fun setNotes() {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getAll())
listNotes.postValue(listItems)
}
fun setNotesByType(label: String) {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getByLabel(label))
listNotes.postValue(listItems)
}
fun setNotesByTitle(title: String) {
val listItems = arrayListOf<Note>()
listItems.addAll(dao.getByTitle(title))
listNotes.postValue(listItems)
}
fun setNotesByAssocEventId(assocEventId: String): Note {
return dao.getByAssocEventId(assocEventId)
}
fun insertNote(note: Note) {
dao.insert(note)
}
fun updateNote(note: Note) {
dao.update(note)
}
fun deleteNote(note: Note) {
dao.delete(note)
}
fun getNotes(): LiveData<ArrayList<Note>> {
return listNotes
}
}
The method in the DAO need to be changed a little
#Query("SELECT * FROM notes WHERE assocEventId = :assocEventId")
fun getByAssocEventId(assocEventId: String): Note
should be
#Query("SELECT * FROM notes WHERE assocEventId LIKE :assocEventId")
fun getByAssocEventId(assocEventId: String): LiveData<List<Note>>
In order to support wild character search, "%${assocId}%", LIKE keyword.
To get one Note only
#Query("SELECT * FROM notes WHERE assocEventId LIKE :assocEventId LIMIT 1")
fun getByAssocEventId(assocEventId: String): LiveData<Note>
in view model
fun setNotesByAssocEventId(assocEventId: String): LiveData<Note>{
return dao.getByAssocEventId(assocEventId)
}
in the activity
notesViewModel.setNotesByAssocEventId("%${assocId}%").observe(this, {
if(it!=null){
//if you using for single note only
}
//if(it.isNotEmpty()){
//if you using for list
//}
})
The Entity class. I want to fetch items as camera detector detects objects. What kind of typeconverter would take list of items class, confidence etc. ?
#Entity(tableName = "Objects_table")
data class Objects(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "object_id")
var id: Int,
#ColumnInfo(name = "object_name")
var name: DetectionResult?,
#ColumnInfo(name = "detected_time")
var time: Result?)
The Results class that I want to fetch
data class Result(
val id: Int,
val title: String,
val confidence: Float,
val location: RectF
) {
val text: String by lazy {
"$id:$title[${"%.2f".format(confidence)}]"
}
}
The ViewModel class:
class ObjectViewModel(private val repository: ObjectRepository) : ViewModel(), Observable {
val objects = repository.objects
private var objectDetected: ObjectDetectorAnalyzer? = null
fun saver(){
val name = objectDetected?.nobjects?.get(0)
val conf = objectDetected?.nobjects?.get(1)
}
The class from where I reference list of objects that lively changes:
class ObjectDetectorAnalyzer(
private val context: Context,
private val config: Config,
private val onDetectionResult: (Result) -> Unit
): ImageAnalysis.Analyzer {
private var inputArray = IntArray(config.inputSize * config.inputSize)
val nobjects = detect(inputArray)
private fun detect(inputArray: IntArray): List<DetectionResult> {
var detector = objectDetector
if (detector == null) {
detector = ObjectDetector(
assetManager = context.assets,
isModelQuantized = config.isQuantized,
inputSize = config.inputSize,
labelFilename = config.labelsFile,
modelFilename = config.modelFile,
numDetections = config.numDetection,
minimumConfidence = config.minimumConfidence,
numThreads = 1,
useNnapi = false
)
objectDetector = detector
}
return detector.detect(inputArray)
}
}
Thanks in advance!!
This example might help you.
I have one Animal entity and have its Breed List
#Entity(tableName = "Animal")
public class Animal {
#PrimaryKey(autoGenerate = true)
private long pid;
#TypeConverters(BreedConverter.class)
#ColumnInfo(name = "breed")
private ArrayList<Breed> breedArrayList;
}
I have annotation with #TypeConverters pointing to BreedConverter.class
class BreedConverter {
#TypeConverter
fun toBreedList(data: String): ArrayList<Breed> {
val listType = object : TypeToken<ArrayList<Breed>>() {}.type
return GsonBuilder().create().fromJson(data, listType)
}
#TypeConverter
fun toBreedString(breed: ArrayList<Breed>): String {
return GsonBuilder().create().toJson(breed)
}
}
Its working fine for my case. You can try this way. TypeConverter would convert the list to string while persisting and will convert back to list from string when you access this object using getters.