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.
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
In Android room persistent library how to insert entire Model object into table which has in itself another list.
Let me show you what i mean :
#Entity(tableName = TABLE_NAME)
public class CountryModel {
public static final String TABLE_NAME = "Countries";
#PrimaryKey
private int idCountry;
private List<CountryLang> countryLang = null;
public int getIdCountry() {
return idCountry;
}
public void setIdCountry(int idCountry) {
this.idCountry = idCountry;
}
public String getIsoCode() {
return isoCode;
}
public void setIsoCode(String isoCode) {
this.isoCode = isoCode;
}
/**
here i am providing a list of coutry information how to insert
this into db along with CountryModel at same time
**/
public List<CountryLang> getCountryLang() {
return countryLang;
}
public void setCountryLang(List<CountryLang> countryLang) {
this.countryLang = countryLang;
}
}
my DAO looks like this:
#Dao
public interface CountriesDao{
#Query("SELECT * FROM " + CountryModel.TABLE_NAME +" WHERE isoCode =:iso_code LIMIT 1")
LiveData<List<CountryModel>> getCountry(String iso_code);
#Query("SELECT * FROM " + CountryModel.TABLE_NAME )
LiveData<List<CountryModel>> getAllCountriesInfo();
#Insert(onConflict = REPLACE)
Long[] addCountries(List<CountryModel> countryModel);
#Delete
void deleteCountry(CountryModel... countryModel);
#Update(onConflict = REPLACE)
void updateEvent(CountryModel... countryModel);
}
When i call database.CountriesDao().addCountries(countryModel); i get the following room db compile error:
Error:(58, 31) error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
should there be another table called CountryLang ? and if so how to tell room to connect them on insert statement ?
The CountryLang object itself looks like this:
public class CountryLang {
private int idCountry;
private int idLang;
private String name;
public int getIdCountry() {
return idCountry;
}
public void setIdCountry(int idCountry) {
this.idCountry = idCountry;
}
public int getIdLang() {
return idLang;
}
public void setIdLang(int idLang) {
this.idLang = idLang;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
the response looks like this:
"country_lang": [
{
"id_country": 2,
"id_lang": 1,
"name": "Austria"
}
]
For every country so its not going to be more then one item here. Im comfortable desgning it for just one item in the country_lang list. So i can just make a table for country_lang and then some how link it to CountryModel. but how ? can i use foreign key ? i was hoping i did not have to use a flat file. so your saying i have to store it as json ? Is it recommended not to use room for temporary ? what to use instead ?
You can easly insert the class with list object field using TypeConverter and GSON,
public class DataConverter {
#TypeConverter
public String fromCountryLangList(List<CountryLang> countryLang) {
if (countryLang == null) {
return (null);
}
Gson gson = new Gson();
Type type = new TypeToken<List<CountryLang>>() {}.getType();
String json = gson.toJson(countryLang, type);
return json;
}
#TypeConverter
public List<CountryLang> toCountryLangList(String countryLangString) {
if (countryLangString == null) {
return (null);
}
Gson gson = new Gson();
Type type = new TypeToken<List<CountryLang>>() {}.getType();
List<CountryLang> countryLangList = gson.fromJson(countryLangString, type);
return countryLangList;
}
}
Next, Add the #TypeConverters annotation to the AppDatabase class
#Database(entities = {CountryModel.class}, version = 1)
#TypeConverters({DataConverter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract CountriesDao countriesDao();
}
For more information about TypeConverters in Room check our blog here and the official docs.
Here is the Aman Gupta's converter in Kotlin for lazy Googler's who enjoy copy pasting:
class DataConverter {
#TypeConverter
fun fromCountryLangList(value: List<CountryLang>): String {
val gson = Gson()
val type = object : TypeToken<List<CountryLang>>() {}.type
return gson.toJson(value, type)
}
#TypeConverter
fun toCountryLangList(value: String): List<CountryLang> {
val gson = Gson()
val type = object : TypeToken<List<CountryLang>>() {}.type
return gson.fromJson(value, type)
}
}
Also, add the #TypeConverters annotation to the AppDatabase class
#Database(entities = arrayOf(CountryModel::class), version = 1)
#TypeConverters(DataConverter::class)
abstract class AppDatabase : RoomDatabase(){
abstract fun countriesDao(): CountriesDao
}
As Omkar said, you cannot. Here, I describe why you should always use #Ignore annotation according to the documentation: https://developer.android.com/training/data-storage/room/referencing-data.html#understand-no-object-references
You will treat the Country object in a table to retrieve the data of its competence only; The Languages objects will go to another table but you can keep the same Dao:
Countries and Languages objects are independent, just define the primaryKey with more fields in Language entity (countryId, languageId). You can save them in series in the Repository class when the active thread is the Worker thread: two requests of inserts to the Dao.
To load the Countries object you have the countryId.
To load the related Languages objects you already have the countryId, but you will need to wait that country is loaded first, before to load the languages, so that you can set them in the parent object and return the parent object only.
You can probably do this in series in the Repository class when you load the country, so you will load synchronously country and then languages, as you would do at the Server side! (without ORM libraries).
You cannot.
The only way to achieve this is to use #ForeignKey constraint. If you want to still keep the list of object inside your parent POJO, you have to use #Ignore or provide a #TypeConverter
For more info, follow this blog post:-
https://www.bignerdranch.com/blog/room-data-storage-on-android-for-everyone/
and sample code:-
https://github.com/googlesamples/android-architecture-components
I had a similar situation. To solve this, I used TypeConverts and Moshi to parse the list to string.
Follow the steps below:
1 - Create a class with converters.
class Converters {
private val moshi = Moshi.Builder().build()
private val listMyData : ParameterizedType = Types.newParameterizedType(List::class.java, MyModel::class.java)
private val jsonAdapter: JsonAdapter<List<MyModel>> = moshi.adapter(listMyData)
#TypeConverter
fun listMyModelToJsonStr(listMyModel: List<MyModel>?): String? {
return jsonAdapter.toJson(listMyModel)
}
#TypeConverter
fun jsonStrToListMyModel(jsonStr: String?): List<MyModel>? {
return jsonStr?.let { jsonAdapter.fromJson(jsonStr) }
}
}
2 - Define the class with Converters in your RoomDatabase class.
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {...}
...you add the #TypeConverters annotation to the AppDatabase class so that Room can use the converter that you've defined for each entity and DAO in that AppDatabase...
...sometimes, your app needs to use a custom data type whose value you would like to store in a single database column. To add this kind of support for custom types, you provide a TypeConverter, which converts a custom class to and from a known type that Room can persist.
References:
Moshi Parsing List (Kotlin)
How to parse a list? #78 (answered by Jake Wharton)
Use type converters (official documentation)
Moshi library
I did something similar to #Daniel Wilson, however, I used Moshi since it is the suggested library. To learn more about the difference between Moshi and Gson I suggest you watch this video.
In my case, I had to store a List<LatLng> inside the Room database. In case you didn't know LatLng is used to handle the geographic coordinates, which means latitude and longitude.
To achieve that I used this code:
class Converters {
private val adapter by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val listMyData = Types.newParameterizedType(List::class.java, LatLng::class.java)
return#lazy moshi.adapter<List<LatLng>>(listMyData)
}
#TypeConverter
fun toJson(coordinates: List<LatLng>) : String {
val json = adapter.toJson(coordinates)
return json
}
#TypeConverter
fun formJson(json: String) : List<LatLng>? {
return adapter.fromJson(json)
}
}
Add #Embedded for the custom object field (refer following eg)
//this class refers to pojo which need to be stored
#Entity(tableName = "event_listing")
public class EventListingEntity implements Parcelable {
#Embedded // <<<< This is very Important in case of custom obj
#TypeConverters(Converters.class)
#SerializedName("mapped")
public ArrayList<MappedItem> mapped;
//provide getter and setters
//there should not the duplicate field names
}
//add converter so that we can store the custom object in ROOM database
public class Converters {
//room will automatically convert custom obj into string and store in DB
#TypeConverter
public static String
convertMapArr(ArrayList<EventListingEntity.MappedItem> list) {
Gson gson = new Gson();
String json = gson.toJson(list);
return json;
}
//At the time of fetching records room will automatically convert string to
// respective obj
#TypeConverter
public static ArrayList<EventsListingResponse.MappedItem>
toMappedItem(String value) {
Type listType = new
TypeToken<ArrayList<EventsListingResponse.MappedItem>>() {
}.getType();
return new Gson().fromJson(value, listType);
}
}
//Final db class
#Database(entities = {EventsListingResponse.class}, version = 2)
#TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
....
}
I'm coming really late to the party, but I'd really recommend using Kotlin Serialization.
class CountryLangConverter {
#TypeConverter
fun toCountryLang(countryLang: String): CountryLang =
Json.decodeFromString(countryLang)
#TypeConverter
fun fromCountryLang(countryLang: CountryLang): String =
Json.encodeToString(countryLang)
}
And don't forget to check the official documentation about Room and its Type Converters.