Passing arguments to AndroidViewModel - android

I'm following the google tutorial for Room persistence but i'm stuck, right now I have the tutorial all working fine but I need to expand it and be able to pass parameters to the ViewModel because what I need is to be able to submit different queries to the repo, and maybe i'm wrong but right now i'm doing it in the ViewModel which should be able to read his field and choose the right method to talk with the repo.
WordViewModel:
public class WordViewModel extends AndroidViewModel {
private WordRepository mRepository;
private LiveData<List<Word>> mAllWords;
public int mode = 0;
public WordViewModel (Application application) {
super(application);
mRepository = new WordRepository(application);
if (mode==0)
mAllWords = mRepository.getAllWords();
else
mAllWords = mRepository.getSomethingElse();
}
LiveData<List<Word>> getAllWords() { return mAllWords; }
public void insert(Word word) { mRepository.insert(word); }
}
Then in the activity the triggers the model view we got this
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
mWordViewModel.mode=1; //MY ADDITION, not working
...
mWordViewModel.getAllWords().observe(this, new Observer<List<Word>>() {
#Override
public void onChanged(#Nullable final List<Word> words) {
// Update the cached copy of the words in the adapter.
adapter.setWords(words);
}
});
...
Now the problem is that the field access and edit (the "mode" field) i've made is not working, it's like the field is getting resetted when the ViewModel is actually called and so it's always 0. What am i Missing? What is the easiest workaround considering that mode is just for explaining and eventually i'll need a lot of parameters (so creating various ViewModel is not an option)

I think you're running in to issues related to lifecycle of ViewModel itself and different variables etc you're using. I'd recommend using something like MediatorLiveData for what you're trying to do...for example (this is in Kotlin btw as that's what I'm using for similar logic I have)
class WordViewModel : ViewModel() {
.....
val mode: MutableLiveData<Int> = MutableLiveData()
val mAllWords = MediatorLiveData<List<Word>>().apply {
this.addSource(mode) {
if (mode.value == 0)
this.value = mRepository.getAllWords()
else
this.value = mRepository.getSomethingElse()
}
}
init {
mode.value = 0
}
fun setMode(m: Int) {
mode.value = m
}
}
The code where I'm doing this here is https://github.com/joreilly/galway-bus-android/blob/master/base/src/main/java/com/surrus/galwaybus/ui/viewmodel/BusStopsViewModel.kt

Related

How to use LiveData for searching?

I have a ViewModel that has a MutableLiveData of an arraylist of class Course
private var coursesList: MutableLiveData<ArrayList<Course>> = MutableLiveData()
This coursesList is filled with data got from an API (by Retrofit): coursesList.postValue(response.body())
Now, a user can search for a course by its name. The function that I have for searching is that I iterate through the elements of the coursesList and check if its name is equal to what a user typed. It returns an arrayList with the courses that start with the name typed (this list is later sent to a fragment which passes it to an adapter to be shown in a recyclerview):
fun getCoursesList(): MutableLiveData<ArrayList<Course>> {
return coursesList
}
fun searchCourses(searchString: String): ArrayList<Course> {
val resultsList: ArrayList<Course> = ArrayList()
if (getCoursesList().value == null) return resultsList
if (getCoursesList().value!!.size > 0) {
for (course in getCoursesList().value!!.iterator()) {
if (course.name.toLowerCase(Locale.ROOT).startsWith(searchString)) {
resultsList.add(course)
}
}
}
resultsList.sortBy { it.price }
return resultsList
}
This function works and all but my instructor asked me to use LiveData for searching without giving any additional hints on how to do that.
So my question is how to use LiveData for searching? I tried to search for answers, I saw that some used LiveDataTransformations.switchMap but they were all using RoomDAOs and I couldn't adapt it to the code that I have.
Any help would be appreciated very much. Thanks in advance.
Maybe that can help you a little bit,
class YourViewModel(
private val courcesRepository: CourcesRepository
) : ViewModel() {
// Private access - mutableLiveData!
private val _coursesList = MutableLiveData<ArrayList<Course>>()
// Public access - immutableLiveData
val coursesList: LiveData<ArrayList<Course>>
get() = _coursesList
init {
// mutableLiveData initialize, automatic is immutable also initialize
_coursesList.postValue(getCourses())
}
// Here you get your data from repository
private fun getCourses(): ArrayList<Course> {
return courcesRepository.getCources()
}
// Search function
fun searchCourses(searchString: String) {
// you hold your data into this methode
val list: ArrayList<Course> = getCources()
if (searchString.isEmpty()) {
// here you reset the data if search string is empty
_coursesList.postValue(list)
} else {
// here you can search the list and post the new one to your LiveData
val filterList = list.filter {
it.name.toLowerCase(Locale.ROOT).startsWith(searchString)
}
filterList.sortedBy { it.price }
_coursesList.postValue(filterList)
}
}
}
The first tip is you should use LiveData like below, that is also recommended from google's jet pack team. The reason is so you can encapsulate the LivaData.
The second tip is you should use kotlin's idiomatic way to filter a list. Your code is readable and faster.
At least is a good idea to make a repository class to separate the concerns in your app.
And some useful links for you:
https://developer.android.com/jetpack/guide
https://developer.android.com/topic/libraries/architecture/livedata
I hope that's helpful for you
Ii is hard to guess the desired outcome, but a possible solution is to use live data for searched string also. And then combine them with coursesList live data into live data for searched courses, like this for example.
val searchStringLiveData: MutableLiveData<String> = MutableLiveData()
val coursesListLiveData: MutableLiveData<ArrayList<Course>> = MutableLiveData()
val searchedCourses: MediatorLiveData<ArrayList<Course>> = MediatorLiveData()
init {
searchedCourses.addSource(searchStringLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
searchedCourses.addSource(coursesListLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
}
fun combineLiveData(searchStringLiveData: LiveData<String>, coursesListLiveData: LiveData<ArrayList<Course>> ): ArrayList<Course> {
// your logic here to filter courses
return ArrayList()
}
I haven't run the code so I am not 100% sure that it works, but the idea is that every time either of the two live data changes value, searched string or courses, the combine function is executed and the result is set as value of the searchedCourses mediator live data. Also I omitted the logic of the filtering for simplicity.

MVVM injecting Activity string resource reader

So, everyone knows that passing a Context reference (which is not Application) to ViewModel is a bad thing. In my case, there are some items to be ordered alphabetically using an android string resource representation (so, to read it I need an Activity Context).
What is the recommended way to do it? Passing a List of items from ViewModel to Activity, to read those strings and back to ViewModel does look a bit not so MVVM-ish, and injecting ViewModel with a string resource reader would leak the Context..
Any thoughts on that?
One option would be to extend from AndroidViewModel instead, which has a reference to the Application Context. You can then use that Context to load the string resources and deliver them back to your Activity.
public class MyViewModel extends AndroidViewModel {
private final LiveData<String> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
statusLabel.setValue(context.getString(R.string.labelString));
}
public LiveData<String> getStringResource() {
return stringResource;
}
}
However, as it is pointed out in this Android Developers Medium Post by Jose Alcerreca, this is not the recommended practice because if, for example, the Locale changes and the Activity gets rebuilt, the ViewModel will not react to this configuration change and will keep delivering the obsolete strings (from the previous Locale).
Therefore, the suggested approach is to only return the resources ids from the ViewModel and get the strings on the Activity.
public class MyViewModel extends ViewModel {
public final LiveData<Integer> stringResource = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringResource.setValue(R.string.labelString);
}
public LiveData<Integer> getStringResource() {
return stringResource;
}
}
UPDATE
Since you must get the string resources from your Activity but apply the sorting logic in your ViewModel, I don't think you can't avoid passing the List<String> back to your ViewModel to be sorted:
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
List<String> sortedStrings = viewModel.sortList(Arrays.asList(resolvedStrings));
updateUi(sortedStrings);
});
}
}
If you think that's not MVVM'ish enough, maybe you can keep resolved List<String> in your ViewModel and have an extra LiveData with the sorted list, that will be updated every time the LiveData holding the original string list changes.
public class MyViewModel extends ViewModel {
public final MutableLiveData<Integer> stringArrayId = new MutableLiveData<>();
public final MutableLiveData<List<String>> stringsList = new MutableLiveData<>();
public final LiveData<List<String>> sortedStringList;
public MyViewModel(Application context) {
super(context);
stringArrayId.setValue(R.array.string_array_id);
sortedStringList = Transformations.map(stringsList, l -> {
Collections.sort(l);
return l;
});
}
public LiveData<Integer> getStringArrayId() {
return stringArrayId;
}
public LiveData<List<String>> sortedStringList() {
return sortedStringList;
}
public void setStringsList(List<String> resolvedStrings) {
stringsList.setValue(resolvedStrings);
}
}
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
viewModel.getStringArrayId().observe(this, strArrayId -> {
String[] resolvedStrings = getResources().getStringArray(strArrayId);
viewModel.setStringsList(Arrays.asList(resolvedStrings));
});
viewModel.sortedStringList().observe(this, sortedStrings -> updateUi(sortedStrings));
}
}
It feels over-engineered to me, and you still have to send the List<String> back to your ViewModel. However, having it this way might help if the sorting order depends on a Filter that can change during runtime. Then, you can add a MediatorLiveData to react either when the Filter changes or the list of Strings changes, then your view only have to inform those changes to the ViewModel and will observe the sorted list.
Ideally Data Binding should be used with which this problem can easily be solved by resolving the string inside the xml file. But implementing data binding in an existing project can be too much.
For a case like this I created the following class. It covers all cases of strings with or without arguments and it does NOT require for the viewModel to extend AndroidViewModel and this way also covers the event of Locale change.
class ViewModelString private constructor(private val string: String?,
#StringRes private val stringResId: Int = 0,
private val args: ArrayList<Any>?){
//simple string constructor
constructor(string: String): this(string, 0, null)
//convenience constructor for most common cases with one string or int var arg
constructor(#StringRes stringResId: Int, stringVar: String): this(null, stringResId, arrayListOf(stringVar))
constructor(#StringRes stringResId: Int, intVar: Int): this(null, stringResId, arrayListOf(intVar))
//constructor for multiple var args
constructor(#StringRes stringResId: Int, args: ArrayList<Any>): this(null, stringResId, args)
fun resolve(context: Context): String {
return when {
string != null -> string
args != null -> return context.getString(stringResId, *args.toArray())
else -> context.getString(stringResId)
}
}
}
USAGE
for example we have this resource string with two arguments
<string name="resource_with_args">value 1: %d and value 2: %s </string>
In ViewModel class:
myViewModelString.value = ViewModelString(R.string.resource_with_args, arrayListOf(val1, val2))
In Fragment class (or anywhere with available context)
textView.text = viewModel.myViewModelString.value?.resolve(context)
Keep in mind that the * on *args.toArray() is not a typing mistake so do not remove it. It is syntax that denotes the array as Object...objects which is used by Android internaly instead of Objects[] objects which would cause a crash.

Get value of live data

I am using Room, ViewModel and two-way databinding. In this simple example I need to select data from database and validate it. If data are valid then expose it to databinding. In the other case I have to select other data from database.
DAO:
#Dao
public interface IDAOQuestion {
#Query("SELECT * FROM Question WHERE questionId = :questionId")
Question selectQuestion(long qeustionId);
}
Entity:
#Entity
public class Question {
#NonNull
#PrimaryKey(autoGenerate = true)
private long questionId;
#NonNull
private MutableLiveData<Integer> correct = new MutableLiveData<>();
public int getCorrectValue() {
return correct.getValue() == null ? 0 : correct.getValue();
}
}
ViewModel
public class QuestionViewModel extends AndroidViewModel {
public LiveData<Question> mLDQuestion = new MutableLiveData<>();
public void getQuestion(int questionId) {
//pseudo DAO access
Question question = IDAOQuestion.selectQuestion(questionId);
//here it is always true (question.getCorrectValue() returns 0)
if(question.getCorrectValue() == 0) {
getQuestion(questionId + 1);
} else {
mLDQuestion.setValue(question);
}
}
}
Also I have type converter to convert int from database to LiveData of entity.
public class LiveDataIntegerTypeConverter {
#TypeConverter
public static int toInteger(LiveData<Integer> value) {
if (value == null || value.getValue() == null) {
return 0;
} else {
return value.getValue();
}
}
#TypeConverter
public static MutableLiveData<Integer> toObservable(int value) {
MutableLiveData<Integer> liveData = new MutableLiveData<>();
liveData.postValue(value);
return liveData;
}
}
In ViewModel in the function getQuestion I have some "validation". It dowsn't work right. the code question.getCorrectValue() always returns 0. It is because of using MutableLiveData.postValue in TypeConverter and not existing observer on the property.
As workaround I can create another entity (POJO class) without LiveData and use it for validation. After that I can reselect data or map it to "LiveData version" of object. But this seems to be crazy and too complicated. What is the right approach to solve this?
This is just simple example. The logic is just for illustration. I understand why this is happening. Also I can't solve this by changing my SELECT query. Also I have to use the live data on my attributes because I am using it with two way databinding with depending attributes.

Paging Library Filter/Search

I am using the Android Paging Library like described here:
https://developer.android.com/topic/libraries/architecture/paging.html
But i also have an EditText for searching Users by Name.
How can i filter the results from the Paging library to display only matching Users?
You can solve this with a MediatorLiveData.
Specifically Transformations.switchMap.
// original code, improved later
public void reloadTasks() {
if(liveResults != null) {
liveResults.removeObserver(this);
}
liveResults = getFilteredResults();
liveResults.observeForever(this);
}
But if you think about it, you should be able to solve this without use of observeForever, especially if we consider that switchMap is also doing something similar.
So what we need is a LiveData<SelectedOption> that is switch-mapped to the LiveData<PagedList<T>> that we need.
private final MutableLiveData<String> filterText = savedStateHandle.getLiveData("filterText")
private final LiveData<List<T>> data;
public MyViewModel() {
data = Transformations.switchMap(
filterText,
(input) -> {
if(input == null || input.equals("")) {
return repository.getData();
} else {
return repository.getFilteredData(input); }
}
});
}
public LiveData<List<T>> getData() {
return data;
}
This way the actual changes from one to another are handled by a MediatorLiveData.
I have used an approach similar to as answered by EpicPandaForce. While it is working, this subscribing/unsubscribing seems tedious. I have started using another DB than Room, so I needed to create my own DataSource.Factory anyway. Apparently it is possible to invalidate a current DataSource and DataSource.Factory creates a new DataSource, that is where I use the search parameter.
My DataSource.Factory:
class SweetSearchDataSourceFactory(private val box: Box<SweetDb>) :
DataSource.Factory<Int, SweetUi>() {
var query = ""
override fun create(): DataSource<Int, SweetUi> {
val lazyList = box.query().contains(SweetDb_.name, query).build().findLazyCached()
return SweetSearchDataSource(lazyList).map { SweetUi(it) }
}
fun search(text: String) {
query = text
}
}
I am using ObjectBox here, but you can just return your room DAO query on create (I guess as it already is a DataSourceFactory, call its own create).
I did not test it, but this might work:
class SweetSearchDataSourceFactory(private val dao: SweetsDao) :
DataSource.Factory<Int, SweetUi>() {
var query = ""
override fun create(): DataSource<Int, SweetUi> {
return dao.searchSweets(query).map { SweetUi(it) }.create()
}
fun search(text: String) {
query = text
}
}
Of course one can just pass a Factory already with the query from dao.
ViewModel:
class SweetsSearchListViewModel
#Inject constructor(
private val dataSourceFactory: SweetSearchDataSourceFactory
) : BaseViewModel() {
companion object {
private const val INITIAL_LOAD_KEY = 0
private const val PAGE_SIZE = 10
private const val PREFETCH_DISTANCE = 20
}
lateinit var sweets: LiveData<PagedList<SweetUi>>
init {
val config = PagedList.Config.Builder()
.setPageSize(PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.setEnablePlaceholders(true)
.build()
sweets = LivePagedListBuilder(dataSourceFactory, config).build()
}
fun searchSweets(text: String) {
dataSourceFactory.search(text)
sweets.value?.dataSource?.invalidate()
}
}
However the search query is received, just call searchSweets on ViewModel. It sets search query in the Factory, then invalidates the DataSource. In turn, create is called in the Factory and new instance of DataSource is created with new query and passed to existing LiveData under the hood..
You can go with other answers above, but here is another way to do that: You can make the Factory to produce a different DataSource based on your demand. This is how it's done:
In your DataSource.Factory class, provide setters for parameters needed to initialize the YourDataSource
private String searchText;
...
public void setSearchText(String newSearchText){
this.searchText = newSearchText;
}
#NonNull
#Override
public DataSource<Integer, SearchItem> create() {
YourDataSource dataSource = new YourDataSource(searchText); //create DataSource with parameter you provided
return dataSource;
}
When users input new search text, let your ViewModel class to set the new search text and then call invalidated on the DataSource. In your Activity/Fragment:
yourViewModel.setNewSearchText(searchText); //set new text when user searchs for a text
In your ViewModel, define that method to update the Factory class's searchText:
public void setNewSearchText(String newText){
//you have to call this statement to update the searchText in yourDataSourceFactory first
yourDataSourceFactory.setSearchText(newText);
searchPagedList.getValue().getDataSource().invalidate(); //notify yourDataSourceFactory to create new DataSource for searchPagedList
}
When DataSource is invalidated, DataSource.Factory will call its create() method to create newly DataSource with the newText value you have set. Results will be the same

Android Architecture Components LiveData

I'm trying to implement a simple App using Architecture Components.
I can get the info from RestApi services using Retrofit2.
I can show the info in the respective Recyclerview and when I rotate the phone everything works as it should.
Now I want to filter by a new kind of object (by string)
Can someone guide me a little with the ViewModel, I don't know what is the best practice to do that...
I'm using MVVM...
This is my ViewModel:
public class ListItemViewModel extends ViewModel {
private MediatorLiveData<ItemList> mList;
private MeliRepository meliRepository;
/* Empty Contructor.
* To have a ViewModel class with non-empty constructor,
* I have to create a Factory class which would create instance of you ViewModel and
* that Factory class has to implement ViewModelProvider.Factory interface.
*/
public ListItemViewModel(){
meliRepository = new MeliRepository();
}
public LiveData<ItemList> getItemList(String query){
if(mList == null){
mList = new MediatorLiveData<>();
LoadItems(query);
}
}
private void LoadItems(String query){
String queryToSearch = TextUtils.isEmpty(query) ? "IPOD" : query;
mList.addSource(
meliRepository.getItemsByQuery(queryToSearch),
list -> mList.setValue(list)
);
}
}
UPDATE
I resolved this using transformation a package from lifecycle library...
enter link description here
public class ListItemViewModel extends ViewModel {
private final MutableLiveData<String> mQuery = new MutableLiveData<>();
private MeliRepository meliRepository;
private LiveData<ItemList> mList = Transformations.switchMap(mQuery, text -> {
return meliRepository.getItemsByQuery(text);
});
public ListItemViewModel(MeliRepository repository){
meliRepository = repository;
}
public LiveData<ItemList> getItemList(String query){
return mList;
}
}
#John this is my solution. I'm using lifecycle library and the solution was easier than I thought. Thx!
I'm more familiar with doing this in Kotlin but you should be able to translate this to Java easily enough (or perhaps now is a good time to start using Kotlin :) )....adapting similar pattern I have here I believe you'd do something like:
val query: MutableLiveData<String> = MutableLiveData()
val mList = MediatorLiveData<List<ItemList>>().apply {
this.addSource(query) {
this.value = meliRepository.getItemsByQuery(query)
}
}
fun setQuery(q: String) {
query.value = q
}
I'm using this pattern in following https://github.com/joreilly/galway-bus-android/blob/master/app/src/main/java/com/surrus/galwaybus/ui/viewmodel/BusStopsViewModel.kt

Categories

Resources