I have implemented class which extends ItemKeyedDataSource and provides paging data from room database's data access object (DAO). My DAO's query methods pass lists of data objects (not wrapped by LiveData) to DataSource callbacks.
What is the recommended way to invalidate DataSource after changes occur in it's wrapped database table, for example if changes come from background Service? How automatic data invalidation is implemented in DataSource.Factory<Integer, T> return parameter that DAOs can generate?
Automatic DataSource invalidation can be implemented by hooking InvalidationTracker.Observer to InvalidationTracker.
You can get InvalidationTracker instance from getInvalidationTracker().
I implemented my InvalidationTracker.Observer like this:
public class DataSourceTableObserver extends InvalidationTracker.Observer {
private DataSource dataSource;
public DataSourceTableObserver(#NonNull String tableName) {
super(tableName);
}
#Override
public void onInvalidated(#NonNull Set<String> tables) {
if (dataSource != null) dataSource.invalidate();
}
public void setCurrentDataSource(DataSource source) {
dataSource = source;
}
}
And I'm using it in my inner DataSource.Factory class like this:
public static class Factory implements DataSource.Factory<TvProgram, TvProgram> {
private Context appContext;
private DataSourceTableObserver observer;
private InvalidationTracker tracker;
private int channelId;
public Factory(Context context, int channelId) {
appContext = context.getApplicationContext();
observer = new DataSourceTableObserver(AppDatabase.PROGRAMS_TABLE);
tracker = AppDatabase.getInstance(appContext).getInvalidationTracker();
tracker.addObserver(observer);
this.channelId = channelId;
}
#Override
public DataSource<TvProgram, TvProgram> create() {
EpgDataSource epgDataSource = new EpgDataSource(appContext, channelId);
observer.setCurrentDataSource(epgDataSource);
return epgDataSource;
}
public void cleanUp() {
tracker.removeObserver(observer);
observer = null;
}
}
When DataSourceTableObserver invalidates DataSource, it's Factory inner class creates new DataSource instance with newest data.
Related
The structure of my application is as follows:
MainActivity(Activity) containing Bottom Navigation View with three fragments nested below
HomeFragment(Fragment) containing TabLayout with ViewPager with following two tabs
Journal(Fragment)
Bookmarks(Fragment)
Fragment B(Fragment)
Fragment C(Fragment)
I am using Room to maintain all the records of journals. I'm observing one LiveData object each in Journal and Bookmarks fragment. These LiveData objects are returned by my JournalViewModel class.
JournalDatabase.java
public abstract class JournalDatabase extends RoomDatabase {
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService dbWriteExecutor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
private static JournalDatabase INSTANCE;
static synchronized JournalDatabase getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), JournalDatabase.class, "main_database")
.fallbackToDestructiveMigration()
.build();
}
return INSTANCE;
}
public abstract JournalDao journalDao();
}
JournalRepository.java
public class JournalRepository {
private JournalDao journalDao;
private LiveData<List<Journal>> allJournals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalRepository(Application application) {
JournalDatabase journalDatabase = JournalDatabase.getInstance(application);
journalDao = journalDatabase.journalDao();
allJournals = journalDao.getJournalsByDate();
bookmarkedJournals = journalDao.getBookmarkedJournals();
}
public void insert(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.insert(journal);
});
}
public void update(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.update(journal);
});
}
public void delete(Journal journal) {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.delete(journal);
});
}
public void deleteAll() {
JournalDatabase.dbWriteExecutor.execute(() -> {
journalDao.deleteAll();
});
}
public LiveData<List<Journal>> getAllJournals() {
return allJournals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
JournalViewModel.java
public class JournalViewModel extends AndroidViewModel {
private JournalRepository repository;
private LiveData<List<Journal>> journals;
private LiveData<List<Journal>> bookmarkedJournals;
public JournalViewModel(#NonNull Application application) {
super(application);
repository = new JournalRepository(application);
journals = repository.getAllJournals();
bookmarkedJournals = repository.getBookmarkedJournals();
}
public void insert(Journal journal) {
repository.insert(journal);
}
public void update(Journal journal) {
repository.update(journal);
}
public void delete(Journal journal) {
repository.delete(journal);
}
public void deleteAll() {
repository.deleteAll();
}
public LiveData<List<Journal>> getAllJournals() {
return journals;
}
public LiveData<List<Journal>> getBookmarkedJournals() {
return bookmarkedJournals;
}
}
I'm instantiating this ViewModel inside onActivityCreated() method of both Fragments.
JournalFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
journalAdapter.submitList(list);
}
});
}
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getBookmarkedJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
adapter.submitList(list);
}
});
}
However, the problem when I use this approach is as I delete make some changes in any of the Fragment like delete or update some Journal some other Journal's date field changes randomly.
I was able to solve this issue by using single LiveData object and observe it in both fragments. The changes I had to make in BookmarkFragment is as follows:
BookmarksFragment.java
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
JournalFactory factory = new JournalFactory(requireActivity().getApplication());
journalViewModel = new ViewModelProvider(requireActivity(), factory).get(JournalViewModel.class);
journalViewModel.getAllJournals().observe(getViewLifecycleOwner(), new Observer<List<Journal>>() {
#Override
public void onChanged(List<Journal> list) {
List<Journal> bookmarkedJournals = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).getBookmark() == 1)
bookmarkedJournals.add(list.get(i));
}
adapter.submitList(bookmarkedJournals);
}
});
}
It works properly now.
However, I want to know why it didn't work using my first approach which was to use two different LiveData objects and observe them in different fragments.
Are multiple LiveData objects not meant to be used in single ViewModel?
OR
Are two instances of same ViewModel not allowed to exist together while making changes and fetching different LiveData objects from the same table simultaneously?
I found out the reason causing this problem.
As I was using LiveData with getViewLifecycleOwner() as the LifecycleOwner, the observer I passed as parameter was never getting removed. So, after switching to a different tab, there were two active observers observing different LiveData objects of same ViewModel.
The way this issue can be solved is by storing the LiveData object in a variable then removing the observer as you switch to different fragment.
In my scenario, I solved this issue by doing the following:
//store LiveData object in a variable
LiveData<List<Journal>> currentLiveData = journalViewModel.getAllJournals();
//observe this livedata object
currentLiveData.observer(observer);
Then remove this observer in a suitable Lifecycle method or anywhere that suits your needs like
#Override
public void onDestroyView() {
super.onDestroyView();
//if you want to remove all observers
currentLiveData.removeObservers(getViewLifecycleOwner());
//if you want to remove particular observers
currentLiveData.removeObserver(observer);
}
I am migrating an app from a LoaderManager with Callbacks to an implementation using ViewModel and LiveData. I would like to keep using the existing SQLiteDatabase.
The main implementation works OK. The Activity instantiates the ViewModel and creates an Observer which updates the View if it observes changes in the MutableLiveData that lives in the ViewModel. The ViewModel gets it data (cursor) from the SQLiteDatabase through a query using a ContentProvider.
But I have other activities that can make changes to the database, while MainActivity is stopped but not destroyed. There is also a background service that can make changes to the database while the MainActivity is on the foreground.
Other activities and the background service can change values in the database and therefore can have an effect to the MutableLiveData in the ViewModel.
My question is: How to observe changes in the SQLiteDatabase in order to update LiveData?
This is a simplified version of MainActivity:
public class MainActivity extends AppCompatActivity {
private DrawerAdapter mDrawerAdapter;
HomeActivityViewModel homeActivityViewModel;
private Observer<Cursor> leftDrawerLiveDataObserver = new Observer<Cursor>() {
#Override
public void onChanged(#Nullable Cursor cursor) {
if (cursor != null && cursor.moveToFirst()) { // Do we have a non-empty cursor?
mDrawerAdapter.setCursor(cursor);
}
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
homeActivityViewModel = ViewModelProviders.of(this).get(HomeActivityViewModel.class);
homeActivityViewModel.getLiveData().observe(this, leftDrawerLiveDataObserver);
homeActivityViewModel.updateLiveData(); //,LEFT_DRAWER_LIVEDATA_ID);
}
#Override
protected void onResume(){ // update the LiveData on Resume
super.onResume();
homeActivityViewModel.updateLiveData();
}
}
This is my ViewModel:
public class HomeActivityViewModel extends AndroidViewModel {
public HomeActivityViewModel(Application application) {
super(application);
}
#NonNull
private final MutableLiveData<Integer> updateCookie = new MutableLiveData<>();
#NonNull
private final LiveData<Cursor> cursorLeftDrawer =
Transformations.switchMap(updateCookie,
new Function<Integer, LiveData<Cursor>>() {
private QueryHandler mQueryHandler;
#Override
public LiveData<Cursor> apply(Integer input) {
mQueryHandler = new QueryHandler(getApplication().getContentResolver());
MutableLiveData<Cursor> cursorMutableLiveData = new MutableLiveData<>();
mQueryHandler.startQuery(ID, cursorMutableLiveData, URI,
new String[]{FeedData.ID, FeedData.URL},
null,null,null
);
return cursorMutableLiveData;
}
}
);
// By changing the value of the updateCookie, LiveData gets refreshed through the Observer.
void updateLiveData() {
Integer x = updateCookie.getValue();
int y = (x != null) ? Math.abs(x -1) : 1 ;
updateCookie.setValue(y);
}
#NonNull
LiveData<Cursor> getLiveData() {
return cursorLeftDrawer;
}
/**
* Inner class to perform a query on a background thread.
* When the query is completed, the result is handled in onQueryComplete
*/
private static class QueryHandler extends AsyncQueryHandler {
QueryHandler(ContentResolver cr) {
super(cr);
}
#Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
MutableLiveData<Cursor> cursorMutableLiveData = (MutableLiveData<Cursor>) cookie;
cursorMutableLiveData.setValue(cursor);
}
}
}
Maybe you should take a look Room. A Room database uses SQLite in the background and will automatically notify your LiveData objects when any changes have been made in the database. Thus you never need to worry about queries and cursors and so on.
Take a look at this tutorial!
I am trying to follow Android architecture guidelines to make this app. I have a MovieRepository which is responsible for fetching JSON (data layer), and I have a ViewModel that supplies data to the UI in my MainActivity. I am using retrofit 2 for my networking task.
MovieRespository code:
public class MovieRepository {
private static final String TAG = MovieRepository.class.getSimpleName();
public LiveData<ReturnMovie> search(String term) {
final MutableLiveData<ReturnMovie> data = new MutableLiveData<>();
MovieService service = ServiceGenerator.createService(MovieService.class);
Call<ReturnMovie> call = service.requestMovie(term, MovieAPIUtils.KEY);
call.enqueue(new Callback<ReturnMovie>() {
#Override
public void onResponse(Call<ReturnMovie> call, Response<ReturnMovie> response) {
ReturnMovie movie = response.body();
data.setValue(movie);
Log.d(TAG, data.getValue().getPage().toString());
}
#Override
public void onFailure(Call<ReturnMovie> call, Throwable t) {
Log.d(TAG, t.toString());
}
});
return data;
}
}
MovieService:
public interface MovieService {
#GET(MovieAPIUtils.Path.MOVIE_PATH + "/{param}")
Call<ReturnMovie> requestMovie (#Path("param") String endpoints,
#Query(MovieAPIUtils.Query.API_QUERY) String key);
}
ViewModel:
public class MainActivityMovieViewModel extends ViewModel {
private static final String TAG = MainActivityMovieViewModel.class.getSimpleName();
private LiveData<ReturnMovie> movie;
private MovieRepository repo = new MovieRepository();
public LiveData<ReturnMovie> getMovie(String searchTerm) {
if (movie == null) {
movie = repo.search(searchTerm);
}
return movie;
}
}
MainActivity:
public class MainActivity extends AppCompatActivity implements MovieAdapter.MovieOnClickListener {
private RecyclerView mRecyclerView;
private MovieAdapter mMovieAdapter;
private MainActivityMovieViewModel viewModel;
private static final String TAG = MainActivity.class.getSimpleName();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = ViewModelProviders.of(this).get(MainActivityMovieViewModel.class);
initRecyclerViewWithMovies();
ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue();
// other code....
}
In the onResponse(new Callback<ReturnMovie>) I was able to retrieve the movie object and I proved it by logging one of its property values, so there IS a valid ReturnMovie object. However, in my MainActivity, the method ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue(); gives me a null. I checked everywhere is just cannot see where the problem is.
The rest of the code is on my Github:
https://github.com/brendoncheung/PopularMovie/tree/mvvm_approach
You use android architecture components, which uses observable pattern in live data. If you are just getting the current value of livedata, you are not sure it has been processed yet.
Instead of
ReturnMovie movie = viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).getValue(); i think you should use
viewModel.getMovie(MovieAPIUtils.Endpoints.POPULAR_ENDPOINT).observe(this, new Observer<ReturnMovie>() {
#Override
public void onChanged(#Nullable ReturnMovie movie) {
//do stuff with the movie
doSomething(movie);
}
});
You don't have to manage unsubscription since architecture component manages unsubscription for activities itself
Take a look on : https://developer.android.com/topic/libraries/architecture/livedata
So according to android developers: "Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI. ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance."
In the code below there is an asynchronous class that gets called in deleteItem function. My question is this: Does ViewModel also handles the asynchronous calls made inside it or will cause memory leaks?
Thank you
public class BorrowedListViewModel extends AndroidViewModel {
private final LiveData<List<BorrowModel>> itemAndPersonList;
private AppDatabase appDatabase;
public BorrowedListViewModel(Application application) {
super(application);
appDatabase = AppDatabase.getDatabase(this.getApplication());
itemAndPersonList = appDatabase.itemAndPersonModel().getAllBorrowedItems();
}
public LiveData<List<BorrowModel>> getItemAndPersonList() {
return itemAndPersonList;
}
public void deleteItem(BorrowModel borrowModel) {
new deleteAsyncTask(appDatabase).execute(borrowModel);
}
private static class deleteAsyncTask extends AsyncTask<BorrowModel, Void, Void> {
private AppDatabase db;
deleteAsyncTask(AppDatabase appDatabase) {
db = appDatabase;
}
#Override
protected Void doInBackground(final BorrowModel... params) {
db.itemAndPersonModel().deleteBorrow(params[0]);
return null;
}
}
}
I would provide an example, probably you need to modify the code.
First you need a live data change and subscribe to that in your view. Then in the controller you post the value telling the subscriber that something appends. This way asynchronously the view would get alerted.
private MutableLiveData<String> databaseLiveData = new MutableLiveData<>();
...
And in the deleteAsyncTask class you can add:
protected void onPostExecute(Void result) {
databaseLiveData.postValue("some data deleted");
}
And in the BorrowedListViewModel class this method to access from the view add this method:
public LiveData<String> getChanger() {
return databaseLiveData;
}
In the view e.g.Activity add this:
private BorrowedListViewModel mBorrowedListViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
BorrowedListViewModel = ViewModelProviders.of(this).get(BorrowedListViewModel.class);
subscribe();
}
private void subscribe() {
final Observer<String> liveDataChange = new Observer<String>() {
#Override
public void onChanged(#Nullable final String message) {
Log.d("Activity", message);
}
};
liveDataChange.getChanger().observe(this, liveDataChange);
}
Hope this help.
public class MyApp extends MultiDexApplication {
private static MyApp instance;
private static class LazyHolder {
private static final Realm INSTANCE = Realm.getDefaultInstance();
}
public static Realm getRealm() {
return LazyHolder.INSTANCE;
}
public static Context getContext() {
return instance;
}
#Override
public void onCreate() {
instance = this;
super.onCreate();
Realm.init(this);
}
}
this is my application activity.
one recyclerView Adapter is using two activities.
first, normal Article List Activity.
second, find Article List Activity.
when I select an Article with the same Url for any Activity, I want to save the Url for that Article.
And this is how I determine if it's stored in Realm in onBindViewHolder().
RealmResults<Article> results = MyApp.getRealm().where(Article.class).equalTo("link", list.get(position).getLink()).findAll();
boolean isSeenArticle = results.size() >= 1;
if(hasKeyword(position)) {
String[] temp = list.get(position).getTitle().split(keyword);
setTitleSpannable(holder.title, temp, isSeenArticle);
}
else {
holder.title.setText(list.get(position).getTitle());
holder.title.setTextColor(isSeenArticle ? context.getResources().getColor(R.color.OneDarkSemiSemiGray) : context.getResources().getColor(R.color.textColorPrimary));
}
color of the holder.articler_title text changes on one side only.
However, both activities appear to have different Realm Instances.
How can I use the same Realm Instance?