Android implement search with view model and live data - android

I'm working on a project in android for a udacity course I'm currently trying to implement a search function while adhering to android architecture components and using firestore and room I'm fairly new to all these concepts so please point out anything that seems wrong.
So I made a database repository to keep my firestore and room databases in sync and to deliver the data. I'm then using viewmodel and the observer pattern (I think) so my observer gets the data and looks for changes gives it to my adapter (refreshMyList(List)) which populates a recyclerview like this :
contactViewModel = ViewModelProviders.of(this).get(ContactsViewModel.class);
contactViewModel.getAllContacts().observe(this, new
Observer<List<DatabaseContacts>>() {
#Override
public void onChanged(#Nullable List<DatabaseContacts>
databaseContacts) {
ArrayList<DatabaseContacts> tempList = new ArrayList<>();
tempList.addAll(databaseContacts);
contactsAdapter.refreshMyList(tempList);
if (tempList.size() < 1) {
results.setVisibility(View.VISIBLE);
} else {
results.setVisibility(View.GONE);
}
}
});
I now want to perform a search of the data, I have my room queries all set up fine and I have methods in my data repository to get contacts based on a search string but I cant seem to refresh my list I've read that there are ways to do it like Transformations.switchMap ? but i cant seem to wrap my head around how it works can anyone help me
Currently I'm trying to return a List of results from an async task, it used to return live data but I changed it as getValue() was always null, not sure if that's correct, heres the async :
private static class searchContactByName extends AsyncTask<String, Void,
ArrayList<DatabaseContacts>> {
private LiveDatabaseContactsDao mDao;
searchContactByName(LiveDatabaseContactsDao dao){
this.mDao = dao;
}
#Override
protected ArrayList<DatabaseContacts> doInBackground(String... params) {
ArrayList<DatabaseContacts> contactsArrayList = new ArrayList<>();
mDao.findByName("%" + params[0] + "%");
return contactsArrayList;
}
}
I call this from my contacts repository in its own sort of wrapper :
public List<DatabaseContacts> getContactByName(String name) throws
ExecutionException, InterruptedException {
//return databaseContactsDao.findByName(name);
return new searchContactByName(databaseContactsDao).execute(name).get();
}
and this is called from my view model like this :
public List<DatabaseContacts> getContactByName(String name) throws
ExecutionException, InterruptedException {
return contactRepository.getContactByName(name);
}
I'm then calling this from my fragment :
private void searchDatabase(String searchString) throws ExecutionException,
InterruptedException {
List<DatabaseContacts> searchedContacts =
contactViewModel.getContactByName("%" + searchString + "%");
ArrayList<DatabaseContacts> contactsArrayList = new ArrayList<>();
if (searchedContacts != null){
contactsArrayList.addAll(searchedContacts);
contactsAdapter.refreshMyList(contactsArrayList);
}
}
and this is called from an on search query text changed method in my onCreateOptionsMenu :
#Override
public boolean onQueryTextChange(String newText) {
try {
searchDatabase(newText);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
but it just does nothing my original recyclerview contents never change any ideas?

you can use Transformation.switchMap to do search operations.
In viewmodel create MutableLiveData which has latest search string.
Inside viewmodel use:
LiveData<Data> data =
LiveDataTransformations.switchMap(searchStringLiveData, string ->
repo.loadData(string)))
Return the above live data to activity so it can observe and update view.

I faced the same issue and I managed to fix it using
switchMap
and
MutableLiveData
We just need to use MutableLiveData to set the current value of editText, and when the user search we call setValue(editText.getText())
public class FavoriteViewModel extends ViewModel {
public LiveData<PagedList<TeamObject>> teamAllList;
public MutableLiveData<String> filterTextAll = new MutableLiveData<>();
public void initAllTeams(TeamDao teamDao) {
this.teamDao = teamDao;
PagedList.Config config = (new PagedList.Config.Builder())
.setPageSize(10)
.build();
teamAllList = Transformations.switchMap(filterTextAll, input -> {
if (input == null || input.equals("") || input.equals("%%")) {
//check if the current value is empty load all data else search
return new LivePagedListBuilder<>(
teamDao.loadAllTeam(), config)
.build();
} else {
System.out.println("CURRENTINPUT: " + input);
return new LivePagedListBuilder<>(
teamDao.loadAllTeamByName(input), config)
.build();
}
});
}
}
in Activity of fragment
viewModel = ViewModelProviders.of(activity).get(FavoriteViewModel.class);
viewModel.initAllTeams(AppDatabase.getInstance(activity).teamDao());
FavoritePageListAdapter adapter = new FavoritePageListAdapter(activity);
viewModel.teamAllList.observe(
activity, pagedList -> {
try {
Log.e("Paging ", "PageAll" + pagedList.size());
try {
//to prevent animation recyclerview when change the list
recycleFavourite.setItemAnimator(null);
((SimpleItemAnimator) Objects.requireNonNull(recycleFavourite.getItemAnimator())).setSupportsChangeAnimations(false);
} catch (Exception e) {
}
adapter.submitList(pagedList);
} catch (Exception e) {
}
});
recycleFavourite.setAdapter(adapter);
//first time set an empty value to get all data
viewModel.filterTextAll.setValue("");
edtSearchFavourite.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
#Override
public void afterTextChanged(Editable editable) {
//just set the current value to search.
viewModel.filterTextAll.setValue("%" + editable.toString() + "%");
}
});
Room Dao
#Dao
public interface TeamDao {
#Query("SELECT * FROM teams order by orders")
DataSource.Factory<Integer, TeamObject> loadAllTeam();
#Query("SELECT * FROM teams where team_name LIKE :name or LOWER(team_name_en) like LOWER(:name) order by orders")
DataSource.Factory<Integer, TeamObject> loadAllTeamByName(String name);
}
PageListAdapter
public class FavoritePageListAdapter extends PagedListAdapter<TeamObject, FavoritePageListAdapter.OrderHolder> {
private static DiffUtil.ItemCallback<TeamObject> DIFF_CALLBACK =
new DiffUtil.ItemCallback<TeamObject>() {
// TeamObject details may have changed if reloaded from the database,
// but ID is fixed.
#Override
public boolean areItemsTheSame(TeamObject oldTeamObject, TeamObject newTeamObject) {
System.out.println("GGGGGGGGGGGOTHERE1: " + (oldTeamObject.getTeam_id() == newTeamObject.getTeam_id()));
return oldTeamObject.getTeam_id() == newTeamObject.getTeam_id();
}
#Override
public boolean areContentsTheSame(TeamObject oldTeamObject,
#NonNull TeamObject newTeamObject) {
System.out.println("GGGGGGGGGGGOTHERE2: " + (oldTeamObject.equals(newTeamObject)));
return oldTeamObject.equals(newTeamObject);
}
};
private Activity activity;
public FavoritePageListAdapter() {
super(DIFF_CALLBACK);
}
public FavoritePageListAdapter(Activity ac) {
super(DIFF_CALLBACK);
this.activity = ac;
}
#NonNull
#Override
public OrderHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_favourite, parent, false);
return new FavoritePageListAdapter.OrderHolder(view);
}
#Override
public void onBindViewHolder(#NonNull OrderHolder holder,
int position) {
System.out.println("GGGGGGGGGGGOTHERE!!!");
if (position <= -1) {
return;
}
TeamObject teamObject = getItem(position);
try {
holder.txvTeamRowFavourite.setText(teamObject.getTeam_name());
} catch (Exception e) {
e.printStackTrace();
}
}
public class OrderHolder extends RecyclerView.ViewHolder {
private TextView txvTeamRowFavourite;
OrderHolder(View itemView) {
super(itemView);
txvTeamRowFavourite = itemView.findViewById(R.id.txv_team_row_favourite);
}
}
}

Here is a working example in KOTLIN
in the Fragment
binding.search.addTextChangedListener { text ->
viewModel.searchNameChanged(text.toString())
}
viewModel.customers.observe(this, Observer {
adapter.submitList(it)
binding.swipe.isRefreshing=false
})
search -> is my edit text
customers -> is the data list in the viewModel
View Model
private val _searchStringLiveData = MutableLiveData<String>()
val customers = Transformations.switchMap(_searchStringLiveData){string->
repository.getCustomerByName(string)
}
init {
refreshCustomers()
_searchStringLiveData.value=""
}
fun searchNameChanged(name:String){
_searchStringLiveData.value=name
}

I faced the same issue and solved it with the answer of #Rohit, thanks! I simplified my solution a bit to illustrate it better. There are Categories and each Category has many Items. The LiveData should only return items from one Category. The user can change the Category and then the fun search(id: Int) is called, which changes the value of a MutableLiveData called currentCategory. This then triggers the switchMap and results in a new query for items of the category:
class YourViewModel: ViewModel() {
// stores the current Category
val currentCategory: MutableLiveData<Category> = MutableLiveData()
// the magic happens here, every time the value of the currentCategory changes, getItemByCategoryID is called as well and returns a LiveData<Item>
val items: LiveData<List<Item>> = Transformations.switchMap(currentCategory) { category ->
// queries the database for a new list of items of the new category wrapped into a LiveData<Item>
itemDao.getItemByCategoryID(category.id)
}
init {
currentCategory.value = getStartCategoryFromSomewhere()
}
fun search(id: Int) { // is called by the fragment when you want to change the category. This can also be a search String...
currentCategory.value?.let { current ->
// sets a Category as the new value of the MutableLiveData
current.value = getNewCategoryByIdFromSomeWhereElse(id)
}
}
}

I implement the bar code searching product using the following approach.
Everytime the value of productBarCode changes, the product will be searched in the room db.
#AppScoped
class PosMainViewModel #Inject constructor(
var localProductRepository: LocalProductRepository) : ViewModel() {
val productBarCode: MutableLiveData<String> = MutableLiveData()
val product: LiveData<LocalProduct> = Transformations.switchMap(productBarCode) { barcode ->
localProductRepository.getProductByBarCode(barcode)
}
init {
productBarCode.value = ""
}
fun search(barcode: String) {
productBarCode.value = barcode
}}
In activity
posViewModel.product.observe(this, Observer {
if (it == null) {
// not found
} else {
productList.add(it)
rvProductList.adapter!!.notifyDataSetChanged()
}
})
for searching
posViewModel.search(barcode) //search param or barcode

Related

Recyclerview data disappears when device is rotated

Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).

Filling an adapter from different LiveData sources

I'm playing with LiveData and want to understand what it can do.
I want to fill my RecyclerView with data from different sources by switch(using filters if you like).
Filtering values inside an adapter is not an option.
So, I decided to use MediatorLiveData inside my view model.
Dao:
#Query("SELECT * FROM tasks WHERE completed = 0")
LiveData<List<Task>> getActiveTasksLiveData();
#Query("SELECT * FROM tasks")
LiveData<List<Task>> getAllTasksLiveData();
#Query("SELECT * FROM tasks WHERE completed = 1")
LiveData<List<Task>> getClosedTasksLiveData();
Repo:
public LiveData<List<Task>> getActiveTasks() {
return mTaskDao.getActiveTasksLiveData();
}
public LiveData<List<Task>> getAllTasks() {
return mTaskDao.getAllTasksLiveData();
}
public LiveData<List<Task>> getClosedTasks() {
return mTaskDao.getClosedTasksLiveData();
}
ViewModel
public class MainViewModel extends AndroidViewModel {
private final String TAG = "MainViewModel";
private final AppDataRepository mData;
private MediatorLiveData<List<Task>> mMediatorTasks;
public MainViewModel(#NonNull Application application) {
super(application);
mData = AppDataInjector.getDataRepository(application.getApplicationContext());
mMediatorTasks = new MediatorLiveData<>();
mMediatorTasks.setValue(null);
}
public LiveData<List<Task>> getTasks(){
return mMediatorTasks;
}
public void changeTasksOption(int index){
mMediatorTasks.removeSource(mData.getAllTasks());
mMediatorTasks.removeSource(mData.getActiveTasks());
mMediatorTasks.removeSource(mData.getClosedTasks());
if (index == R.id.navigation_all){
Log.i(TAG, "Add source: all");
mMediatorTasks.addSource(mData.getAllTasks(), new Observer<List<Task>>() {
#Override
public void onChanged(List<Task> tasks) {
Log.i(TAG, "Add source: all - setValue");
mMediatorTasks.setValue(tasks);
}
});
} else if (index == R.id.navigation_closed){
Log.i(TAG, "Add source closed");
mMediatorTasks.addSource(mData.getClosedTasks(), new Observer<List<Task>>() {
#Override
public void onChanged(List<Task> tasks) {
Log.i(TAG, "Add source: closed - setValue");
mMediatorTasks.setValue(tasks);
}
});
} else {
Log.i(TAG, "Add source active");
mMediatorTasks.addSource(mData.getActiveTasks(), new Observer<List<Task>>() {
#Override
public void onChanged(List<Task> tasks) {
Log.i(TAG, "Add source: active - setValue");
mMediatorTasks.setValue(tasks);
}
});
}
}
}
Fragment
public View onCreateView(#NonNull LayoutInflater inflater,
#Nullable ViewGroup container,
#Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_main, container, false);
mNavigationView = view.findViewById(R.id.navigation);
mFab = view.findViewById(R.id.fabMain);
mRecyclerView = view.findViewById(R.id.mainRecyclerView);
tasksAdapterLive = new TasksAdapterLive(mAdapterCallback);
RecyclerView.LayoutManager manager = new GridLayoutManager(getContext(), 1);
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setAdapter(tasksAdapterLive);
// set up bottom navigation listener
mNavigationView.setOnNavigationItemSelectedListener(item -> {
mViewModel.changeTasksOption(item.getItemId());
return true;
});
return view;
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
mViewModel.getTasks().observe(this, tasks -> {
if (tasks != null) {
tasksAdapterLive.setTasks(tasks);
tasksAdapterLive.notifyDataSetChanged();
}
});
mViewModel.changeTasksOption(mNavigationView.getSelectedItemId());
}
As you can see, I've decided to use MediatorLiveData inside my view model.
My main goal - change data inside adapter when changeTasksOption() called from fragment.
I use removeSource(), because how I understand it removes LiveData source from observing.
But, in my case it does not.
When I launch app, logs are:
MainViewModel: Add source active
MainViewModel: Add source: active - setValue
When I try switch to another source - logs are
MainViewModel: Add source: all
MainViewModel: Add source: all - setValue
MainViewModel: Add source: active - setValue
MainViewModel: Add source: all - setValue
MainViewModel: Add source: active - setValue
*** repeats about 100 times
RecyclerView is blinking
So, I kindly ask.
What am I doing wrong?
Did I misunderstood the documentation?
What really removeSourse() does?
Because in my case it does not remove sources.
In case my method implementing this is wrong, how do you suggest I do?
Thank you!
EDTITED:
After experimenting for couple of hours I've found solution. Yeep, this is bad(or maybe not?). But clearly this is not universal, because we do not use Romm + LiveData
Create normal Room functions that return List
#Query("SELECT * FROM tasks WHERE completed = 0")
List<Task> getActiveTasks();
#Query("SELECT * FROM tasks")
List<Task> getAllTasks();
#Query("SELECT * FROM tasks WHERE completed = 1")
List<Task> getClosedTasks();
Created MutableLiveData in repo
private MutableLiveData<List<Task>> mTasksTestActive, mTasksTestAll, mTasksTestClosed;
Add theese functions to repo
public LiveData<List<Task>> getActiveTasksTest() {
Executors.newSingleThreadExecutor().execute(() -> {
List<Task> taskList = mTaskDao.getActiveTasks();
mTasksTestActive.postValue(taskList);
});
return mTasksTestActive;
}
public LiveData<List<Task>> getAllTasksTest() {
Executors.newSingleThreadExecutor().execute(() -> {
List<Task> taskList = mTaskDao.getAllTasks();
mTasksTestAll.postValue(taskList);
});
return mTasksTestAll;
}
public LiveData<List<Task>> getClosedTasksTest() {
Executors.newSingleThreadExecutor().execute(() -> {
List<Task> taskList = mTaskDao.getClosedTasks();
mTasksTestClosed.postValue(taskList);
});
return mTasksTestClosed;
}
ViewModel changes:
public void changeTasksOption(int index) {
mMediatorTasks.removeSource(mData.getAllTasksTest());
mMediatorTasks.removeSource(mData.getActiveTasksTest());
mMediatorTasks.removeSource(mData.getClosedTasksTest());
if (index == R.id.navigation_all) {
Log.i(TAG, "Add source: all");
mMediatorTasks.addSource(mData.getAllTasksTest(), tasks -> {
Log.i(TAG, "Add source: all - postValue");
mMediatorTasks.postValue(tasks);
});
} else if (index == R.id.navigation_closed) {
Log.i(TAG, "Add source closed");
mMediatorTasks.addSource(mData.getClosedTasksTest(), tasks -> {
Log.i(TAG, "Add source: closed - postValue");
mMediatorTasks.postValue(tasks);
});
} else {
Log.i(TAG, "Add source active");
mMediatorTasks.addSource(mData.getActiveTasksTest(), tasks -> {
Log.i(TAG, "Add source: active - postValue");
mMediatorTasks.postValue(tasks);
});
}
}
And now, by switching UI, I have my result. No more loops and everything seems go ok.
But still! This is a bad solution. Maybe something is wrong with Room?
public void changeTasksOption(int index){
mMediatorTasks.removeSource(mData.getAllTasks());
mMediatorTasks.removeSource(mData.getActiveTasks());
mMediatorTasks.removeSource(mData.getClosedTasks());
No this is not how it should be!
The selected option should be in a LiveData. Then you can use Transformations.switchMap { against that LiveData to select the correct LiveData<List<Task>>.
private MutableLiveData<Integer> mSelectedIndex = new MutableLiveData<>();
private final LiveData<List<Task>> mMediatorTasks = Transformations.switchMap(mSelectedIndex, (index) -> {
if (index == R.id.navigation_all) {
return mData.getAllTasksTest();
} else if (index == R.id.navigation_closed) {
return mData.getClosedTasksTest();
} else {
return mData.getActiveTasksTest();
}
});
public void changeTasksOption(int index) {
mSelectedIndex.setValue(index);
}
public LiveData<List<Task>> getTasks(){
return mMediatorTasks;
}
Also, you should bring your mData.get*() methods to return LiveData<List<Task>> from the DAO again, that was a better solution.
You are returning values from your repo synchronously in your previous repo code -
public LiveData<List<Task>> getActiveTasks() {
return mTaskDao.getActiveTasksLiveData();
}
public LiveData<List<Task>> getAllTasks() {
return mTaskDao.getAllTasksLiveData();
}
public LiveData<List<Task>> getClosedTasks() {
return mTaskDao.getClosedTasksLiveData();
}
So when you call removeSource(mData.getAllTasksTest()), it synchronously fetches data from the repo and that is why you are receiving data from all the repos.
In your edited code, you are using a worker thread to fetch data, which means that your source livedata gets removed from the mediator live data before the repo returns any value.

Not getting LiveData observable values within Fragments (ItemKeyedDataSource)

I am working with Firestore and successfully integrated it with Paging Library using ItemKeyedDataSource. Here is a gist:
public class MessageDataSource extends ItemKeyedDataSource<Query, Message> {
//... private members
MessageDataSource(Query query) {
mQuery = query;
}
#Override
public void loadInitial(#NonNull LoadInitialParams<Query> params, #NonNull LoadInitialCallback<Message> callback) {
mLoadStateObserver.postValue(LoadingState.LOADING);
mQuery.limit(params.requestedLoadSize).get()
.addOnCompleteListener(new OnLoadCompleteListener() {
#Override
protected void onSuccess(QuerySnapshot snapshots) {
getLastDocument(snapshots);
// I'm able to get the values here
List<Message> m = snapshots.toObjects(Message.class);
for (Message message : m) {
Log.d(TAG, "onSuccess() returned: " + message.getTitle());
}
callback.onResult(snapshots.toObjects(Message.class));
}
#Override
protected void onError(Exception e) {
Log.w(TAG, "loadInitial onError: " + e);
}
});
}
#Override
public void loadAfter(#NonNull LoadParams<Query> params, #NonNull LoadCallback<Message> callback) {
Log.d(TAG, "LoadingState: loading");
mLoadStateObserver.postValue(LoadingState.LOADING);
params.key.limit(params.requestedLoadSize).get()
.addOnCompleteListener(new OnLoadCompleteListener() {
#Override
protected void onSuccess(QuerySnapshot snapshots) {
getLastDocument(snapshots);
callback.onResult(snapshots.toObjects(Message.class));
}
#Override
protected void onError(Exception e) {
Log.w(TAG, "loadAfter onError: " + e);
}
});
}
private void getLastDocument(QuerySnapshot queryDocumentSnapshots) {
int lastDocumentPosition = queryDocumentSnapshots.size() - 1;
if (lastDocumentPosition >= 0) {
mLastDocument = queryDocumentSnapshots.getDocuments().get(lastDocumentPosition);
}
}
#Override
public void loadBefore(#NonNull LoadParams<Query> params, #NonNull LoadCallback<Message> callback) {}
#NonNull
#Override
public Query getKey(#NonNull Message item) {
return mQuery.startAfter(mLastDocument);
}
/*
* Public Getters
*/
public LiveData<LoadingState> getLoadState() {
return mLoadStateObserver;
}
/* Factory Class */
public static class Factory extends DataSource.Factory<Query, Message> {
private final Query mQuery;
private MutableLiveData<MessageDataSource> mSourceLiveData = new MutableLiveData<>();
public Factory(Query query) {
mQuery = query;
}
#Override
public DataSource<Query, Message> create() {
MessageDataSource itemKeyedDataSource = new MessageDataSource(mQuery);
mSourceLiveData.postValue(itemKeyedDataSource);
return itemKeyedDataSource;
}
public LiveData<MessageDataSource> getSourceLiveData() {
return mSourceLiveData;
}
}
}
And then within MessageViewModel class's constructor:
MessageViewModel() {
//... Init collections and query
// Init Paging
MessageDataSource.Factory mFactory = new MessageDataSource.Factory(query);
PagedList.Config config = new PagedList.Config.Builder()
.setPrefetchDistance(10)
.setPageSize(10)
.setEnablePlaceholders(false)
.build();
// Build Observables
mMessageObservable = new LivePagedListBuilder<>(mFactory, config)
.build();
mLoadStateObservable = Transformations.switchMap(mMessageObservable, pagedListInput -> {
// No result here
Log.d(TAG, "MessageViewModel: " + mMessageObservable.getValue());
MessageDataSource dataSource = (MessageDataSource) pagedListInput.getDataSource();
return dataSource.getLoadState();
});
}
Note the situation:
When I'm initializing the viewmodel in MainActivity#oncreate method and observing it, it is working as intended and is able to view it in recyclerview.
Later I decided to create a Fragment and refactored it by moving all the logic to the Fragment and when I try to observe the same livedata, no values are returned. Here's how I doing it.
Within Fragment:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
mViewModel = ViewModelProviders.of(getActivity()).get(MessageViewModel.class);
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//...
mViewModel.getMessageObserver().observe(this, messages -> {
Log.d(TAG, "onCreateView() returned: " + messages.size());
});
mViewModel.getLoadingStateObserver().observe(this, loadingState -> {
Log.d(TAG, "onCreateView() returned: " + loadingState.name());
});
return view;
}
The interesting part:
Within the Fragment the loadstate is returning the values LOADING and SUCCESS
Within MessageDataSource, values of the query is successfully returned but while observing the same in the Fragment, I get no values.
What am I doing wrong here?
P.S: I'm learning Android.
With fragments a few problems can occur. Observers are set in onActivityCreated() to ensure the view is created and change 'this' in the observe statement to 'getViewLifecycleOwner()'. This prevent observers from firing more than once after the fragment is popped of the backstack for example. You can read about it here.
So change your observer to:
mViewModel.getLoadingStateObserver().observe(getViewLifecycleOwner(), loadingState -> {
Log.d(TAG, "onCreateView() returned: " + loadingState.name());
});
The example code showed on Share data between fragments is bare minimal and just looking at it I got the wrong overview until I read this part very carefully:
These fragments can share a ViewModel using their activity scope to
handle this communication, as illustrated by the following sample
code:
So basically you have to initialize the viewmodel in the Activity: ViewModelProviders.of(this).get(SomeViewModel.class);
And then on the Activity's fragment you can initialize it as:
mViewModel = ViewModelProviders.of(getActivity()).get(SomeViewModel.class);
mViewModel.someMethod().observe(this, ref -> {
// do things
});
This is what I was doing wrong and now it's fixed.

How to integrate Android Paging Library with NetworkBoundResource

My app is using Android's Architecture components library and is displaying a list of items fetched from a paginated REST api with an infinite scroll effect.
What I'm trying to do is to use the Paging Library in conjunction with a NetworkBoundResource, so that when the user scrolls down the list, the next items are fetched from the database and displayed if they exist, and the API is simultaneously called to update items in DB.
I could not find any example of these two patterns cohabiting.
Here is the DAO:
#Query("SELECT * FROM items ORDER BY id DESC")
LivePagedListProvider<Integer,MyItem> loadListPaginated();
Here is my NetworkBoundResource implementation:
public class PagedListNetworkBoundResource extends NetworkBoundResource<PagedList<MyItem>, List<MyItem>> {
#Override
protected void saveCallResult(#NonNull List<MyItem> items) {
// Inserting new items into DB
dao.insertAll(items);
}
#Override
protected boolean shouldFetch(#Nullable PagedList<MyItem> data) {
return true;
}
#NonNull
#Override
protected LiveData<PagedList<MyItem>> loadFromDb() {
return Transformations.switchMap(dao.loadListPaginated().create(INITIAL_LOAD_KEY, PAGE_SIZE),
new Function<PagedList<MyItem>, LiveData<List<MyItem>>>() {
#Override
public LiveData<PagedList<MyItem>> apply(final PagedList<MyItem> input) {
// Here I must load nested objects, attach them,
// and return the fully loaded items
}
});
}
#NonNull
#Override
protected LiveData<ApiResponse<List<MyItem>>> createCall() {
// I don't get the current paged list offset to perform a call to the API
return ...;
}
}
I also search lot about NetworkBoundResource i came to conclusion that NetworkBoundResource & Paging Lib its not related to each other. They both have there own functionality
As per article give by google about paging library
https://developer.android.com/topic/libraries/architecture/paging.html
1.for loading data from local db you need use DataSource
My Dao
#Dao
public interface UserDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(User... user);
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(List<User> users);
#Query("Select * from User ")
public abstract DataSource.Factory<Integer,User> getList();
}
2.then requesting data from network we need implement BoundaryCallback class with LivePagedListBuilder
public class UserBoundaryCallback extends PagedList.BoundaryCallback<User> {
public static final String TAG = "ItemKeyedUserDataSource";
GitHubService gitHubService;
AppExecutors executors;
private MutableLiveData networkState;
private MutableLiveData initialLoading;
public UserBoundaryCallback(AppExecutors executors) {
super();
gitHubService = GitHubApi.createGitHubService();
this.executors = executors;
networkState = new MutableLiveData();
initialLoading = new MutableLiveData();
}
public MutableLiveData getNetworkState() {
return networkState;
}
public MutableLiveData getInitialLoading() {
return initialLoading;
}
#Override
public void onZeroItemsLoaded() {
//super.onZeroItemsLoaded();
fetchFromNetwork(null);
}
#Override
public void onItemAtFrontLoaded(#NonNull User itemAtFront) {
//super.onItemAtFrontLoaded(itemAtFront);
}
#Override
public void onItemAtEndLoaded(#NonNull User itemAtEnd) {
// super.onItemAtEndLoaded(itemAtEnd);
fetchFromNetwork(itemAtEnd);
}
public void fetchFromNetwork(User user) {
if(user==null) {
user = new User();
user.userId = 1;
}
networkState.postValue(NetworkState.LOADING);
gitHubService.getUser(user.userId,20).enqueue(new Callback<List<User>>() {
#Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
executors.diskIO().execute(()->{
if(response.body()!=null)
userDao.insert(response.body());
networkState.postValue(NetworkState.LOADED);
});
}
#Override
public void onFailure(Call<List<User>> call, Throwable t) {
String errorMessage;
errorMessage = t.getMessage();
if (t == null) {
errorMessage = "unknown error";
}
Log.d(TAG,errorMessage);
networkState.postValue(new NetworkState(Status.FAILED, errorMessage));
}
});
}
}
3.My VM Code to load data from DB + Network
public class UserViewModel extends ViewModel {
public LiveData<PagedList<User>> userList;
public LiveData<NetworkState> networkState;
AppExecutors executor;
UserBoundaryCallback userBoundaryCallback;
public UserViewModel() {
executor = new AppExecutors();
}
public void init(UserDao userDao)
{
PagedList.Config pagedListConfig =
(new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(10)
.setPageSize(20).build();
userBoundaryCallback = new UserBoundaryCallback(executor);
networkState = userBoundaryCallback.getNetworkState();
userList = (new LivePagedListBuilder(userDao.getList(), pagedListConfig).setBoundaryCallback(userBoundaryCallback))
.build();
}
}
This assumes that each item in the callback has contains an index/offset. Typically that is not the case - the items may only contain ids.

Android Rxjava subscribe to a variable change

I am learning Observer pattern, I want my observable to keep track of a certain variable when it changes it's value and do some operations, I've done something like :
public class Test extends MyChildActivity {
private int VARIABLE_TO_OBSERVE = 0;
Observable<Integer> mObservable = Observable.just(VARIABLE_TO_OBSERVE);
protected void onCreate() {/*onCreate method*/
super();
setContentView();
method();
changeVariable();
}
public void changeVariable() {
VARIABLE_TO_OBSERVE = 1;
}
public void method() {
mObservable.map(value -> {
if (value == 1) doMethod2();
return String.valueOf(value);
}).subScribe(string -> System.out.println(string));
}
public void doMethod2() {/*Do additional operations*/}
}
But doMethod2() doesn't get called
Nothing is magic in the life : if you update a value, your Observable won't be notified. You have to do it by yourself. For example using a PublishSubject.
public class Test extends MyChildActivity {
private int VARIABLE_TO_OBSERVE = 0;
Subject<Integer> mObservable = PublishSubject.create();
protected void onCreate() {/*onCreate method*/
super();
setContentView();
method();
changeVariable();
}
public void changeVariable() {
VARIABLE_TO_OBSERVE = 1;
// notify the Observable that the value just change
mObservable.onNext(VARIABLE_TO_OBSERVE);
}
public void method() {
mObservable.map(value -> {
if (value == 1) doMethod2();
return String.valueOf(value);
}).subScribe(string -> System.out.println(string));
}
public void doMethod2() {/*Do additional operations*/}
}
If interested here a Kotlin version of Variable class, which lets subscribers to be updated after every variable change.
class Variable<T>(private val defaultValue: T) {
var value: T = defaultValue
set(value) {
field = value
observable.onNext(value)
}
val observable = BehaviorSubject.createDefault(value)
}
Usage:
val greeting = Variable("Hello!")
greeting.observable.subscribe { Log.i("RxKotlin", it) }
greeting.value = "Ciao!"
greeting.value = "Hola!"
This will print:
"Hello!"
"Ciao!"
"Hola!"
#dwursteisen Nothing is magic, no, but I think we can get it a little more magic than that... 😊
How about using an Rx BehaviourSubject in this way:
import rx.functions.Action1;
import rx.subjects.BehaviorSubject;
public class BehaviourSubjectExample {
public BehaviourSubjectExample() {
subject.skip(1).subscribe(new Action1<Integer>() {
#Override
public void call(Integer integer) {
System.out.println("The value changed to " + integer );
}
});
}
public final BehaviorSubject<Integer> subject = BehaviorSubject.create(0);
public int getValue() { return subject.getValue(); }
public void setValue(int value) { subject.onNext(value); }
}
Remove the .skip(1) if you want the observing code to see the initial value.
The variable backing remains with the BehaviourSubject and can be accessed through conventional Java Getter/Setter. This is a toy example of course: If your use case were really this simple there'd be no excuse for not just writing:
private int value = 0;
public int getValue() { return value; }
public void setValue(int value) {
this.value = value;
System.out.println("The value changed to " + value );
}
...but the use of BehaviourSubject lets you bridge changes to other Rx data-streams inside your class for composing more advanced behaviours.

Categories

Resources