I have a Dictionary app where I want to assign existing synonyms to a word in the dictionary. To accomplish this I am using is using a M:N relationship between the word and synonym tables.
Entities:
#Entity(tableName = "word_table",
indices = #Index(value = "word", unique = true))
public class Word {
#PrimaryKey(autoGenerate = true)
private long id;
private String word;
#Ignore
public Word(String word) {
this.word = word;
}
public Word(long id, String word) {
this.id = id;
this.word = word;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
}
#Entity(tableName = "synonym_table")
public class Synonym {
#PrimaryKey(autoGenerate = true)
private long sid;
private String synonym;
#Ignore
public Synonym(String synonym) {
this.synonym = synonym;
}
public Synonym(long sid, String synonym) {
this.sid = sid;
this.synonym = synonym;
}
public long getSid() {
return sid;
}
public void setSid(long id) {
this.sid = sid;
}
public String getSynonym() {
return synonym;
}
public void setSynonym(String synonym) {
this.synonym = synonym;
}
}
#Entity(tableName = "word_synonym_join_table",
primaryKeys= {"word_id" , "synonym_id"},
foreignKeys = {#ForeignKey(entity = Word.class, parentColumns = "id", childColumns = "word_id"),
#ForeignKey(entity = Synonym.class, parentColumns = "sid", childColumns = "synonym_id")})
public class WordSynonymJoin {
#ColumnInfo(name = "word_id")
private long wordId;
#ColumnInfo(name = "synonym_id")
private long synonymId;
public WordSynonymJoin(long wordId, long synonymId) {
this.wordId = wordId;
this.synonymId = synonymId;
}
public long getWordId() {
return wordId;
}
public void setWordId(long wordId) {
this.wordId = wordId;
}
public long getSynonymId() {
return synonymId;
}
public void setSynonymId(long synonymId) {
this.synonymId = synonymId;
}
}
To retrieve the data for the Word and associated Synonyms, I created a POJO called WordWithSynonyms.
public class WordWithSynonyms {
#Embedded
public Word word;
#Embedded
public WordSynonymJoin wordSynonymJoin;
}
The Daos are as follows:
#Dao
public interface WordDao {
#Query("SELECT * FROM word_table")
public LiveData<List<Word>> getAllWords();
#Query("SELECT * FROM word_table WHERE id =:wordId")
public LiveData<List<Word>> getWordById(long wordId);
#Query("SELECT * from word_table WHERE word =:value")
public LiveData<List<Word>> getWordByValue(String value);
#Insert
public long insert(Word word);
#Delete
public void delete(Word word);
#Update
public void update(Word word);
#Query("DELETE FROM word_table")
public void deleteAll();
}
#Dao
public interface SynonymDao {
#Query("SELECT * FROM synonym_table")
public LiveData<List<Synonym>> getAllSynonyms();
#Query("SELECT * FROM synonym_table WHERE synonym =:value")
public LiveData<List<Synonym>> getSynonymByValue(String value);
#Insert
public void insert(Synonym synonym);
#Delete
public void delete(Synonym synonym);
#Query("DELETE FROM synonym_table")
public void deleteAll();
}
#Dao
public interface WordSynonymJoinDao {
#Query("SELECT * FROM word_table INNER JOIN word_synonym_join_table " +
"ON word_table.id = word_synonym_join_table.word_id " +
"WHERE word_synonym_join_table.synonym_id =:synonymId")
public LiveData<List<WordWithSynonyms>> getWordsBySynonym(long synonymId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id =:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByWord(long wordId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id !=:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByNotWord(long wordId);
#Insert
public void insert(WordSynonymJoin wordSynonymJoin);
#Delete
public void delete(WordSynonymJoin wordSynonymJoin);
#Query("DELETE FROM word_synonym_join_table")
public void deleteAll();
}
When I arrive on the Synonyms Activity, i pass the wordId to retrieve the current synonyms for that word through a ViewModel observer.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synonym);
Intent intent = getIntent();
wordId = Long.parseLong(intent.getStringExtra(EXTRA_WORD_ID));
//SynonymViewModel synonymViewModel = ViewModelProviders.of(this).get(SynonymViewModel.class);
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(this).get(WordSynonymJoinViewModel.class);
//synonymAdapter = new SynonymListAdapter(this);
synonymAdapter = new SynonymWithWordListAdapter(this);
synonynRecyclerView = findViewById(R.id.recycler_view_syonym);
if (wordId != 0) {
wordSynonymJoinViewModel.getSynonymsByWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
synonymAdapter.setSynonyms(synonymWithWords);
synonymAdapter.notifyDataSetChanged();
}
});
}
synonynRecyclerView.setAdapter(synonymAdapter);
synonynRecyclerView.setLayoutManager(new LinearLayoutManager(SynonymActivity.this));
}
I then give the user the opportunity to associate an existing, unassigned synonym from the Synonym table to the Word table.
I retrieve the unused and available Synonyms through a separate ViewModel observer inside of an AlertDialog which uses a spinner to display them via the WordSynonymJoin table using another ViewModel observer.
Finally, inside of that ViewModel observer when the user clicks the OK button on the AlertDialog, a third VieModel observer is ran to do the actual insertion into the WordSynonymJoin table.
case R.id.synonym_assign_synonym:
final WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
List<String> synonymsNotAssignList = new ArrayList<>();
for (SynonymWithWords sww : synonymWithWords)
synonymsNotAssignList.add(sww.synonym.getSynonym());
AlertDialog.Builder assignSynonymDialog = new AlertDialog.Builder(SynonymActivity.this);
assignSynonymDialog.setTitle("Select New Category:");
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.alert_dialog_spinner_view, null);
final Spinner synonymSpinner = (Spinner) view.findViewById(R.id.alert_dialog_spinner);
final SynonymViewModel synonymViewModel = ViewModelProviders.of(SynonymActivity.this).get(SynonymViewModel.class);
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter(SynonymActivity.this, android.R.layout.simple_spinner_dropdown_item, synonymsNotAssignList);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
synonymSpinner.setAdapter(spinnerAdapter);
synonymSpinner.setSelection(synonymId);
assignSynonymDialog.setView(view);
assignSynonymDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String synonymValue = synonymSpinner.getSelectedItem().toString();
// get new synonym id
synonymViewModel.getSynonymByValue(synonymValue).observe(SynonymActivity.this, new Observer<List<Synonym>>() {
#Override
public void onChanged(#Nullable List<Synonym> synonyms) {
long id = 0;
if (!synonyms.get(0).getSynonym().equals(synonymValue)) {
if (synonyms.size() > 1)
Toast.makeText(SynonymActivity.this, "Query found " + synonyms.size() + " which is more than the one expected.", Toast.LENGTH_SHORT).show();
} else {
id = synonyms.get(0).getSid();
}
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.insert(new WordSynonymJoin(wordId, id));
}
});
}
});
assignSynonymDialog.setNegativeButton("Cancel", null);
assignSynonymDialog.create();
assignSynonymDialog.show();
}
});
return true;
On the first pass, all seems to work well. However, on successive passes where the user continues to add new synonyms to the word, it takes that many clicks on the cancel button of the AlertDialog to exit after each synonym added. 2 synonyms added, 2 click on the cancel to get back to main Activity. 3 synonyms added, 3 clicks on the cancel to remove the AlertDialog.
I am very new to this whole concept of MVVM and Room persistence so I know there will be issues. Here is the code for the AlertDialog for adding existing, unassigned synonyms to the current word.
I don't like how much code is being used for this, but i have not been able to word my searches so that I can find ways around it.
My questions are:
Why is the code cycling +1 every time I enter associate new synonym to the word? Am I suppose to be clearing something out.
Is this coding even remotely right?
This seems like an awful lot of work to accomplish something so seemingly small. I think I have missed something. Have I made this abnormally complicated?
What am I missing that this code looks so cumbersome and unwieldy?
It seems a very cumbersome way to retrieve values and I don't really think i need to observe every query that I ran above. Maybe I am wrong.
Is there a direction of study that will help me understand this better?
Could this be where the Rx Java comes in?
I can certainly provide more code as needed.
Any help would be appreciated.
TRDL: Don't call .observe outside of ON_CREATE state.
You made a LiveData mistake... but you are not alone! That mistake is the most common LiveData mistake on StackOverflow: calling .observe outside of Activity#onCreate(). This includes calling .observe in a click listener, on onResume, broadcast receiver, etc.
The problem I see in most people who uses LivedData for the first time is that they treat LiveData just like a call back, when they are not. LiveData is a stream. LiveData does not notify just one time. The Observers attached to the LiveData will continue to be notified until they are unsubscribed. Also, It is meant to be subscribed at the beginning of the life-cycle, (e.g. Activity#onCreate or Fragment#onViewCreated) and unsubscribed at the end of the life-cycle. LiveData automatically handles the unsubscription part, so all you need to make sure is to subscribe in onCreate.
The fundamental reason you are keep getting +1 Dialog is that the previous observer is not dead and you are keep adding a new subscription to the database each time you repeat the same thing. Try rotating the phone and see if the number of dialog resets back to 1. That's because all of the previous observers are unsubscribed when you rotate the screen and activity is recreated.
Maybe you could call isShowing() and see if any dialog is open, as suggested in another answer. However, doing so is just a work around. What if it was a Toast or something else that you can't check? Besides, you are lucky that you could easily spot this bug. You might be having this duplicate observer bug some place else that is not visually noticeable.
So I think you already know how to use LiveData, but it is just that you need to know how to implement reactive pattern correctly. It would be too much to explain in one writing but let me give you a simple example:
Lets say you have a button that when you press, you fetch some data from DB. In a callback-like design you often call some function in ViewModel and pass a callback instance. For example you have this:
//ViewModel
void getSynonymsByNotWord(WordSynonymJoin word, Callback callback) { ... }
//Activity
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSysnonymsByNotWord(wordId, callback);
}
You perform an action to ViewModel and you receive the response through callback. This is perfectly fine for callback. However, you can't do the same with LiveData. When using LiveData, View layer don't expect that there will be a response for each of the action. Instead, View layer should always blindly listen to the response, even before the button is clicked.
//ViewModel
private MutableLiveData wordQuery;
private Livedata synonymsByNotWord = Transformations.switchMap(wordQuery, word -> {
return repository.getSynonymsByWord(word);
});
LiveData getCurrentSynonymsByNotWord() {
return synonymsByNotWord;
}
void changeCurrentSynonymsByNotWord(WordSynonymJoin word) {
wordQuery.postValue(word);
}
//Activity
void onCreate() {
wordSynonymJoinViewModel.getCurrentSynonymsByNotWord().observe(...);
}
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSynonymsByNotWord(wordId);
}
And also it is okay to, but you normally don't get ViewModel from ViewModelProviders every time you need a view model. You should just get one view model at onCreate, save it as an activity instance variable, and use the same instance in the rest of the activity.
Here:
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
you are monitoring for synonyms, but inside of the observing, you show a dialog and allow more synonyms to be added. Everytime a new synonym is added, it creates a new AlertDialog.
So this is why you have to press cancel on each dialog.
To fix that, you can assign your AlertDialog to a field and use the isShowing() method to decide if you want to show another dialog (i.e. don't show another one if one is already showing.
https://developer.android.com/reference/android/app/Dialog.html#isShowing()
As for all your other questions, I'm sorry it's a bit too much for me to unpack.
I can share my thoughts on how I would do this though:
I want to assign existing synonyms to a word in the dictionary.
Perhaps forget the database to start with and create an in memory solution.
Then later you can change this to be persisted.
In memory the structure looks like a Hashtable of Dictionary words and Synonym lookups Map<String, List<String>>.
This Map would be in a class called Repository that exposes someway for you to observe and update it (RxJava Observable) or LiveData like you have already.
Your Fragment would observe this Map displaying it in a RecyclerView using MVVM or MVP whatever you want.
You have a clicklistener on each row of the RecyclerView to add a new synonym. On click opens the dialog (or a new activity/fragment). After the user types the synonym you will save this through the repository to your Map - and therefore the original observer will update the RecyclerView.
You should not get in a loop state of opening multiple dialogs :/
Hope that helps, tbh it sounds like you are on the right track and just need to work at it a bit more.
I'm building an offline-first app with the database setup as the single source of truth. I am using Room to simplify the database handling, and LiveData to simplify observable data patterns.
I am also using Retrofit to make any network calls required to populate the database with new data.
I have set up an observer in my Fragment as follows:
private void setUpObserver() {
tfViewModel = ViewModelProviders.of(getActivity()).get(TFViewModel.class);
tfViewModel.getAllPosts().observe(getActivity(),
newPosts -> {
if (newPosts != null && newPosts.size() > 0) {
lottieAnimationView.setVisibility(View.INVISIBLE);
mPostsAdapter.updateItems(newPosts);
}
});
tfViewModel.fetchNextData(currentPage);
}
When my app first starts, I'm deleberately truncating each table in my database using Room callbacks so that new data is fetched every time. (For testing. This beats the offline-first experience and must not be done in production.)
Anyway, so when it first starts, it calls the fetchNextData method of the viewmodel which in turn asks the Repository to fetch the data.
Here's my ViewModel:
public class TFViewModel extends AndroidViewModel {
private TFRepository mRepository;
private LiveData<List<Post>> mPostList;
public TFViewModel(Application application) {
super(application);
mRepository = new TFRepository(application);
mPostList = mRepository.getAllPosts();
}
public LiveData<List<Post>> getAllPosts() {
return mPostList;
}
public void fetchNextData(int page) {
mRepository.fetchNextPosts(page);
}
}
In the repository, I use my DAOs to insert posts into the database. To fetch new data, I use a Service Class to fetch new data for me. When the fetch call returns, I use an AsyncTask to insert the new posts to my database. (Details omitted for brevity):
public class TFRepository {
private PostDao postDao;
private LiveData<List<Post>> postList;
private RetrofitSingleton retrofitSingleton;
public TFRepository(Application application) {
TFRoomDatabase db = TFRoomDatabase.getDatabase(application);
postDao = db.postDao();
retrofitSingleton = RetrofitSingleton.getInstance(application.getApplicationContext());
postList = postDao.getAllPosts();
}
public LiveData<List<Post>> getAllPosts() {
return postList;
}
public void fetchNextPosts(int page) {
getPostList(page);
}
private void getPostList(int page) {
APICaller.getInstance(retrofitSingleton).getFeed(page,
new NetworkResponseListener<BaseResponse<FeedResponse>>() {
#Override
public void onResponseReceived(BaseResponse<FeedResponse> feedResponseBaseResponse) {
if (feedResponseBaseResponse == null) return;
List<Post> posts = feedResponseBaseResponse.getData().getPosts();
new insertAllPostsAsyncTask(postDao).execute(posts);
}
#Override
public void onError(String errorMessage) {
}
});
}
}
The OBSERVER I had setup in my fragment gets an empty list the first time around. The API call returns with the first page of posts and it receives 10 posts the second time. The view is popualted. Everything is good.
Problem: As the user scrolls down, the Fragment asks the ViewModel to fetch more data. The ViewModel asks the Repository to fetch new data. The Retrofit call goes and comes back with the new data. It is inserted in the database. BUT THE OBSERVER IS NOT NOTIFIED. What am I missing?
NOTE: I do not want to use a MutableLiveData as I want to maintain the DB as the single source of truth. Also, as the docs state that LiveData is notified whenever the underlying DB changes, my implementation should work with LiveData.
I'm using room library with android. When I insert something in the database I get data for current running application, but when I restart the application, there is nothing in the screen. How to get previous stored data without using LiveData ?
If you are looking forward to get previous data, the following Codelab might help you. But this really depends on your implementation.
How to get previous stored data without using LiveData ?
How to get all rows stored in the room library using Async Task?
Explanation:
In Your Dao include the following code:
#Query("SELECT * FROM user")
List<CustomObject> getAll();
In your Repository class include this snippet:
public List<CustomObject> getAll() throws ExecutionException, InterruptedException {
return new getAllAsyncTask(mCustomObjectDao).execute().get();
}
private static class getAllAsyncTask extends android.os.AsyncTask<Void, Void, List<CustomObject>> {
private CustomObjectDao mAsyncTaskDao;
List<CustomObject> a;
getAllAsyncTask(CustomObjectDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected List<CustomObject> doInBackground(Void... voids) {
return mAsyncTaskDao.getAll();
}
}
While learning the new android Architecture component’s ViewModel and LiveData, having a little confusion when observe the LiveData changing from database source change, and how this would work with Cursor adapter.
in https://developer.android.com/reference/android/widget/CursorAdapter.html, it says
int FLAG_REGISTER_CONTENT_OBSERVER
If set the adapter will register a content observer on the cursor
and will call onContentChanged() when a notification comes in. Be
careful when using this flag: you will need to unset the current
Cursor from the adapter to avoid leaks due to its registered
observers. This flag is not needed when using a CursorAdapter
with a CursorLoader.
so the with cursorAdaptor it has a way to get the ‘live update’ when the database data is updated.
is there a way to use the LiveData (to observe the database data update) with the cursorAdaptor?
trying to show the question of where to use the liveData updating the cursor in snippet below:
(with the sample of https://codelabs.developers.google.com/codelabs/android-persistence)
the Book:
#Entity
public class Book {
public #PrimaryKey String id;
public String title;
}
The ViewModel:
public class BooksBorrowedByUserViewModel extends AndroidViewModel {
public final LiveData<List<Book>> books;
private AppDatabase mDb;
public BooksBorrowedByUserViewModel(Application application) {
super(application);
createDb();
// Books is a LiveData object so updates are observed.
books = mDb.bookModel().findBooksBorrowedByName("Mike"); //<=== this ViewModel specific to one type query statement
}
public void createDb() {
mDb = AppDatabase.getInMemoryDatabase(this.getApplication());
// Populate it with initial data
DatabaseInitializer.populateAsync(mDb);
}
}
is this the way to use LiveData observer to force reload cursor?
private CursorAdapter listAdapter;
private BooksBorrowedByUserViewModel mViewModel;
private void subscribeUiBooks() {
mViewModel.books.observe(this, new Observer<List<Book>>() {
#Override
public void onChanged(#NonNull final List<Book> books) {
showBooksInUi(books, mBooksTextView); //<== the sample’s code
// if would like to update the cursorAdaptor
//
// ??? to requery the database and swap cursor here?
// Cursor data = queryData(buildSqlStatement()); // build the same sql statement as used in the BooksBorrowedByUserViewModel
// listAdapter.swapCursor(data)
}
});
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//having a list using CursorAdaptor
ListView list = getListView();
listAdapter = new CursorAdapter(getActivity(), null, 0)
list.setAdapter(listAdapter);
// Get a reference to the ViewModel for this screen.
mViewModel = ViewModelProviders.of(this).get(BooksBorrowedByUserViewModel.class);
subscribeUiBooks();
}
CursorAdapter is an old stuff, you should use Room + LiveData + RecyclerView.
Your data layer:
public LiveData<List<UserEntity>> getUsers() {
return userDao.getUsers();
}
Your activity:
viewModel.getUsers().observe(this, new Observer<List<UserEntity>>() {
#Override
public void onChanged(#Nullable List<UserEntity> users) {
if (users != null) {
adapter.setUsers(users);
}
}
});
In adapter:
private List<UserEntity> users = new ArrayList<>();
public void setUsers(List<UserEntity> users) {
this.users.clear();
this.users.addAll(users);
notifyDataSetChanged();
}
So when your activity lunch you should get live data from Room and subscribe to it. After that when you add something to that table, room automatically update observers, so you should just setup new data to the adapter and notify it.
Guide to App Architecture
LiveData
Room
i have a listview in my project that links to database and shows its context from database but my problem is whenever my application runs it records goes twice (ex.2records first run, 4records with same context,...) and i do not know that wat is problem
this is my database class:
new File(DIR_DATABASE).mkdirs();
dataBase = SQLiteDatabase.openOrCreateDatabase(DIR_DATABASE + "/information.sqlite", null);
dataBase.execSQL("CREATE TABLE IF NOT EXISTS information (" +
"information_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE ," +
"information_title TEXT)");
dataBase.execSQL("INSERT INTO information (information_title) VALUES ('قسمت اول')");
dataBase.execSQL("INSERT INTO information (information_title) VALUES ('قسمت دوم')");
and its my main class that shows listview:
ListView lstC findViewById(R.id.lstContent);
adapter = new AdapterNote(title);
lstContent.setAdapter(adapter);
readFromDataBase();
adapter.notifyDataSetChanged();
}
private void readFromDataBase() {
Cursor cursor = G.dataBase.rawQuery("SELECT * FROM information", null);
while (cursor.moveToNext()) {
StructNote detail = new StructNote();
detail.title = cursor.getString(cursor.getColumnIndex("information_title"));
title.add(detail);
}
cursor.close();
adapter.notifyDataSetChanged();
}
You need to understand SQLiteOpenHelper for this. This is workflow issue, so it would be better you should go through some tutorial. This is a nice tutorial where you can learn the concept.
In very short i am listing few points that may be useful for you:
In your application you create a subclass of the SQLiteOpenHelper class.SQLiteOpenHelper is a helper class to manage database creation and version management. In subclass override, onCreate() and onUpgrade().
public class MySQLiteHelper extends SQLiteOpenHelper {
public void onCreate(SQLiteDatabase database) {
// create database command.
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// upgrade database command here.
}
}
Create a DAO class that will manage the interaction with the database. Your CRUD methods will go here. See DAO pattern for details. This class will centralize the access to database, hence your activity, fragment will interact with this class to perform operation. Direct access to database won't be allowed.
public class ModelDataSource {
// needed to perform operation on database
private SQLiteDatabase database;
//needed to retrieve database object
private MySQLiteHelper dbHelper;
public boolean insertModel(Model model) {
// perform insert operation on database
}
}
In your activity, you can interact with DAO to perform some action.
public class YourActivity extends Activity {
private ModelDataSource datasource;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
datasource = new ModelDataSource(this);
datasource.open();
boolean insertion_status = datasource.insert(modelobject);
}
}