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
Related
I want to get a ViewModel for an attribute of another object held in another ViewModel.
I have this relationship: In a house there are multiple people. (A 1:n relationship where people are encoded in the Houses table, rather than using a join table.) I have a problem in this scenario:
An existing House is to be shown in HouseDetailsActivity, which contains a HouseDetailsFragment and a PeopleListFragment. The HouseDetailsActivity gets the HouseViewModel in onCreate, like this:
houseViewModel = ViewModelProviders.of(this, new HouseViewModel.Factory(getApplication(), id)).get(HouseViewModel.class);
The HouseViewModel is able to return LiveData, as it gets the HouseEntity from the database. The PeopleListFragment needs to get LiveData for the list of people for that house from somewhere, but should not need knowledge of any view model other than PeopleListViewModel. So, also in the HouseDetailsActivity onCreate, I get a PeopleListViewModel, like this:
peopleListViewModel = ViewModelProviders.of(this).get(PeopleListViewModel.class);
that I expect can be shared with the PeopleListFragment, getting it like this:
peopleViewModel = ViewModelProviders.of(getActivity()).get(PeopleListViewModel.class);
The problem is how to get the list of people in LiveData into the ViewModel. The list of people in the HouseEntity inside the HouseDetailsActivity (HouseDetailsViewModel) is not LiveData. (I want to be able to see the list of people from the HouseEntity in the PeopleListFragment via a PeopleListViewModel.)
I've seen the documentation for MediatorLiveData, which I don't think applies here, because ultimately there is only 1 source of the PeopleList.
public class HouseDetailsActivity
{
protected void onCreate(Bundle savedInstanceState)
{
houseViewModel = ViewModelProviders.of(this, new HouseViewModel.Factory(getApplication(), id)).get(HouseViewModel.class);
peopleListViewModel = ViewModelProviders.of(this).get(PeopleListViewModel.class);
/* This can't be done, because the HouseEntity may not yet be loaded to the ViewModel. ie. NullPointerException here
List<Person> people = m_houseViewModel.getHouse().getPeopleList();
peopleListViewModel.setPeople(people);
*/
}
}
#Entity(tableName="houses")
public class HouseEntity implements MutableHouse
{
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name="hid")
public int id = 0;
#ColumnInfo(name="address")
private String address = null;
/** This is the encoded people, for multiple in a single database field. */
#ColumnInfo(name="residents")
private String residents = null;
public List<Person> getPeopleList ()
{ return HouseEncoding.decodePeople(getResidents()); }
...
}
public class HouseViewModel
{
private final int houseId;
private MutableLiveData<HouseEntity> house; // The list of people is inside house here, but not as LiveData
public LiveData<HouseEntity> getObservableHouse ()
{ return house; }
HouseViewModel (#NonNull Application application, int houseId)
{
super(application);
this.houseId = houseId;
this.house = getRepository().getHouseObservable(houseId);
}
/**
* A creator is used to inject the house ID into the ViewModel
*/
public static class Factory extends ViewModelProvider.NewInstanceFactory
{
#NonNull
private Application application;
private int houseId;
public Factory (#NonNull Application application, int houseId)
{
this.application = application;
this.houseId = houseId;
}
#Override
#NonNull
public <T extends ViewModel> T create (#NonNull Class<T> modelClass)
{
//noinspection unchecked
return (T) new HouseViewModel(application, houseId);
}
}
}
public class PeopleListViewModel
{
private MutableLiveData<List<Person>> people;
void setPeople (List<Person> people)
{ this.people.setValue(people); }
...
}
Within the PeopleListFragment:
private void observerSetup ()
{
peopleViewModel.getPeople().observe(this, people -> {
adapter.setPeople(people); // for RecyclerView
});
}
I see you have initialized these two viewmodels differently.
houseViewModel = ViewModelProviders.of(this, new HouseViewModel.Factory(getApplication(), id)).get(HouseViewModel.class);
peopleListViewModel = ViewModelProviders.of(this).get(PeopleListViewModel.class);
Just change:
houseViewModel = ViewModelProviders.of(this, new HouseViewModel.Factory(getApplication(), id)).get(HouseViewModel.class);
to this:
houseViewModel = ViewModelProviders.of(this, ViewModelProviders.of(this).get(HouseViewModel.class);
You can initialize two viewmdoels in a view. Don't be shy about it.
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.
I've followed https://github.com/googlecodelabs/android-build-an-app-architecture-components.
I want to be able to fetch weather data by city name.
I've created the required method in the DAO class.
I've changed my code in the Repository class to:
public LiveData<List<ListWeatherEntry>> getCurrentWeatherForecasts(String cityName) {
initializeData();
Date today = SunshineDateUtils.getNormalizedUtcDateForToday();
return mWeatherDao.getCurrentWeatherForecasts(today,cityName);
}
But in my ViewModel class when I'm trying to use this function in the Transformation.switchMap, Im getting compile time error that the method getCurrentWeatherForecasts(String ) cannot be applied to getCurrentWeatherForecasts().
Here's my code in ViewModel class:
private final SunshineRepository mRepository;
public LiveData<List<ListWeatherEntry>> mForecast;
private final MutableLiveData<String> cityName = new MutableLiveData();
public MainActivityViewModel(SunshineRepository repository) {
this.mRepository = repository;
mForecast = Transformations.switchMap(this.cityName,(city)->
mRepository.getCurrentWeatherForecasts(city));
}
I've read Android's Transformations.switchMap docs, but I couldn't figure out what am I doing wrong.
Can anyone explain me what's wrong with my code.
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
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