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.
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 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.
I've set up a room database with 3 columns (title, descriptions, genre). I want to query the genre column with a user-specified genre(comedy, horror, etc) and return the results.
DAO Interface
I want the Query to only retrieve the entries where the genre matches the genre selected by the user.
#Dao
public interface MovieDAO {
#Query SELECT * FROM movie_table WHERE genre")
pubic LiveData<List<Movie>> getAllMovies();
}
Repository Class
In the Repository.class, can I pass the genre String selected by the user to the Query this way?
public class MovieRepository {
private MovieDao movieDao;
private LiveData<List<Movie>> allMovies;
public MovieRepository(Application application) {
MovieDatabase database = MovieDatabase.getInstance(application);
movieDao = database.MovieDao();
allMovies = movieDao.getAllMovies
}
public void findMoviesByGenre(String genre) {
movieDao.findMoviesByGenre(genre);
}
}
ViewModel class
I'm not sure if I'm missing something in the findMovieByGenre() method
public class MovieViewModel exteneds AndroidViewModel {
private MovieRepository repository;
private LiveData<List<Movie>> allMovies
// Constructor,
public MovieViewModel(#NonNull Application application) {
super(application);
repository = new MovieRepository(Application)
allMovies = repository.getAllMovies();
}
**public void findMovieByGenre(String genre) {
repository.findMoviesByGenre(genre);
}**
}
Activity
This is the part I'm really struggling with, how does the activity call the ViewModel and pass in the genre string parameter? I've tried the approach below but the observe returns the following error.
Cannot resolve method 'observe(com.example.roomexample.MainActivity, anonymous android.arch.lifecycle.Observer>)'
If I remove the genre string in from of the observe, I get the error below.
findMovieByGenre(String)in MovieViewModel cannot be applied
to ()
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
movieViewModel = ViewModelProviders.of(this). get(MovieViewModel.class);
movieViewModel.findMovieByGenre("comedy").observe(this, new Observer<List<Movie>>() {
#Override
public void onChanged(#Nullable final List<Movie> movies) {
Log.d("Movie", movies.get(num).getTitle());
Log.d("Movie", movies.get(num).getDescription());
Log.d("Movie", movies.get(num).getGenre());
}
});
}
In short I want to match the genre selected by the user and match it to the genre entry in the database and return the matching results.
My code is based on the following tutorials. If you have any additional material that code help me in my quest please pass it along.
Google coding Labs
Coding in Flow
Here is a link to my code as it currently stands.
https://github.com/Shawn-Nichol/RoomExample
If you are using MVVM architecture with LiveData follow this method.
1. Observe the LiveData List in MoviesActivity.java
final LiveData<List<MoviesData>> viewModelData = moviesViewModel.getMoviesByGenre("comedy");
viewModelData.observe(this, new Observer<List<MoviesData>>() {
#Override
public void onChanged(List<MoviesData> moviesData) {
//Handle the Movies List here.
}
});
2. In MoviesViewModel.java
public LiveData<List<NotificationData>> getMoviesByGenre(String genere) {
MoviesRepository mRepository = new MoviesRepository(application);
LiveData<List<MoviesData>> mMoviesData = mRepository.getMoviesByGenre(genere);
return mMoviesData;
}
3. MoviesRepository.java
private MoviesDao mMoviesDao;
//Initialize.
AppDatabase db = AppDatabase.getAppDatabase(application);
mMoviesDao = db.moviesDao();
public LiveData<List<MoviesData>> getMoviesByGenre(String genere) {
mMovies = mMoviesDao.findMovieByGenre(genere);
return mMovies;
}
3. In MoviesDao
#Query("SELECT * FROM movie_table ORDER BY genre")
public LiveData<List<Movies> findMovieByGenre(String genre);
So you can Observe query result in your Activity class' Observe method.
To access your app's data using the Room persistence library, you work with data access objects, or DAOs. You may have to use DAO in android room.
By accessing a database using a DAO class instead of query builders or direct queries, you can separate different components of your database architecture
#Dao
public interface MyDao {
#Query("SELECT * FROM movie_table ORDER BY genre")
public ListMovies[] findMovieByGenre(String Genre);
}
I'm trying to store values of some variable that my application regulary obtains from API. I whant to add new row to the database table only when variable changes its value to be able to show user some kind of "history of changes". I'm using ROOM for storing data.
I've created an entity:
#Entity(tableName = "balance_history",
indices = {#Index("received_at")})
public class BalanceResponse {
//region getters & setters
...
//endregion
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "id")
private long mId;
#ColumnInfo(name = "money")
private double mMoney;
#ColumnInfo(name = "received_at")
private DateTime mReceivedAt;
}
Dao:
#Dao
public abstract class DatabaseDao {
#Query("SELECT * FROM balance_history ORDER BY received_at DESC LIMIT 1")
public abstract LiveData<BalanceResponse> selectLatestBalanceResponse();
public void insertNewBalanceResponse(BalanceResponse balanceResponse) {
String sqlRequest = "INSERT INTO balance_history(money, received_at) " +
"SELECT ?, ? " +
"WHERE NOT EXISTS(SELECT 1 FROM (SELECT * FROM balance_history ORDER BY received_at DESC LIMIT 1) WHERE money = ?);";
SupportSQLiteDatabase database = DatabaseStorage.getInstance().getAppDatabase().getOpenHelper().getWritableDatabase();
database.execSQL(sqlRequest,
new Object[]{balanceResponse.getMoney(), balanceResponse.getReceivedAt().getMillis(), balanceResponse.getMoney()});
}
}
Database object:
#Database(entities = {BalanceResponse.class}, version = 1)
#TypeConverters(DateTimeConverter.class)
public abstract class AppDatabase
extends RoomDatabase {
public abstract DatabaseDao getDatabseDao();
}
Singleton for storing single database object:
public class DatabaseStorage {
//region singleton
private static final DatabaseStorage ourInstance = new DatabaseStorage();
public static DatabaseStorage getInstance() {
return ourInstance;
}
//endregion
#NonNull
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
#NonNull
private AppDatabase mAppDatabase;
private DatabaseStorage() {
mAppDatabase =
Room.databaseBuilder(MyApp.getAppContext(), AppDatabase.class, "app-database")
.build();
}
}
And viewmodel that I instantiate in my Activity's onCreate():
public class BalanceView implements Observer<BalanceResponse> {
private LiveData<BalanceResponse> mLatestBalanceResponse;
public BalanceView(LifecycleOwner lifecycleOwner){
mLatestBalanceResponse = DatabaseStorage.getInstance().getAppDatabase().getDatabseDao()
.selectLatestBalanceResponse();
mLatestBalanceResponse.observe(lifecycleOwner, this);
//finding views here
}
#Override
public void onChanged(#Nullable BalanceResponse balanceResponse) {
//displaying changes here
}
}
I've expected triggering of BalanceView.onChanges() method each time when method DatabaseDao.insertNewBalanceResponse() inserts a row.
Actually BalanceView.onChanges() method never gets fired. Why is that so? How can I accomplish this?
p.s. However, If I replace method DatabaseDao.insertNewBalanceResponse() with original:
#Insert
public abstract Long insertBalanceResponse(BalanceResponse balanceResponse);
Everithing works fine and method onChange() gets invoked. But this kind of insert statement doesn't fit my needs.
I have the same issue and here I got a hint to solve this issue.
#Dao
interface RawDao {
#RawQuery
User getUserViaQuery(SupportSQLiteQuery query);
}
SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
new Object[]{userId});
User user2 = rawDao.getUserViaQuery(query);
For More details, check https://developer.android.com/reference/android/arch/persistence/room/RawQuery.