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.
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.
I am using Android Room, and I would like to get ID of new inserted row. I have declared column in my model class:
#PrimaryKey (autoGenerate = true)
#ColumnInfo (name = "productID")
int id;
And then I know I can retrive it by dao returning long:
#Insert
long insert(Product p);
At first I was using "thread" calls directly in View. And as you know, it is not recommended method. So I am trying to change it for ModelView and repository. But I don't know how can I get this ID.
My repository class:
public class ProductRepository {
private ProductDao mProductDao;
ProductRepository(Application application) {
AppDatabase db = AppDatabase.getDatabase(application);
mProductDao = db.pDao();
}
public void insertProduct(Product p) {
new insertAsyncTask(mProductDao).execute(p);
}
private static class insertAsyncTask extends AsyncTask<Product, Void, Void> {
private ProductDao mAsyncTaskDao;
insertAsyncTask(ProductDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(final Product... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
}
And my model class:
public class ProductModelView extends AndroidViewModel {
private ProductRepository mRepository;
public ProductModelView(Application application) {
super(application);
mRepository = new ProductRepository(application);
}
public void insert(Product p) {
mRepository.insertProduct(p);
}
}
And in my Activity I am inserting new object like this:
mProductModelView.insert(pc);
So how I can retrive this long value from "insert" and get it in my activity? I guess LiveData could be a good way to go, but to be honest I dont havy any ideas how to achieve it :(
The best way to do this is by using LiveData. If you want to use MVVM might as well learn how to use LiveData. It's easy.
In your DAO interface, declare a method like this:
#Query("SELECT * FROM Product ORDER BY id DESC LIMIT 1")
LiveData<Product> getLastProductLive();
This method returns the last Product inserted as LiveData
Then inside your Repository:
public LiveData<Product> getLastProductLive(){
return mProductDao.getLastProductLive();
}
And then inside your ViewModel:
public LiveData<Product> getLastProductLive(){
return mRepository.getLastProductLive();
}
And finally inside your Activity:
mProductViewModel.getLastProductLive().observe(this, product -> {
long lastInsertedRowId = product.getId();
}
By using LiveData, any time that a product is added to table, it triggers this method and you can get the id of the last inserted row.
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'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
I was looking at the BasicSample app from Android Architecture components sample. In the ProductViewModel.java file, some comments read:
It's not
actually necessary in this case, as the product ID can be passed in a public method.
Based on my understanding of the comment, I would like to know if it's possible to pass the productId to the ProductViewModel without using a factory, and how this can be done.
I have implemented Transformation, and I know I can pass a productId using switchMap. But I was looking for a way to initialize the model with a single id.
public class ProductViewModel extends AndroidViewModel {
private final LiveData<ProductEntity> mObservableProduct;
public ObservableField<ProductEntity> product = new ObservableField<>();
private final int mProductId;
private final LiveData<List<CommentEntity>> mObservableComments;
public ProductViewModel(#NonNull Application application, DataRepository repository,
final int productId) {
super(application);
mProductId = productId;
mObservableComments = repository.loadComments(mProductId);
mObservableProduct = repository.loadProduct(mProductId);
}
....
/**
* A creator is used to inject the product ID into the ViewModel
* <p>
* This creator is to showcase how to inject dependencies into ViewModels. It's not
* actually necessary in this case, as the product ID can be passed in a public method.
*/
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
private final int mProductId;
private final DataRepository mRepository;
public Factory(#NonNull Application application, int productId) {
mApplication = application;
mProductId = productId;
mRepository = ((BasicApp) application).getRepository();
}
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
//noinspection unchecked
return (T) new ProductViewModel(mApplication, mRepository, mProductId);
}
}
}
In reference to the sample, when the comment says:
the product ID can be passed in a public method
this is referring to the fact you can create a public setter method.
Since the productId is used to get a LiveData from your database, you should use a switchMap, as you mentioned. This is because switchMap allows you to lookup and update what a LiveData is pointing to, without needing to re-setup observers. If you didn't use a switchMap, you'd need to tell your Activity to observe the newly looked-up LiveData, and potentially stop observing the old LiveData object. More description of this is included in the docs.
One more note - the factory is also useful here because you're passing in or injecting the DataRepository dependency via the constructor. This is a preferable way to get the repository into the class because it's easy to mock the repository when testing.
With that in mind, if you wanted to do this would a factory, your code might look something like:
public class ProductViewModel extends AndroidViewModel {
private final LiveData<ProductEntity> mProduct;
private final LiveData<List<CommentEntity>> mComments;
private final MutableLiveData<Integer> mProductId = new MutableLiveData<>();
public ProductViewModel(#NonNull Application application) {
super(application);
// Have some way to get your repository, this is not great for testing...
Repository repository = ((BasicApp) application).getRepository();
mProduct = Transformations.switchMap(mProductId, id -> {
return repository.loadProduct(id);
}
mComments = Transformations.switchMap(mComments, id -> {
return repository.loadComments(id);
}
}
public void setProductId(int productId) {
mProductId.setValue(productId); // This will trigger both of those switchMap statements
}
}
Where does the productId come from ?
If it's loaded from repository (database or web services), you don't have to expose it on ViewModel I guess.
If it's a "dynamic" value, stored into SharedPreferences or setted from views, you can expose a Setter as follow
.
public void setProductId(int productId) {
if(mProductId == -1) { // or check mObservableComments
mProductId = productId;
mObservableComments = repository.loadComments(mProductId);
mObservableProduct = repository.loadProduct(mProductId);
}
}