How to handle Item clicks for a recycler view using RxJava - android

I was interested to find out what is the best way to respond to a item click of a recycler view.
Normally I would add a onclick() listener to the ViewHolder and pass back results to the activity/fragment through a interface.
I thought about adding a Observable in the onBindViewHolder but i do not want to create a new Observable for every item binding.

You can use RxBinding and then create a subject inside of your adapter, then redirect all the events to that subject and just create a getter of the subject to act as an observable and finally just subscribe you on that observable.
private PublishSubject<View> mViewClickSubject = PublishSubject.create();
public Observable<View> getViewClickedObservable() {
return mViewClickSubject.asObservable();
}
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup pParent, int pViewType) {
Context context = pParent.getContext();
View view = (View) LayoutInflater.from(context).inflate(R.layout.your_item_layout, pParent, false);
ViewHolder viewHolder = new ViewHolder(view);
RxView.clicks(view)
.takeUntil(RxView.detaches(pParent))
.map(aVoid -> view)
.subscribe(mViewClickSubject);
return viewHolder;
}
An usage example could be:
mMyAdapter.getViewClickedObservable()
.subscribe(view -> /* do the action. */);

Step 1: Move the business logic out of the activities to domain classes/services
Optional: Use https://github.com/roboguice/roboguice to easily wire up your services with each other.
Step 2: #Inject (or just set) your service into your adapter
Step 3: Grab https://github.com/JakeWharton/RxBinding and use the super powers in your adapter:
RxView.clicks(button).subscribe(new Action1<Void>() {
#Override
public void call(Void aVoid) {
myCoolService.doStuff();
}
});
Step 4: Get a runtime crash and learn how to deal with subscriptions
Step 5: PROFIT :)

I would suggest you to do with your initial aproach of an observable per element on click, but in order to avoid create a new observable every time you can just cache the items emitted the first time using cache.
/**
* Here we can prove how the first time the items are delayed 100 ms per item emitted but second time becuase it´s cached we dont have any delay since
* the item emitted are cached
*/
#Test
public void cacheObservable() {
Integer[] numbers = {0, 1, 2, 3, 4, 5};
Observable<Integer> observable = Observable.from(numbers)
.doOnNext(number -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
})
.cache();
long time = System.currentTimeMillis();
observable.subscribe(System.out::println);
System.out.println("First time took:" + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
observable.subscribe(System.out::println);
System.out.println("Second time took:" + (System.currentTimeMillis() - time));
}

My solution was much like #epool 's except use EventBus model.
First, create a RxBus:
RxBus.java
public class RxBus {
private final Subject<Object, Object> _bus = new SerializedSubject<>(PublishSubject.create());
public void send(Object o) { _bus.onNext(o); }
public Observable<Object> toObserverable() { return _bus; }
public boolean hasObservers() { return _bus.hasObservers(); }
}
Then, you have two way to use RxBus. Create your custom Application class with RxBus reference or create RxBus in Activity/Fragment then pass it to adapter. I'm use the first.
MyApp.java
public class MyApp extends Application {
private static MyApp _instance;
private RxBus _bus;
public static MyApp get() { return _instance; }
#Override
public void onCreate() {
super.onCreate();
_instance = this;
_bus = new RxBus();
}
public RxBus bus() { return _bus; }
}
then use
MyApp.get().bus()
to get RxBus instance.
The usage of RxBus in Adpater was like this:
public class MyRecyclerAdapter extends ... {
private RxBus _bus;
public MykRecyclerAdapter (...) {
....
_bus = MyApp.get().bus();
}
public ViewHolder onCreateViewHolder (...) {
_sub = RxView.longClicks(itemView) // You can use setOnLongClickListener as the same
.subscribe(aVoid -> {
if (_bus.hasObservers()) { _bus.send(new SomeEvent(...)); }
});
}
}
You can send any class with _bus.send(), which We will recieve in the Activity:
RxBus bus = MyApp.get().bus(); // get the same RxBus instance
_sub = bus.toObserverable()
.subscribe(e -> doSomething((SomeEvent) e));
About unsubscribe.
In MyRecyclerAdapter call _sub.unsubscribe() in clearup() methods and call _sub.unsubscribe() in Activity's onDestory().
#Override
public void onDestroy() {
super.onDestroy();
if (_adapter != null) {
_adapter.cleanup();
}
if (_sub != null) {
_sub.unsubscribe()
}
}

We generally need the Pojo/Model class from list on clicked index. I do it in following way:
1) Create a BaseRecyclerViewAdapter
abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val clickListenerPublishSubject = PublishSubject.create<T>()
fun observeClickListener(): Observable<T> {
return clickListenerPublishSubject
}
fun performClick(t: T?) {
t ?: return
clickListenerPublishSubject.onNext(t)
}
}
2) In any adapter (For example MyAdapter)
class MyAdapter(private val events: List<Event>, context: Context) : BaseRecyclerViewAdapter<Event>() {
//... Other methods of RecyclerView
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
if (holder is EventViewHolder) {
holder.eventBinding?.eventVm = EventViewModel(events[position])
holder.eventBinding?.root?.setOnClickListener({ _ ->
// This notifies the subscribers
performClick(events[position])
})
}
}
}
3) Inside the Activity or Fragment where click listener is needed
myAdapter?.observeClickListener()
?.subscribe({ eventClicked ->
// do something with clicked item
})

Related

Android observe data not being called

Hi I am new to android development and am trying to get my head around the architecture but have spent the past 2 days trying to figure out LiveData
I am using an SDK which allows me to scan some sensors, I display the sensors on the device and then toggle a switch to connect the sensor. Once the sensor is connected I have created a button which runs a funcion called startMeasuring() everytime some data is measured a callback is hit and this is where my struggle begins.
In my MainActivity I have the following code which is ran once I toggle the switch to connect to the sensor.
public void onConnectedSensorClick(BluetoothDevice sensor, Integer position, Boolean checked) {
XsensDotDevice xsDevice = new XsensDotDevice(this, sensor, new XsDevice(this));
if (checked) {
xsDevice.connect();
mMainActivityViewModel.addConnectedSensor(xsDevice);
}
}
The XsensDotDevice() expects 3 parameters the context, scanned sensor and the callback class.
In my callback calss the following callback function is overridden
#Override
public void onXsensDotDataChanged(String s, XsensDotData xsensDotData) {
}
This function is the one which gets triggered when I start measuring and the sensor sends the device a measurement.
I am have created a ViewModel and Repository which is what I want to use to store this data so I can access it back in my MainActivity using an Observer
I got the ViewModel and Repository working for my scanned devices but I'm not sure how to get this working for the measurement data because I can't access the ViewModel in my callback class XsDevice() to pass the data to the Repository
What I want to do is somehow pass the XsensDotData (measurement data) to the SensorDataRepository and then create an Observer in my MainActivity like so.
mMainActivityViewModel.getSensorData().observe(this, new Observer<XsensDotData>() {
#Override
public void onChanged(XsensDotData xsensDotData) {
for(int i = 0; i< xsensDotData.getFreeAcc().length; i++){
Log.d("Sensor Data Acceleration " + i, String.valueOf(xsensDotData.getFreeAcc()[i]));
}
}
});
I have already created a Repository and ViewModel which i will show below
Repository
public class SensorDataRepository {
private static SensorDataRepository instance;
private XsensDotData dataSet = new XsensDotData();
public static SensorDataRepository getInstance() {
if (instance == null) {
instance = new SensorDataRepository();
}
return instance;
}
public MutableLiveData<XsensDotData> getSensorData() {
MutableLiveData<XsensDotData> data = new MutableLiveData<>();
data.setValue(dataSet);
return data;
}
public void addSensorData(XsensDotData data) {
dataSet = data;
}
}
ViewModel
public class MainActivityViewModel extends ViewModel {
private MutableLiveData<ArrayList<BluetoothDevice>> mScannedSensors;
private ScannedSensorRepository mScannedSensorRepo;
private MutableLiveData<ArrayList<XsensDotDevice>> mConnectedSensors;
private ConnectedSensorRepository mConnectedSensorRepo;
private MutableLiveData<XsensDotData> mSensorData;
private SensorDataRepository mSensorDataRepo;
public void init() {
if (mScannedSensors != null) {
return;
}
mScannedSensorRepo = ScannedSensorRepository.getInstance();
mScannedSensors = mScannedSensorRepo.getScannedSensors();
if (mConnectedSensorRepo != null) {
return;
}
mConnectedSensorRepo = ConnectedSensorRepository.getInstance();
mConnectedSensors = mConnectedSensorRepo.getConnectedSensors();
if (mSensorDataRepo != null) {
return;
}
mSensorDataRepo = SensorDataRepository.getInstance();
mSensorData = mSensorDataRepo.getSensorData();
}
public LiveData<ArrayList<BluetoothDevice>> getScannedSensors() {
return mScannedSensors;
}
public void addScannedSensor(BluetoothDevice device) {
mScannedSensorRepo.addScannedSensors(device);
}
public LiveData<ArrayList<XsensDotDevice>> getConnectedSensors() {
return mConnectedSensors;
}
public void addConnectedSensor(XsensDotDevice device) {
mConnectedSensorRepo.addConnectedSensors(device);
}
public LiveData<XsensDotData> getSensorData() {
return mSensorData;
}
public void addSensorData(XsensDotData data) {
mSensorDataRepo.addSensorData(data);
}
}
I included the code for the scanned and connect devices in the ViewModel in case it come in handy and helps explain whats going on.
Thank you for any help!
Here is a simple example of how I use LiveData. In my view model I will have a value as so :
var isInternetAvailable = MutableLiveData<Boolean>().apply { value = true }
in my activity I will have the code:
viewmodel.isInternetAvailable.observe(this, Observer {
// execute your logic here
var theValue = viewmodel.isInternetAvailable.value!!
}
Then in my viewModel when the internet has changed I will use
viewmodel.isInternetAvailable.postValue(true)
So for your code - as far as I can see, you're observing the function but not posting to it in order to trigger your observer function
You can use
MutableLiveData<XsensDotData> data = new MutableLiveData<>();
and then
data.postValue(dataSet)
That should hopefully trigger your observer

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.

Android implement search with view model and live data

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

No adapter attached; skipping layout when clicking back button and starting the app again

Android Studio 3.2 Canary 18
kotlin_version = 1.2.50
I have a simple app that uses a recyclerview and adapter. When the app starts is load all the data.
However, when I click the back button and start the app again. It won't display the data (blank).
If I clear the app from memory and start the app. The data will load as normal.
I am loading the data from sqlite and the data is loaded each time. as it populates the insectDataModelList.
After going into the RecyclerView.java source code the reason is the mAdapter is null. However, I have
checked that the adapter is correct when I set it to the recyclerview.
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
...
}
My MainActivity.java is Java
public class MainActivity extends AppCompatActivity {
private RecyclerView rvInsects;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
}
private void setupAdapter(List<InsectDataModel> insectDataModelList) {
final LayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false);
rvInsects.setLayoutManager(layoutManager);
rvInsects.setHasFixedSize(true);
final InsectAdapter insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
insectAdapter.notifyDataSetChanged();
}
/* Callback from database */
public void loadAllInsects(final Cursor cursor) {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
final List<InsectDataModel> insectDataModelList = insectInteractorMapper.map(cursor);
/* data loaded with 24 items */
setupAdapter(insectDataModelList);
}
}
InsectAdapter.kt is Kotlin.
class InsectAdapter(private val insectList: MutableList<InsectDataModel>)
: RecyclerView.Adapter<InsectAdapter.CustomInsectHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInsectHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.insect_row_item, parent, false)
return CustomInsectHolder(view)
}
override fun onBindViewHolder(holder: CustomInsectHolder, position: Int) {
holder.tvFriendlyName.text = insectList[position].friendlyName
holder.tvScientificName.text = insectList[position].scientificName
}
override fun getItemCount(): Int {
return insectList.size
}
class CustomInsectHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivDangerLevel: DangerLevelView = itemView.findViewById(R.id.ivDangerLevel)
val tvFriendlyName: TextView = itemView.findViewById(R.id.tvFriendlyName)
val tvScientificName: TextView = itemView.findViewById(R.id.tvScientificName)
}
}
The database I use rxjava2 to do the query
public class DatabaseManager {
private static DatabaseManager sInstance;
private MainActivity mainActivity;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance(MainActivity context) {
if (sInstance == null) {
sInstance = new DatabaseManager(context);
}
return sInstance;
}
private DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = context;
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Everything works as expected when the apps installs for the first time. And if you clear it out of memory.
However, its only when you click the back button, and then try and start the app it will not load any data
because of the mAdapter being null in the RecyclerView class.
When I click the back button and then start the app again. All I get is a blank screen i.e.
Updated DatabaseManager class that removes the singleton and used a weakreference to ensure that the MainActivity instance is garbage collected.
public class DatabaseManager {
private WeakReference<MainActivity> mainActivity;
private BugsDbHelper mBugsDbHelper;
public DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = new WeakReference<>(context);
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Many thanks for any suggestions,
When you click the back button and relaunch the app, a new instance of MainActivity is started.
At the same time, your DatabaseManager is a singleton. Its reference is stored as a static variable. It survives the activity recreation. It will live until the process is killed.
So, when you run queryAllInsects for the second time, the callback is sent to the old instance of MainActivity, which is not visible anymore.
You should not keep a reference to MainActivity in DatabaseManager. It's a memory leak, because it cannot be garbage collected.
The issue is most likely that you are loading the data in your onCreate() and not in onResume(). When you press back to "close the app" you are not necessarily clearing the UI stack from memory. That's why when you go back into the app, it doesn't invoke onCreate() again, and doesn't load your data again.
Keep everything the same, just move your data loading from onCreate() to onResume(). That way, whenever the screen is shown to the user, the data will load.
Few observations:
You are still passing the MainActivity to the BugsDbHelper class, take care of the reference there.
It's probably a good idea to include a "cleaning method" in Singleton classes, which should be called in onStop() or onDestroy() of an activity. onStop() is preferred since onDestroy() is not guaranteed to be called immediately.
The "cleaning method" in Singleton class should do the following:
a) Nullify any references to the parameters, objects, context or callbacks you have asked as a dependency in the constructor or otherwise.
b) If the Singleton class has created "new" objects with context dependencies, make sure to include similar cleaning methods in these classes too.
To avoid crashes and memory leakage in fragment/activities, make sure you are cleaning up your recycler view/adapter in onStop(). The callbacks can be received anytime, and if that happens while your activity is in the background, you are bound to get a "force close" fortune cookie.
Keep an eye on the activity/fragment lifecycle. A lot of issues are just because of ignoring the lifecycle callbacks. These are there for a reason, utilize them.
Put this 2 lines in onResume() and remove from onCreate() and try it.
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
I suggest the following changes:
MainActivity, the less code you write in the activity the better, move all the data retrieval part to the DatabaseManager. Also setup the RecyclerView once and only update the dataset when appropriate:
public class MainActivity extends AppCompatActivity {
private List<InsectDataModel> insectDataModelList = new ArrayList<>();
private Disposable disposable;
private RecyclerView rvInsects;
private InsectAdapter insectAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
setupAdapter();
//Request Data, take advantage of RxJava to load data asynchronously
DatabaseManager.getInstance(this)
.queryAllInsects("friendlyName")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<InsectDataModel>>() {
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(List<InsectDataModel> response) {
insectDataModelList.clear();
insectDataModelList.addAll(response);
insectAdapter.notifyDatasetChanged();
}
#Override
public void onError(Throwable e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});;
}
private void setupAdapter() {
//Setup RecyclerView Only need to be called once
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
LayoutManager layoutManager = new LinearLayoutManager(this); // LinearLayoutManager is Vertical by default
rvInsects.setLayoutManager(layoutManager); // You don't event have to define it as RecyclerView use LinearLayoutManager.Vertical by default
rvInsects.setHasFixedSize(true);
insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
}
#Override
protected void onDestroy() {
//Dispose observer if activity is destroyed to prevent memory leak
if(disposable != null && !disposable.isDisposed())
disposable.dispose();
super.onDestroy();
}
}
And in DatabaseManager, instead of observing the data source(Cursor) and notify the requester(Activity) via callback, we get the data stream and pass it the caller to observe:
public class DatabaseManager {
private static DatabaseManager sInstance;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance() {
if (sInstance == null) {
sInstance = new DatabaseManager();
}
return sInstance;
}
private DatabaseManager() {
// Move the actualy database initiation to application class or singleton
mBugsDbHelper = BugsDbHelper.getInstance(); // or ApplicationController.getDbHelper();
}
#SuppressLint("CheckResult")
public SingleObserver<List<InsectDataModel>> queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.map(new Function<Cursor, List<Object>>() {
#Override
public List<Object> apply(Cursor cursor) throws Exception {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
return insectInteractorMapper.map(cursor);
}
});
}
}
Now the solution here is to rely on the RxJava to change the callback pattern to the observer pattern. So instead of passing the activity (callback) and waiting to be called, we get the data steram (observable) and observe it for the response. This eliminate the leak problem all together and enhance the readability and maintainability.
Also don't forget to move the Database initialization to the Application class or a Singleton instance to prevent multiple instantiation. The easier solution would be like:
public class ApplicationController extends Application {
private BugsDbHelper mBugsDbHelper;
#Override
public void onCreate() {
super.onCreate();
mBugsDbHelper = new BugsDbHelper(this);
}
public BugsDbHelper getDbHelper(){
return mBugsDbHelper ;
}
}

Unit test using Mockito- Make mock of Abstract, void method

I am trying MVP pattern with TDD.
I have the following contract for Model, View And Presenter
Contract Class
interface GithubContract {
interface View {
void displayUsers(List<GurkhaComboDTO> userList);
}
interface Model {
void getUsersAndPromptPresenter(String userName, Presenter presenter);
}
interface Presenter {
void searchUsers(String userName);
void loadUsers(List<GithubUserDTO> userList);
}
}
I am trying to unit test the presenter logic like this :
Test Class
#RunWith(MockitoJUnitRunner.class)
public class GithubPresenterWithMockitoTest {
#Mock
GithubContract.Model mockedModel;
#Test
public void shouldDisplayUsersToScreen() {
//given
final GithubContract.View view = new MockView(); // I have created the mock myself for the view this time.
final GithubContract.Presenter presenter = new GithubPresenter(view, mockedModel);
***********************************************************
// I do not know what to write here
****************************************************
presenter.searchUsers("");
Assert.assertEquals(true, ((MockView) (view)).enoughItems);
}
}
My MockView / VIEW class looks like this :
This is -> Mock class
class MockView implements GithubContract.View {
boolean enoughItems = false;
#Override
public void displayUsers(List<GurkhaComboDTO> userList) {
enoughItems = true;
}
}
My PRESENTER implementation of contract is like this ..
This is -> Real Class
class GithubPresenter implements GithubContract.Presenter {
private GithubContract.View view;
private GithubContract.Model model;
GithubPresenter(GithubContract.View view, GithubContract.Model model) {
this.view = view;
this.model = model;
}
#Override
public void searchUsers(String userName) {
model.getUsersAndPromptPresenter(userName, this);
}
#Override
public void loadUsers(List<GithubUserDTO> data) {
if (data != null) {
if (!data.isEmpty()) {
view.displayUsers(users);
}
}
}
I have the MODEL class Implementation like this :
This is -> Real Class
public class GithubModel implements Model {
#Inject
GithubAPIService apiService;
private Call<GithubUserListDTO> userListCall;
private Context context;
GithubModel(Context context) {
this.context = context;
apiService = Util.getAPIService(); // I am using dagger, retrofit and okhttp3 with GSON to get Objects directly from network call
}
#Override
public void getUsersAndPromptPresenter(final String userName, final GithubContract.Presenter presenter) {
userListCall = apiService.searchGitHubUsers(userName);
if(Util.isInternetConnected(context)) {
userListCall.enqueue(new Callback<GithubUserListDTO>() {
#Override
public void onResponse(Call<GithubUserListDTO> call, Response<GithubUserListDTO> response) {
try {
presenter.loadUsers(response.body().getList());
} catch (Exception ignored) {
Util.log(ignored.getMessage());
}
}
#Override
public void onFailure(Call<GithubUserListDTO> call, Throwable t) {
}
});
}else {
Util.log("No Internet");
}
}
}
Now the real problem part:
I was successfully able to test the presenter with the mock of GithubContract.Model myself, But I want to use Mockito to mock the Model but as my getUsersAndPromptPresenter() method is abstract, returns void, takes parameters and calls back to presenter from an Inner class inside the method.
How can I mock my Model? If I need to bring some change in architecture in order to be able to make it testable, then please suggest it.
You shouldn't pass presenter to Model, Model and Presenter shouldn't be tightly coupled because it prevents model classes from being reusable. Instead provide succesfull and error callbacks(or a composite object that contains both these callbacks). And then you will be able to capture that callback with mockito and call the required one. Also it's very common today to use RxJava, it makes it easier to mock Model classes.
And here is a general good practice: you should avoid to use And/Or words in method names because it indicates that the method is doing more than one thing which is bad

Categories

Resources