I have a database in my app. Here's the way how I create it:
App class:
public class TraktTvApp extends Application {
private static Context sAppContext;
public static TraktTvApp instance;
private MovieDatabase database;
#Override
public void onCreate() {
super.onCreate();
sAppContext = getApplicationContext();
instance = this;
database = Room.databaseBuilder(this, MovieDatabase.class, "MovieDatabase").build();
}
#NonNull
public static Context getAppContext() {
return sAppContext;
}
public static TraktTvApp getInstance() {
return instance;
}
public MovieDatabase getDatabase() {
return database;
}
}
DAO class
#Dao
public interface MovieDao {
#Query("SELECT * from MovieEntity")
List<MovieEntity> getFavorites();
#Insert(onConflict = OnConflictStrategy.REPLACE)
Completable insertMovie(final MovieEntity movie);
#Delete
void deleteMovie(MovieEntity movie);
}
Database class
#Database(entities = {MovieEntity.class}, version = 1)
public abstract class MovieDatabase extends RoomDatabase {
public abstract MovieDao movieDao();
}
And here's the way how I call insert method:
mCompositeDisposable.add(Observable.fromCallable(()->movieDao.insertMovie(movieEntity))
.doOnSubscribe(disposable -> mView.showLoadingIndicator(true))
.doOnComplete(() -> {
mView.showEmptyState(false);
mView.onMoviesAdded();
})
.doOnError(throwable -> mView.showEmptyState(true))
.doOnTerminate(() -> mView.showLoadingIndicator(false))
.observeOn(Schedulers.io())
.subscribe());
But when I want to check data in my database in Stetho, there's nothing here:
So, what's the matter and how can I solve this problem? It seems to me that it can be problem in creating database, but I used the same way as usual and usually it works ok
call setupDebugTools() in application's onCreate() like
{
super.onCreate()
setupDebugTools()
}
And
private void setupDebugTools() {
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}
}
for more information
Related
I am working with API and Room to get list of Service Technician.
I want to do the following:
Query to getAll() from the Service Technician database table.
Display results to users instantly.
If it is empty or fails to get database results, make API call to get the same list.
Make an API call to get updated list of Service Technician.
Save them into a database table.
Refresh UI changes based on the latest database content.
Here is what I have, and it works. But I think that it can be improved:
This looks like not needed call, if I could use somehow Flowable .flatMap(ids -> techniciansDbRepository.getServiceTechnicians())
I can not use Flowable since filter(ServiceTechnician::isActive).toList(); never completes and I do not get result in subscribe().
public void getServiceTechnicians() {
disposables = RxUtil.initDisposables(disposables);
Disposable disposable = techniciansDbRepository.getServiceTechnicians()
.subscribeOn(Schedulers.io())
.onErrorResumeNext(throwable -> techniciansApiRepository.getServiceTechnicians())
.flatMap(serviceTechnicians -> techniciansApiRepository.getServiceTechnicians())
.flatMap(techniciansDbRepository::insertAll)
.flatMap(ids -> techniciansDbRepository.getServiceTechnicians())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
serviceTechnicians -> view.onServiceTechniciansLoaded(serviceTechnicians),
view::handleError
);
disposables.add(disposable);
}
public class ServiceTechniciansDbRepository implements ServiceTechniciansRepository {
private final ServiceTechnicianDao dao;
public ServiceTechniciansDbRepository(ServiceTechnicianDao dao) {
this.dao = dao;
}
#Override public Single<List<ServiceTechnician>> getServiceTechnicians() {
return dao.getAll()
.flatMapPublisher(Flowable::fromIterable)
.map(serviceTechnicianAndUser -> ServiceTechnicianMapper.convertToApiModel(
serviceTechnicianAndUser.getServiceTechnician(),
serviceTechnicianAndUser
))
.filter(ServiceTechnician::isActive)
.toList();
}
}
#Dao public abstract class ServiceTechnicianDao implements BaseDao<ServiceTechnicianDb> {
#Transaction
#Query("SELECT * FROM service_technician")
public abstract Single<List<ServiceTechnicianAndUser>> getAll();
#Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void insertUser(UserDb user);
}
public interface BaseDao<E> {
#Insert(onConflict = OnConflictStrategy.REPLACE)
Single<Long> insert(E e);
#Insert(onConflict = OnConflictStrategy.REPLACE)
Single<List<Long>> insert(List<E> list);
#Update Single<Integer> update(E e);
#Delete Single<Integer> delete(E e);
}
public class ServiceTechniciansApiRepository implements ServiceTechniciansRepository {
private final ServiceTechnicianApi api;
public ServiceTechniciansApiRepository(ServiceTechnicianApi api) {
this.api = api;
}
#Override public Single<List<ServiceTechnician>> getServiceTechnicians() {
return api.getServiceTechnicians()
.subscribeOn(Schedulers.io());
}
#Override public Single<List<Long>> insertAll(List<ServiceTechnician> serviceTechnicians) {
return Single.error(new CanNotBeUsedWithApiException());
}
}
Any idea of how I could improve more this code?
I'm trying to run simple test to my project but this exception is holding me back and i cannot find out why.
Im doing a simple test to insert some new customers to my database. My first problem i encountered when inserting customers was that I could NOT insert data in the Main Thread so I used an AsyncTask.execute(new Runnable()) but this time i ran into this problem.
I've provided the following classes that should give a good insight I hope..
TestClass:
#RunWith(RobolectricTestRunner.class)
public class ExampleUnitTest {
ShopDatabase db;
private CustomerDao customerDao;
#Before
public void createDB() {
Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, ShopDatabase.class).build();
customerDao = db.customerDao();
}
#Test
public void createUser(){
final Customer customer = new Customer("Test", "Test test", Date.valueOf("2020-10-10"));
AsyncTask.execute(new Runnable() {
#Override
public void run() {
customerDao.insert(customer);
}
});
Customer customerFound = customerDao.getCustomerByName("Test", "Test test");
assertEquals(customerFound.getFirstName(), customer.getFirstName(), "Could not find user..");
}
#After
public void closeDB() {
db.close();
}
Repository:
public class Repository {
private CustomerDao customerDao;
private LiveData<List<Customer>> allCustomers;
private Customer customer;
public Repository(Application application) {
// Get DB instance
ShopDatabase db = ShopDatabase.getInstance(application);
customerDao = db.customerDao();
allCustomers = customerDao.getAllCustomers();
}
public void insert(Customer customer) {
new InsertCustomerAsyncTask(customerDao).execute(customer);
}
public Customer getCustomerByName(String first, String last) {
return customerDao.getCustomerByName(first, last);
}
public LiveData<List<Customer>> getAllCustomers() {
return allCustomers;
}
// Inner Async class to insert customers
private static class InsertCustomerAsyncTask extends AsyncTask<Customer, Void, Void> {
private CustomerDao customerDao;
public InsertCustomerAsyncTask(CustomerDao customerDao) {
this.customerDao = customerDao;
}
#Override
protected Void doInBackground(Customer... customers) {
customerDao.insert(customers[0]);
return null;
}
}
Database:
#Database(entities = {Customer.class}, version = 1)
#TypeConverters({Converters.class})
public abstract class ShopDatabase extends RoomDatabase {
private static ShopDatabase instance;
public abstract CustomerDao customerDao();
public static synchronized ShopDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
ShopDatabase.class,"shop_database")
.fallbackToDestructiveMigration()
.addCallback(roomCallback)
.build();
}
return instance;
}
private static RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() {
#Override
public void onCreate(#NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
new PopulateDbAsyncTask(instance).execute();
}
};
private static class PopulateDbAsyncTask extends AsyncTask<Void, Void, Void> {
private CustomerDao customerDao;
PopulateDbAsyncTask(ShopDatabase db) {
customerDao = db.customerDao();
}
#Override
protected Void doInBackground(Void... voids) {
// Customers
long c = customerDao.insert(new Customer("One", "A", Date.valueOf("2019-05-10")));
long c2 = customerDao.insert(new Customer("Two", "B", Date.valueOf("2020-07-10")));
long c3 = customerDao.insert(new Customer("Three", "C", Date.valueOf("1860-12-10")));
return null;
}
}
}
Firstly I thought it was because my app was not running so it meant my database was closed but that was not the case. I had my app open while testing.
Secondly - when I check if the database is open after i instantiate my database (db = Room.inMemoryDatabaseBuilder(context, ShopDatabase.class).build();) - db.isOpen() -> it returns false every time.
What can cause the problem?
when I check if the database is open after i instantiate my database (db = Room.inMemoryDatabaseBuilder(context, ShopDatabase.class).build();) - db.isOpen() -> it returns false every time.
The database is not opened until you attempt to access it. You can force accessing it (getting a Writable or Readable database/connection aka opening the database file itself which is all done behind the scenes on your behalf) before you return the instance. e.g.
......
instance.getOpenHelper().getWritableDatabase();
return instance;
Example
using :-
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
shopDatabase = ShopDatabase.getInstance(this);
boolean dbopen = shopDatabase.isOpen();
if (dbopen); //<<<<<<<<<< breakpoint here
}
With :-
public static synchronized ShopDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
ShopDatabase.class,"shop_database")
.fallbackToDestructiveMigration()
.addCallback(roomCallback)
.build();
}
//instance.getOpenHelper().getWritableDatabase();
return instance;
}
results in
changing to use :-
public static synchronized ShopDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
ShopDatabase.class,"shop_database")
.fallbackToDestructiveMigration()
.addCallback(roomCallback)
.build();
}
instance.getOpenHelper().getWritableDatabase(); //<<<<<<<<< ADDED
return instance;
}
results in :-
I could NOT insert data in the Main Thread
You can if you add .allowMainThreadQueries() when building
e.g.
public static synchronized ShopDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(),
ShopDatabase.class,"shop_database")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.addCallback(roomCallback)
.build();
}
instance.getOpenHelper().getWritableDatabase();
return instance;
}
You may wish to not use this though.
The Issue
I implemented the Android Architecture library and with this I am restoring data from Room Database, with MVVM (Model View View-Model) and whenever I use the observer to view LiveDada every time I start the app there is a noticeable delay when the items from the recycler view loads.
Image illustrates on what I mean when the app stats and the items show delay when they load.
What I want it to do
What I want to achieve with LiveData is this and the way I was able to achieve this is in my Dao I used a Query to get all of the data and pass it as a List rather than using LiveData and then in the Repository convert it to MutableLiveData and then pass it to database and from there observe it as LiveData in my fragment but using this approach actually doesn't update on delete or on insert unless if i restart the app.
Is there any way to fix this issue?
I would much like to use LiveData.
This is my DevicesDao interface:
#Dao
public interface DevicesDao {
#Insert
void insert(Devices... devices);
#Query("SELECT * FROM devices")
LiveData<List<Devices>> getDevices();
/*
#Query("SELECT * FROM devices")
List<Devices> getDevices();
*/
#Delete
void delete(Devices... device);
#Update
void update(Devices... device);
}
Database:
#Database(entities = {Devices.class}, version = 1)
public abstract class DevicesDatabase extends RoomDatabase {
private static final String DATABASE_NAME = "devices_registered";
private static DevicesDatabase instance;
public abstract DevicesDao devicesDao();
public static DevicesDatabase getInstance(final Context context) {
if (instance == null) {
synchronized (DevicesDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(
context.getApplicationContext(),
DevicesDatabase.class,
DATABASE_NAME)
.fallbackToDestructiveMigration()
.build();
}
}
}
return instance;
}
}
Repository:
public class DevicesRepository {
private final DevicesDao devicesDao;
public DevicesRepository(Application application) {
DevicesDatabase db = DevicesDatabase.getInstance(application);
devicesDao = db.devicesDao();
}
public void addDevices(Devices devices) {
new InsertDeviceAsync(devicesDao).execute(devices);
}
public void updateDevice(Devices devices) {
new UpdateDeviceAsync(devicesDao).execute(devices);
}
public void deleteDevice(Devices devices) {
new DeleteDeviceAsync(devicesDao).execute(devices);
}
//Gets all data from SQLite
public LiveData<List<Devices>> getAllDevices() {
return devicesDao.getDevices();
}
/*
public LiveData<List<Devices>> getAllDevices() {
MutableLiveData<List<Devices>> devices = new MutableLiveData<>();
try {
devices.setValue(new GetDeviceAsync(devicesDao).execute().get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return devices;
}
*/
}
View Model File:
public class HomeFragmentViewModel extends AndroidViewModel {
private final DevicesRepository devicesRepository;
private LiveData<List<Devices>> devices;
public HomeFragmentViewModel(#NotNull Application application) {
super(application);
devicesRepository = new DevicesRepository(application);
devices = devicesRepository.getAllDevices();
}
public LiveData<List<Devices>> getAllDevices() {
return devices;
}
public void addNewDevice(Devices devices) {
devicesRepository.addDevices(devices);
}
public void deleteDevice(Devices devices) {
devicesRepository.deleteDevice(devices);
}
public void editDevice(Devices devices) {
devicesRepository.updateDevice(devices);
}
}
And lasty, the observer in my fragment:
///////Other code
//Implements ViewModel to HomeFragment
homeFragmentViewModel = ViewModelProviders.of(this).get(HomeFragmentViewModel.class);
homeFragmentViewModel.getAllDevices().observe(getViewLifecycleOwner(), devicesList -> {
//Validation tool
validationUtil = new ValidationUtil(devicesList);
//Adds to adapter
adapter.submitList(devicesList);
/////// Other code
});
Thank you for your time!!!
New update: When I reset the app data, it loads fine but as soon as I decide to update the code, the issue comes back regardless of that edit I make in Android Studio.
EventDao
#Dao
public interface EventDao {
#Query("SELECT * FROM events WHERE date_start = :date")
LiveData<List<EventPatientLocation>> test(Date date);
}
EventRepository
public class EventRepository {
private EventDao eventDao;
public EventRepository(Application application) {
MyDatabase db = MyDatabase.getDatabase(application);
eventDao = db.eventDao();
}
public LiveData<List<EventPatientLocation>> test(Date date) {
return eventDao.test(date);
}
EventViewModel
public class EventViewModel extends AndroidViewModel {
private EventRepository repository;
public EventViewModel(#NonNull Application application) {
super(application);
repository = new EventRepository(application);
}
public LiveData<List<EventPatientLocation>> test(Date date) {
return repository.test(date);
}
}
Fragment (onActivityCreated)
eventViewModel = ViewModelProviders.of(activity).get(EventViewModel.class);
LiveData<List<EventPatientLocation>> testEvent = eventViewModel.test(new Date());
testEvent.observe(activity, events -> {
// events is always null!
});
Despite the onChanged callback on fragment is called multiple times, the events list is always null...I know for sure that in database there are candidate rows for the query, so where am I doing wrong? Help please...
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
}
}