Java POJO Object
public class Section {
#ColumnInfo(name="section_id")
public int mSectionId;
#ColumnInfo(name="section_name")
public String mSectionName;
public int getSectionId() {
return mSectionId;
}
public void setSectionId(int mSectionId) {
this.mSectionId = mSectionId;
}
public String getSectionName() {
return mSectionName;
}
public void setSectionName(String mSectionName) {
this.mSectionName = mSectionName;
}
}
My Query method
#Query("SELECT * FROM section")
LiveData<List<Section>> getAllSections();
Accessing DB
final LiveData<List<Section>> sections = mDb.sectionDAO().getAllSections();
On the next line I am checking sections.getValue() which is always giving me null although I have data in the DataBase and later I am getting the value in the onChanged() method.
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
}
});
But when I omit LiveData from the query I am getting the data as expected.
Query Method:
#Query("SELECT * FROM section")
List<Section> getAllSections();
Accessing DB:
final List<Section> sections = mDb.sectionDAO().getAllSections();
On the next line I am checking sections.getValue() which is always giving me null although I have data in the DataBase and later I am getting the value in the onChanged() method.
This is normal behavior, because queries that return LiveData, are working asynchronously. The value is null at that moment.
So calling this method
LiveData<List<Section>> getAllSections();
you will get the result later here
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
}
});
from documentation:
Room does not allow accessing the database on the main thread unless you called allowMainThreadQueries() on the builder because it might potentially lock the UI for long periods of time. Asynchronous queries (queries that return LiveData or RxJava Flowable) are exempt from this rule since they asynchronously run the query on a background thread when needed.
I solve this problem through this approach
private MediatorLiveData<List<Section>> mSectionLive = new MediatorLiveData<>();
.
.
.
#Override
public LiveData<List<Section>> getAllSections() {
final LiveData<List<Section>> sections = mDb.sectionDAO().getAllSections();
mSectionLive.addSource(sections, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sectionList) {
if(sectionList == null || sectionList.isEmpty()) {
// Fetch data from API
}else{
mSectionLive.removeSource(sections);
mSectionLive.setValue(sectionList);
}
}
});
return mSectionLive;
}
LiveData is an asynchronous query, you get the LiveData object but it might contain no data. You could use an extra method to wait for the data to be filled and then extract the data.
public static <T> T getValue(LiveData<T> liveData) throws InterruptedException {
final Object[] objects = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer observer = new Observer() {
#Override
public void onChanged(#Nullable Object o) {
objects[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
liveData.observeForever(observer);
latch.await(2, TimeUnit.SECONDS);
return (T) objects[0];
}
I resolved the similar issue as follows
Inside your ViewModel class
private LiveData<List<Section>> mSections;
#Override
public LiveData<List<Section>> getAllSections() {
if (mSections == null) {
mSections = mDb.sectionDAO().getAllSections();
}
return mSections;
}
This is all required. Never change the LiveData's instance.
I would suggest creating another query without LiveData if you need to synchronously fetch data from the database in your code.
DAO:
#Query("SELECT COUNT(*) FROM section")
int countAllSections();
ViewModel:
Integer countAllSections() {
return new CountAllSectionsTask().execute().get();
}
private static class CountAllSectionsTask extends AsyncTask<Void, Void, Integer> {
#Override
protected Integer doInBackground(Void... notes) {
return mDb.sectionDAO().countAllSections();
}
}
if sections.getValue() is null I have to call api for data and insert
in into the database
You can handle this at onChange method:
sections.observe(this, new Observer<List<Section>>() {
#Override
public void onChanged(#Nullable List<Section> sections){
if(sections == null || sections.size() == 0) {
// No data in your database, call your api for data
} else {
// One or more items retrieved, no need to call your api for data.
}
}
});
But you should better put this Database/Table initialization logic to a repository class. Check out Google's sample. See DatabaseCreator class.
For anyone that comes across this. If you are calling LiveData.getValue() and you are consistently getting null. It is possible that you forgot to invoke LiveData.observe(). If you forget to do so getValue() will always return null specially with List<> datatypes.
Related
In my app, I have a ViewModel looks like that:
public class MyExampleViewModel {
private LiveData<MyEntity> myLiveData;
#Inject
MyRepository myRepository;
#Inject
public MyExampleViewModel() {
}
public void init(final Long id) {
if (this.myLiveData == null) {
this.myLiveData = myRepository.getById(id);
}
}
public void toggleStar() NullPointerException {
final MyEntity myValue = this.myLiveData.getValue();
myValue.setStar(!myValue.getStar());
myRepository.save(myValue);
}
}
Also the code of MyRepository#getById (myDao is a room DAO and it is injected):
public LiveData<MyEntity> getById(final Long id) {
return myDao.getById(id);
}
The code of MyDao#getById:
#Query(
"SELECT * FROM myTable WHERE id=:id"
)
LiveData<MyEntity> getById(final Long id);
I also try to test this ViewModel using
myExampleViewModel.init(myId);
myExampleViewModel.toggleStar();
but after the init call my LiveData value is always null.
My first question is: is it a best practice to use getValue() on my LiveData or should I use Transformation.map?
My second question is: in my test, how can I have a LiveData populated? I tried to use CountingTaskExecutorRule and InstantTaskExecutorRule but without any success.
Thank you for your help!
I understood why myLiveData is not populated in my test. According to the documentation "LiveData objects that are lazily calculated on demand." and LiveData#getValue only get the value if the LiveData is already populated but doesn't calculate the value.
So I fixed my test adding a getter on my LiveData and an observer on my LiveData to force the calculation like that LiveDataUtil.getValue(myExampleViewModel.getMyLiveData()); with LiveDataUtil#getValue:
public class LiveDataUtil {
public static <T> T getValue(final LiveData<T> liveData) throws InterruptedException {
final Object[] data = new Object[1];
final CountDownLatch latch = new CountDownLatch(1);
Observer<T> observer = new Observer<T>() {
#Override
public void onChanged(#Nullable T o) {
data[0] = o;
latch.countDown();
liveData.removeObserver(this);
}
};
new Handler(Looper.getMainLooper()).post(() -> liveData.observeForever(observer));
latch.await(2, TimeUnit.SECONDS);
//noinspection unchecked
return (T) data[0];
}
}
After this fix, MyExampleViewModel class looks like:
public class MyExampleViewModel {
private LiveData<MyEntity> myLiveData;
#Inject
MyRepository myRepository;
#Inject
public MyExampleViewModel() {
}
public void init(final Long id) {
if (this.myLiveData == null) {
this.myLiveData = myRepository.getById(id);
}
}
public void toggleStar() NullPointerException {
final MyEntity myValue = this.myLiveData.getValue();
myValue.setStar(!myValue.getStar());
myRepository.save(myValue);
}
public LiveData<MyEntity> getMyLiveData() {
return myLiveData;
}
}
And my test method:
myExampleViewModel.init(myId);
LiveDataUtil.getValue(myExampleViewModel.getMyLiveData());
myExampleViewModel.toggleStar();
I fixed my test but I still don't know if using LiveData.getValue is a best practice and I found few documentation on this topic. So, I'm interested in this topic if you have more information.
I am running the following valid Room SQL query in order to find the next available ID for the auto incremented payee column, and want to capture the result from it as an Integer. The below code does just that, but it only works when stepping through with breakpoints, and NOT when normally executing:
#Query("SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'payee'")
int getNextAutoIncrementPayeeID();
The following repository method calls the above Dao method:
public void getNextAutoIncrementPayeeID(OnValueListener listener) {
executor.execute(new Runnable() {
#Override
public void run() {
// Call the listener callback
listener.onValue(db.payeeDao().getNextAutoIncrementPayeeID());
}
});
}
The interface used:
public interface OnValueListener {
public void onValue(int value);
}
Now the result of value is assigned to a global variable payeeId but, that only works when running the code with breakpoints:
repository.getNextAutoIncrementPayeeID(new OnValueListener() {
#Override
public void onValue(int value) {
// use "value" which is returned from Room
payeeId = value;
}
});
final int[] result = {0};
int test = result[0];
return test;
This is called in main thread, so it returns 0 before the background thread run by db.payeeDao().getNextAutoIncrementPayeeID() is up.
So, you need to wait until the query db.payeeDao().getNextAutoIncrementPayeeID(); finished and returned a result, but how much you can wait?
Well, in this case you can use a listener and trigger its callback whenever a value returned from Room database.
Create an interface that has a callback that accepts an int
interface OnValueListener {
public void onValue(int value);
}
Pass an instance of the interface to the Repository query
public void getNextAutoIncrementPayeeID(OnValueListener listener) {
executor.execute(new Runnable() {
#Override
public void run() {
// Call the listener callback
listener.onValue(db.payeeDao().getNextAutoIncrementPayeeID());
}
});
}
And implement the OnValueListener instance in activity/fragment of the call:
getNextAutoIncrementPayeeID(new OnValueListener() {
#Override
public void onValue(int value) {
// use "value" which is returned from Room
}
});
I'm using an executor for background operations. I have a method that takes data from a Room Database and returns a string, so that I can send it from the repository into a viewmodel into activity.
How can I return a string in the method while there is a runnable in it? Please see the following code for a better description:
public String singleLoad(final int id){
DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.execute(new Runnable() {
#Override
public void run() {
favNewsDao.loadSingle(id);
}
});
return favNewsDao.loadSingle(id);
}
The return gives an exception, saying that it cannot access the database on the Main Thread. How can I get a string from this method, like I have in this ViewModel class
public String singleLoad(int id) {
return repository.singleLoad(id);
}
Instead of using an Executor you can use an ExecutorService and submit a Callable. More information here: https://developer.android.com/reference/java/util/concurrent/ExecutorService.html#submit(java.util.concurrent.Callable%3CT%3E)
First Approach
In Repository class, use CountDownLatch with a count value of 1, and with until it reaches 0 to return back the correct result this can be achieved by using await() of this CountDownLatch which allows to urge executing the underlying code until the latch count value reaches 0.
CountDownLatch mLatch;
String singleLoad;
public String singleLoad(final int id){
mLatch = new CountDownLatch(1); // latch count is 1
DefaultExecutorSupplier.getInstance().forBackgroundTasks()
.execute(new Runnable() {
#Override
public void run() {
singleLoad = favNewsDao.loadSingle(id);
mLatch.countDown(); // Now you can allow returning back the result (id)
}
});
// Don't return now and wait until the Executer is done
try {
// Application's main thread awaits, till the
// CountDownLatch count value reaches 0
mLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return mId;
}
UPDATE
As first approach might have memory leak, here is a second approach by using a listener instead of CountDownLatch.
Second Approach
Here I am triggering a listener whenever the needed String is returned back from Room database to the Repository class; the listener is registered in the activity, cascaded to ViewModel, and then to the Repository.
The listener has a callback that retrieves the returned String from database which is eventually returned back to the activity.
Dao interface
#Dao
public interface MyDao {
...
#Query("SELECT text FROM notes WHERE id = :id") // change types according to your database
String loadSingle(int id);
}
Repository
public class AppRepository {
// ... non-relevent code is omitted
private static Executor executor = Executors.newSingleThreadExecutor();
public interface OnTextListener {
void onTextReceived(String text);
}
public void getNoteText(int id, OnTextListener listener) {
executor.execute(() -> {
String text = mDb.noteDao().loadSingle(id); // mDb is the database instance in repository
// trigger the listener in the ViewModel
listener.onTextReceived(text);
});
}
}
ViewModel
public class MainViewModel extends AndroidViewModel {
// ... non-relevent code is omitted
public void getNoteText(int id, AppRepository.OnTextListener listener) {
mRepository.getNoteText(id, listener);
}
}
Activity
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ... non-relevent code is omitted
// Retrieve single data from Room database
int id = 39;
mViewModel.getNoteText(id, new AppRepository.OnTextListener() {
#Override
public void onTextReceived(String text) {
// The retrieved text from Room database
Log.d(TAG, "onTextReceived: " + text);
new Handler(Looper.getMainLooper()) {
}.post(new Runnable() {
#Override
public void run() {
Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
}
});
}
});
}
}
In the main activity, I have LiveData which contains members and a click listener. If I click on a member, then his ID is passed with intent.putExtra. That ID is later passed on to the method open in this activity. With this activity, I want to see the details of a member. In my MemberInfo activity, I marked a line where my problem lies.
It shows me this error: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
My DAO consists this code:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
Member getMemberInfo(long id);
This is my main activity:
public class MemberMainActivity extends AppCompatActivity implements MemberListAdapter.MemberClickListener{
private MemberViewModel mMemberViewModel;
private List<Member> mMember;
void setMember(List<Member> members) {
mMember = members;
}
public static final int NEW_MEMBER_ACTIVITY_REQUEST_CODE = 1;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_member);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Intent intent = new Intent(MemberMainActivity.this, NewMemberActivity.class);
startActivityForResult(intent, NEW_MEMBER_ACTIVITY_REQUEST_CODE);
}
});
RecyclerView recyclerView = findViewById(R.id.recyclerviewcard_member);
final MemberListAdapter adapter = new MemberListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
mMemberViewModel = ViewModelProviders.of(this).get(MemberViewModel.class);
mMemberViewModel.getAllMember().observe(this, new Observer<List<Member>>() {
#Override
public void onChanged(#Nullable final List<Member> members) {
mMember = members;
// Update the cached copy of the words in the adapter.
adapter.setMember(members);
}
});
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == NEW_MEMBER_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
Member member = new Member(data.getStringExtra(NewMemberActivity.EXTRA_REPLY), data.getStringExtra(NewMemberActivity.EXTRA_REPLY2));
mMemberViewModel.insert(member);
} else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
public void onMemberClick(int position) {
Member member = mMember.get(position);
Intent intent = new Intent(getApplicationContext(),MemberInfo.class);
intent.putExtra("MemberID", member.getId());
MemberInfo.open(this, member.getId());
}
}
This is my activity:
public class MemberInfo extends AppCompatActivity {
public static void open(Activity activity, long memberid) {
Intent intent = new Intent(activity, MemberInfo.class);
intent.putExtra("MemberID", memberid);
activity.startActivity(intent);
}
private List<Member> mMember;
private MemberViewModel mMemberViewModel;
void setMember(List<Member> members){
mMember = members;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memberinfo);
Log.i("okay", "memberinfo");
Intent intent = getIntent();
if (intent != null && intent.hasExtra("MemberID")) {
long memberid = intent.getLongExtra("MemberID", -1);
// TODO: get customer details based on customer id
TextView firstname = findViewById(R.id.layout_memberfirstname);
TextView surname = findViewById(R.id.layout_membersurname);
TextView balance = findViewById(R.id.layout_memberbalance);
-------------Member member = MemberRoomDatabase.getDatabase().memberDao().getMemberInfo(memberid);-------------
firstname.setText(member.getFirstname());
surname.setText(member.getSurname());
}
else {
Toast.makeText(
getApplicationContext(),
R.string.empty_not_saved,
Toast.LENGTH_LONG).show();
}
}
}
I thought that maybe it is because I'm missing a AsyncTask method. I tried this, but this also didn't work:
private static class insertMemberInfoAsyncTask extends AsyncTask<Member, Void, Void> {
private MemberDao mAsyncTaskDao;
insertMemberInfoAsyncTask(MemberDao dao) {
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground(Member... params) {
Member member = params[0];
mAsyncTaskDao.getMemberInfo(member.getId());
return null;
}
}
public Member getMemberInfo(long id) {
mAllMember = mMemberDao.getAllMember();
Member member = mMemberDao.getMemberInfo(id);
new insertMemberInfoAsyncTask(mMemberDao).execute(member);
return member;
}
I think I use the method wrong. Can anybody help me?
One option is to update your query to this:
#Query("SELECT * FROM member_table WHERE MemberID=:id")
LiveData<Member> getMemberInfo(long id);
(or similar, using Flowable). This avoids the need to manually create your own AsyncTask.
Returning the LiveData wrapper around the Member type automatically signals to Room that the query can/should be performed asynchronously. Per https://developer.android.com/training/data-storage/room/accessing-data (my emphasis):
Note: Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries—queries that return instances of LiveData or Flowable—are exempt from this rule because they asynchronously run the query on a background thread when needed.
You can use Future and Callable. So you would not be required to write a long asynctask and can perform your queries without adding allowMainThreadQueries() or using LiveData.
My dao query:-
#Query("SELECT * from user_data_table where SNO = 1")
UserData getDefaultData();
My repository method:-
public UserData getDefaultData() throws ExecutionException, InterruptedException {
Callable<UserData> callable = new Callable<UserData>() {
#Override
public UserData call() throws Exception {
return userDao.getDefaultData();
}
};
Future<UserData> future = Executors.newSingleThreadExecutor().submit(callable);
return future.get();
}
In my case, it works if you add Dispatcher.IO when you use coroutines:
viewModelScope.launch(Dispatchers.IO) {
//your database call
}
For me allowMainThreadQueries() works.
This allows room to support database access on the main thread.
See the following code
#Database(entities = [Word::class ],version = 1)
abstract class VocabularyDatabase:RoomDatabase() {
companion object {
private lateinit var INSTANCE:VocabularyDatabase
fun getInstance(context:Context):VocabularyDatabase= Room.databaseBuilder(
context,
VocabularyDatabase::class.java,
"vocabulary"
)
.createFromAsset("vocabulary.db")
.allowMainThreadQueries()
.build()
}
abstract fun dao():WordDao
}
Using Future and Callables can be an alternative here. By using Future and Callable you can get rid of AsyncTask and forcing your queries to the main thread.
The syntax would be as follow -
#Throws(ExecutionException::class, InterruptedException::class)
private fun canContinue(id: String): UserData{
val callable = Callable { userDao.getDefaultData() }
val future = Executors.newSingleThreadExecutor().submit(callable)
return future!!.get()
}
And, don't forget the null check for the data returned. Because it might be null
I have a Navigation Drawer which has an adapter to populate a ListView with some data. I'm using Retrofit to make an API call to get the data, and utilizing the callback methodology. However, I'm getting an error within the adapter when the getCount method is called because the callback isn't done yet. Obviously there's an architecture issue I'm running into here, so I'd love to hear any ideas to improve this.
private class NavigationDrawerAdapter extends BaseAdapter {
private Context _ctx;
private List<Foo> _foo;
public NavigationDrawerAdapter(Context ctx) {
_ctx = ctx;
setupFoo();
}
private void setupFoo() {
SharedPrefsHelper prefs = new SharedPrefsHelper(_ctx);
String userID = prefs.getItem("userID", "");
ApiManager.getFooService(_ctx).getFoo(userID, fooCallback);
}
Callback fooCallback = new Callback<List<Foo>>() {
#Override
public void success(List<Foo> foo, Response response) {
_foo = foo;
}
#Override
public void failure(RetrofitError e) {
Log.e(Constants.TAG, "Could not get foo: " + e.getMessage());
}
};
#Override
public int getCount() {
// This will throw NullPointerException because _foo isn't populated yet
return _foo.size();
}
}
This usually works for me:
#Override
public int getItemCount() {
return _foo == null ? 0 : _foo.size();
}
returns 0 if the list of elements is null, otherwise it returns the size.
This is because we know that when _foo is empty, the size is 0 because there is nothing in your list if it is null.
also:
#Override
public void success(List<Foo> foo, Response response) {
_foo = foo;
notifyDataSetChanged();
}
Any time you make changes to the backing data of an adapter, you must call notifyDataSetChanged on the adapter.
In your setupFoo(), you should initialize _foo like _foo = new ArrayList<Foo>().
Then, when the callback returns data, after replacing "old" foo data with new data in the success method, call notifyDataSetChanged() to let BaseAdapter know there's new day available.