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.
Related
I've been doing Android Studio for a month only and I've got to say I'm kind of confused with Room database, I'm sorry if the question sounds confused.
I'm using a Room database as stated, here are my Database-related classes:
#Entity(tableName="board")
public class BoardItem {
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name="board_id")
private int boardId;
#ColumnInfo(name="board_name")
private String boardName;
#ColumnInfo(name="board_description")
private String boardDescription;
#ColumnInfo(name="board_image_list")
#TypeConverters(ImageListTypeConverter.class)
private List<ImageItem> boardImageList;
public BoardItem(String boardName, String boardDescription){
this.boardName = boardName;
this.boardDescription = boardDescription;
this.boardImageList = new ArrayList<>();
}
public int getBoardId() { return boardId; }
public String getBoardName() {
return boardName;
}
public String getBoardDescription() {
return boardDescription;
}
public String getPhotosCount() {
return String.valueOf(this.boardImageList.size());
}
public void setBoardId(int boardId) {
this.boardId = boardId;
}
public List<ImageItem> getBoardImageList() {
return boardImageList;
}
public void setBoardImageList(List<ImageItem> list) { this.boardImageList = list; }
#Dao
public interface BoardItemDAO {
#Insert(onConflict = OnConflictStrategy.IGNORE)
void addBoardItem(BoardItem boardItem);
#Transaction
#Query("SELECT * from board ORDER BY board_id DESC")
LiveData<List<BoardItem>> getBoardItems();
#Transaction
#Query("SELECT * from board ORDER BY board_id DESC")
List<BoardItem> getBoardItemsNow();
}
#Database(entities = {BoardItem.class}, version = 1)
public abstract class BoardItemDatabase extends RoomDatabase {
public abstract BoardItemDAO boardItemDAO();
//Singleton
private static volatile BoardItemDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
static BoardItemDatabase getDatabase(final Context context){
if (INSTANCE == null) {
synchronized (BoardItemDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), BoardItemDatabase.class, "board_database")
.allowMainThreadQueries()
.build();
}
}
}
return INSTANCE;
}
}
//Repository
public class BoardItemRepository {
private BoardItemDAO boardItemDAO;
private LiveData<List<BoardItem>> boardItemList;
private List<BoardItem> boardItemListNow;
public BoardItemRepository (Application application) {
BoardItemDatabase db = BoardItemDatabase.getDatabase(application);
boardItemDAO = db.boardItemDAO();
boardItemList = boardItemDAO.getBoardItems();
boardItemListNow = boardItemDAO.getBoardItemsNow();
}
//Room executes all queries on a separate thread
//Observed LiveData will notify the observer when data has changed
public LiveData<List<BoardItem>> getBoardItemList() { return boardItemList; }
//this method is called on a non-UI thread or the app will throw an exception. Room ensures
//that there are no long running operations on the main thread, blocking the UI.
public void addBoardItem(final BoardItem boardItem) {
BoardItemDatabase.databaseWriteExecutor.execute(() -> boardItemDAO.addBoardItem(boardItem));
}
public List<BoardItem> getBoardItemListNow() {return boardItemListNow; }
}
Note that BoardItem and ImageItem are classes I made myself: a board is supposed to contain multiple ImageItems.
My boardItem has different fields, one of which is a list of ImageItems.
Now, in a specific fragment I try to update this list of ImageItems in a board that already exists in my database, which is the board with id = 0 (the very first board in the db). I try to retrieve the list from the Database and replace it with a new one.
I have used LiveData in certain cases to update the view of my app when the item change, but I have non LiveData methods for this specific piece of my code I need to change my database as soon as I click the button that contains this code:
List<BoardItem> boardItems = boardListViewModel.getBoardItemsNow();
newList = boardItems.get(0).getBoardImageList();
newList.add(newItem);
boardItems.get(0).setBoardImageList(newList);
When I click the button, the code is executed with no errors, but the database isn't updated; it contains the list as it was before, without the new item.
Thanks in advance, again I'm sorry if this sounds confusing!
EDIT:
here's my ViewModel:
public class BoardListViewModel extends AndroidViewModel {
private final MutableLiveData<BoardItem> boardSelected = new MutableLiveData<>();
private LiveData<List<BoardItem>> boardItems;
private List<BoardItem> boardItemsNow;
public BoardListViewModel(#NonNull Application application) {
super(application);
BoardItemRepository repo = new BoardItemRepository(application);
boardItems = repo.getBoardItemList();
boardItemsNow = repo.getBoardItemListNow();
}
public void select(BoardItem boardItem) {
boardSelected.setValue(boardItem);
}
public LiveData<BoardItem> getSelected() {
return boardSelected;
}
public LiveData<List<BoardItem>> getBoardItems() {
return boardItems;
}
public BoardItem getBoardItem(int position) {
return boardItems.getValue() == null ? null : boardItems.getValue().get(position);
}
public List<BoardItem> getBoardItemsNow() { return boardItemsNow; }
}
I believe that your issue is that you are not updating but attempting to add (insert) a new item (row) via :-
#Insert(onConflict = OnConflictStrategy.IGNORE)
void addBoardItem(BoardItem boardItem);
As the boardid already exists and that it is the primary key, which is implicitly unique, a conflict occurs, and this conflict is ignored, thus the new row is not added and the database remains unchanged.
What you should be doing is updating the existing row, so you want an #Update dao.
So add and then use the following dao :-
#Update(onConflict = OnConflictStrategy.IGNORE)
int updateBoardItem(BoardItem boardItem);
the int returned is the number of rows that have been update (you would expect 1, 0 if a conflict resulted in the update being ignored).
I am new to MVVM and trying to clear my rxJava disposables, i have seen some answers saying to clear it in ViewModel in onClear method but how do i get to add the disposable in the first place ?
//Repository Code
public class MyRepository {
public MutableLiveData<String> deleteDraftById(int recordId {
final MutableLiveData<String> result = new MutableLiveData<>();
Completable deleteDraftById = completedDao.deleteDraftById(recordId);
deleteDraftById.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onComplete() {
result.setValue("1");
}
#Override
public void onError(Throwable e) {
result.setValue(e.getMessage());
}
});
return result;
}
}
//ViewModel
public class MyViewModel extends AndroidViewModel {
public MutableLiveData<String> deleteDraftById(int recordId){
return myRepository.deleteDraftById(recordId);
}
}
In my opinion nothing wrong with using live data in repos, for example if single source of truth is needed. Here is what I'd suggested (rxjava 1.x assumed, pseudocode a-la java) :
public class MyRepository {
public final MutableLiveData<String> result = new MutableLiveData<>();
public Completable deleteDraftById(int recordId) {
return completedDao.deleteDraftById(recordId)
.doOnSubscribe(...) //potentially report progress start, if needed
.doOnSuccess(...) //report success to your live data aka result.value = ...
.onErrorComplete(...) //report error to your live data and complete
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
}
public class MyViewModel(....pass MyRepository) extends AndroidViewModel {
//expose live data from repo somehow, may be like this:
public final LiveData<String> abc = myRepository.result;
private final CompositeSubscription compositeSubscription = new CompositeSubscription();
//call this from ui
public void delete(int recordId) {
compositeSubscription.add(
myRepository
.deleteDraftById(recordId)
.subscribe()
)
}
#Override
protected void onCleared() {
super.onCleared();
compositeSubscription.clear();
}
}
What the proper way to create DAO with Room and Retrofit?
I have database module like this:
#Module
public class ApplicationDatabaseModule {
private final String mDatabaseName;
ApplicationDatabase mApplicationDatabase;
public ApplicationDatabaseModule(#ApplicationContext Context context, Class<? extends ApplicationDatabase> roomDataBaseClass, String databaseName) {
mDatabaseName = databaseName;
mApplicationDatabase = Room.databaseBuilder(context, roomDataBaseClass, mDatabaseName).build();
}
#Singleton
#Provides
ApplicationDatabase provideApplicationDatabase() {
return mApplicationDatabase;
}
#Singleton
#Provides
CitiesDao provideCitiesDao() {
return mApplicationDatabase.getCitiesDao();
}
}
POJO class like this:
#Entity
public class City {
#PrimaryKey
#ColumnInfo(name = "id")
private int cityId;
#ColumnInfo(name = "name")
private String cityName;
public int getCityId() {
return cityId;
}
public void setCityId(int cityId) {
this.cityId = cityId;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
#Override
public String toString() {
return "City [cityId = " + cityId + ", cityName = " + cityName + "]";
}
}
DAO interface like this:
#Dao
public interface CitiesDao {
#Insert
void insertCities(City... cities);
#Query("SELECT * FROM City")
City[] queryCities();
}
And API for Retrofit:
public interface CitiesApi {
#GET("/api/cities")
Call<City[]> requestCities();
}
As I know DAO is responsible for accessing data, including data passed through REST-client. But these two parts are represented by interfaces and built into separate classes. What is the proper way to implement DAO?
DAO is responsible for accessing data
yes
, including data passed through REST-client.
God no
What is the proper way to implement DAO?
Room already generates a proper way of implementation for your DAO based on your interface + annotations, I think it's called CitiesDao_Impl.
What the proper way to create DAO with Room and Retrofit?
Room doesn't know about Retrofit and shouldn't need to know about Retrofit. It only cares about local data persistence.
Meaning your DAO needs to look like this:
#Dao
public interface CitiesDao {
#Insert
#Transaction
void insertCities(City... cities);
#Query("SELECT * FROM City")
LiveData<List<City>> queryCities();
}
So what you actually need is a Worker that will fetch new data in background when either cache is invalid (force fetch new data) or when your sync task should run (for example when device is charging and you are on WIFI and you're at 2 AM to 7 AM -- for this you'd need WorkManager).
Immediately fetching new data though is fairly easy, all you need is either an AsyncTask in a singleton context that returns null from doInBackground, or your own Executor that you post your background task to.
public class FetchCityTask extends AsyncTask<Void, Void, Void> {
...
#Override
protected Void doInBackground(Void... params) {
List<City> cities = citiesApi.requestCities().execute().body(); // TODO error handling
citiesDao.insertCities(cities);
return null;
}
}
And then
new FetchCityTask(...).execute();
Now when this task runs, your UI will be updated with latest data by observing the LiveData that you store in a ViewModel.
public class CitiesViewModel
extends ViewModel {
private final CitiesDao citiesDao;
private LiveData<List<City>> liveResults;
public CitiesViewModel(...) {
...
liveResults = citiesDao.queryCities();
}
public LiveData<List<City>> getCities() {
return liveResults;
}
}
And
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_view);
CitiesViewModel viewModel = ViewModelProviders.of(this).get(CitiesViewModel.class, ...);
...
viewModel.getTasks().observe(getViewLifecycle(), list -> {
//noinspection Convert2MethodRef
listAdapter.submitList(list);
});
}
You want to create a repository class to handle your data. Then you simply interact with your repository. Some pseudocode for you:
class Repository {
private CitiesDao localSource;
private CitiesApi remoteSource;
public Repository() {
//initialize objects here
}
City[] getCities() {
if (networkIsAvailable) {
City[] cities = remoteSource.requestCities();
saveCitiesToDatabase(cities);
return cities;
} else {
return localSource.queryCities();
}
}
private void saveCitiesToDatabase(City[] cities) {
//TODO save cities to databse
}
}
I'm using Mockito framework to test a class that returns an Observable (see comments):
This is the my implementation class:
public class DataRepository implements AbstractRepository {
private DataSource dataSource;
private DataMapper dataMapper;
// Constructor
public DataRepository(DataSource dataSource, DataMapper dataMapper) {
this.dataSource = dataSource;
this.dataMapper = dataMapper;
}
/**
* The call to dataSource.getItem(int) returns
* an Observable of type ItemResponse.
* So, in the map I cast it to an object of type Item.
**/
public Observable<Item> getItem(int id) {
return dataSource.getItem(id)
.map(new Function<ItemResponse, Item>() {
#Override
public Item apply(ItemResponse itemResponse) throws Exception {
return dataMapper.transform(itemResponse);
}
});
}
}
Now, this is my test class:
#RunWith(MockitoJUnitRunner.class)
public class DataRepositoryTest {
DataRepository dataRepository;
#Mock
DataSource dataSource;
#Mock
DataMapper dataMapper;
#Before
public void setUp() {
dataRepository = new DataRepository(dataSource, dataMapper);
}
#Test
public void testGetItem() {
// Given
ItemResponse itemResponse = new ItemResponse();
given(dataSource.getItem(anyInt())).willReturn(Observable.just(itemResponse));
// When
dataRepository.getItem(anyInt());
// Verify/then
verify(dataSource).getItem(anyInt()); // This part runs fine.
verify(dataMapper).transform(); // This is failing
}
}
The error message I'm getting is:
Wanted but not invoked:
dataMapper.transform(
com.my.package.ItemResponse#e720b71
);
-> at com.my.package.test.DataRepositoryTest.testGetItem(DataRepositoryTest.java:28)
Actually, there were zero interactions with this mock.
How can I tell Mockito to call the map() operator/method, then apply() of the Observable returned by dataSource.getItem(int)?
Looks like you are not subscribing the Observable<Item> returned by public Observable<Item> getItem(int id) so the .map(...) operator is not being called/executed, try with dataRepository.getItem(anyInt()).subscribe(); just to verify.
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.