Get activity onDestroy() in Recycler Adapter - java

I am using a Handler to display a timer in RecyclerView list item. When I press back the Activity that hosts the RecyclerView is completely destroyed, the Handler() still running in the background. The handler is created and initiated in ViewHolder. Is there any way to remove the callbacks from handler from ViewHolder itself?
My ViewHolder sample code
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CustomRunnable.CustomRunnableListener{
private val handler = Handler()
lateinit var customRunnable: CustomRunnable //custom runnable for my timer logic
fun bind(position: Int, listModelClass: ModelClass?){
if(someCondition){
customRunnable = CustomRunnable(handler, this, textView, listModelClass)
handler.postDelayed(customRunnable, 1000)
}
}
override fun onTimerFinish(listModelClass: ModelClass) {
// I get this call back when the timer finishes
handler.removeCallbacks(customRunnable)
}
}

As per my knowledge, there is no method on adapter that is called when RecyclerView is detached from activity.
Try creating a timer object or a list of objects in your BaseActivity or Application Class and after pressing onBack run a method that will stop that timer or timers.
//Declare timer
CountDownTimer cTimer = null;
//start timer function
void startTimer() {
cTimer = new CountDownTimer(30000, 1000) {
public void onTick(long millisUntilFinished) {
}
public void onFinish() {
}
};
cTimer.start();
}
//cancel timer
void cancelTimer() {
if(cTimer!=null)
cTimer.cancel();
}

You can do it onDestory() of activity
handler.removeCallbacksAndMessages(null);
Remove any pending posts of callbacks and sent messages whose obj is token. If token is null, all callbacks and messages will be removed.

if your Activity extended from androidx.activity.ComponentActivity, you can do this easily, just bind the lifecycle event then the super class would done their job as your desired, the sample code like below:
internal class SampleViewHolder(
private val activity: TheActivity,
view: View
) : RecyclerView.ViewHolder(view) {
fun bind(item: SampleInfo, position: Int) {
val view = itemView
...
bindLifecycle()
}
fun onViewRecycled() {
itemView.imv_sample.onViewRecycled(sampleAdapter.playStateStore)
activity.lifecycle.removeObserver(lifecycleObserver)
}
private val lifecycleObserver = object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
private fun onDestroy() {
MLog.info(TAG, "receive destroy event")
itemView.imv_sample.onHostActivityDestroyed()
}
}
private fun bindLifecycle() {
activity.lifecycle.run {
MLog.info(TAG, "success bind lifecycle")
addObserver(lifecycleObserver)
}
}
}

When the activity is destroyed, set the list adapter to null. This will make sure onViewDetachedFromWindow is called for all the views in list when the activity is destroyed.
#Override
public void onDestroy() {
mList.setAdapter(null);
super.onDestroy();
}
And then you can remove the callbacks from the handler running inside the viewHolder. This requires you to save the handler reference inside your viewHolder.
#Override
public void onViewDetachedFromWindow(#NonNull PurchaseItemViewHolder holder) {
if (holder.handler != null) {
holder.handler.removeCallbacksAndMessages(null);
}
super.onViewDetachedFromWindow(holder);
}

Related

How can you change ViewPager2 position inside the ViewPager2Adapter?

I programmed a Vocabulary Trainer with Vocabulary Cards. The Vocabulary Cards are Entries in a Room Database created from an asset. I am displaying these Vocabulary Cards with ViewPager2 in an Activity. I have a 'correct' and a 'false' button and when the user clicks on either, I want to update the Vocabulary Card (-> The entry in the sqlite database) and automatically swipe to the next item of the ViewPager2.
If I implement the buttons in the ViewPager2Adapter, I can't find a way to change the position of the ViewPager2. If I implement the buttons in the activity the sqlite entry does not update properly (After it updates the entry, the activity is constantly refreshed, it seems like it never the leaves the OnClick methode of the button).
So is it possible to change the position of ViewPager2 from inside the ViewPager2Adpater?
Thanks for your help!
That is the relevant code if I have the buttons in my ViewPager2Adapter. Here I don't know how to change the position of the ViewPager2
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
holder.btn_false.setOnClickListener(v15 -> {
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
That is the relevant code if I have the buttons in the Activity. Here the update function triggers an infinite updating of the Activity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
btn_correct_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
btn_false_2.setOnClickListener(view -> {
int currentPos = viewpager2.getCurrentItem();
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
if (vocabularyCards.size() == currentPos){
Intent intent = new Intent(TestActivity.this, MainActivity.class);
startActivity(intent);
}else {
viewpager2.setCurrentItem(currentPos + 1);
}
VocabularyCard vocabularyCard = vocabularyCards.get(currentPos);
vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard);
});
});
Objects.requireNonNull(getSupportActionBar()).setTitle(getResources().getString(R.string.learn_new_words));
LiveData<List<VocabularyCard>> allNewCards = vocabularyViewModel.getAllNewCards(goal);
allNewCards.observe(this, vocabularyCards -> vocabularyViewModel.setCurrentCards(vocabularyCards));
vocabularyViewModel.getCurrentCards().observe(this, vocabularyCards -> {
viewPager2Adapter.setCurrentCards(vocabularyCards);
viewpager2.setAdapter(viewPager2Adapter);
viewpager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
}
#Override
public void onPageSelected(int position) {
super.onPageSelected(position);
}
#Override
public void onPageScrollStateChanged(int state) {
super.onPageScrollStateChanged(state);
}
});
});
The update function in the Room DAO is straightforward:
#Update
void updateSingleVocabularyCard(VocabularyCard vocabularyCard);
I left out all the code that is not relevant.
There are several ways to propagate an event from the adapter to the activity where you manage your cards using ViewPager2. Let's have a look how it can be done either using an interface or using the same view model. But in any case I strongly recommend you to update your database in a background thread to prevent any possible UI lags.
1. Using an interface
This option is more flexible since you can propagate events as well as pass data as parameters. You can also reuse this interface for other cases. As far as I See you have a holder that has 2 buttons for the users to make choices. So our event here would be something like ChoiceEventListener, let's call this interface like so. Then you'd have to add a method to handle this event from within anywhere you wanna hear this event, and let's call its handle method onChoice(). Finally we would need a variable to indicate what the choice is. Now that ready to implement, let's write the new interface...
ChoiceEventListener.java
public interface ChoiceEventListener {
void onChoice(VocabularyCard vocabularyCard, boolean choice);
}
The next thing to do is to implement this interface where you want to listen to this event. In this case it is in your activity. There are 2 ways to do this:
You make your activity to inherit its methods using the implements keyword
YourActivity.java
public class YourActivity extends AppCompatActivity implements ChoiceEventListener {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, this);
}
#Override
public void onChoice(VocabularyCard vocabularyCard, boolean choice) {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
}
}
You can implement it as an anonymous function
YourActivity.java
public class YourActivity extends AppCompatActivity {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
// You must construct your adapter class with the listener
ViewPager2Adapter adapter = new ViewPager2Adapter(/* Other params... */, (vocabularyCard, choice) -> {
if(choice) {
// User pressed the correct button
}
else {
// User pressed the false button
}
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
});
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Here is your listener to deliver the choice event to it
private final ChoiceEventListener listener;
// Constructor
public ViewPager2Adapter(/* Other params... */, ChoiceEventListener listener) {
/* Other inits */
this.listener = listener;
}
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
listener.onChoice(vocabularyCard, true); // true for correct
});
holder.btn_false.setOnClickListener(v15 -> {
listener.onChoice(vocabularyCard, false); // false for false :)
});
}
}
2. Use the ViewModel for inter-communication
In this option we use a LiveData object to make page switching. The only thing you need to know in your activity is the current position which you get it from the adapter class. Once you update it in the adapter, set the current position value in live data so that you can switch the page in your activity.
VocabularyViewModel.java
public class VocabularyViewModel extends ViewModel {
public MutableLiveData<Integer> mldCurrentPosition = new MutableLiveData<>(0);
}
YourActivity.java
public class YourActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
initAll();
vocabularyViewModel.mldCurrentPosition().observe(this, currentPosition -> {
if(currenPosition == null) return; // ignore when null
viewpager2.setCurrentItem(currentPosition + 1);
}
}
}
Finally the ViewPager2Adapter class implementation would be something like this:
ViewPager2Adapter.java
public class ViewPager2Adapter extends RecyclerView.Adapter<ViewPager2ViewHolder> {
// Use a background thread for database operations
private Executor executor = Executors.newSingleThreadExecutor();
public void onBindViewHolder(#NonNull #NotNull ViewHolder holder, int position) {
VocabularyCard vocabularyCard = currentCards.get(position);
holder.btn_correct.setOnClickListener(view -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
holder.btn_false.setOnClickListener(v15 -> {
// Update card in the background
executor.execute(()-> vocabularyViewModel.updateSingleVocabularyCard(vocabularyCard));
// Then invoke switching to the next card
vocabularyViewModel.mldCurrentPosition.setValue(position + 1);
});
}
}

How to use ViewModel in a fragment?

I'm using the MVVM architecture. I have an activity and a few fragments, I would like to make a request in the API in the activity, and then using ViewModel, thanks to the obtained data, to display them in the fragment. How should I do this? My current solution that doesn't work:
Activity:
viewModelRoutesFragment = new ViewModelProvider(this).get(ViewModelRoutesFragment.class);
viewModelRoutesFragment.init();
Fragment:
viewModelRoutesFragment = new ViewModelProvider(this).get(ViewModelRoutesFragment.class);
viewModelRoutesFragment.getRoutes().observe(getActivity(), new Observer<List<RoutesResponse>>() {
#Override
public void onChanged(List<RoutesResponse> routes) {
//Show data
}
});
Repository:
public class RemoteRepository {
private ApiRequest apiRequest;
private MutableLiveData<List<RoutesResponse>> routes = new MutableLiveData<>();
public RemoteRepository() {
apiRequest = RetrofitRequest.getInstance().create(ApiRequest.class);
}
public MutableLiveData<List<RoutesResponse>> getRoutes() {
apiRequest.getRoutes()
.enqueue(new Callback<List<RoutesResponse>>() {
#Override
public void onResponse(Call<List<RoutesResponse>> call, Response<List<RoutesResponse>> response) {
if (response.isSuccessful())
routes.setValue(response.body());
}
#Override
public void onFailure(Call<List<RoutesResponse>> call, Throwable t) {
Log.i("Failure", "Fail!");
}
});
return routes;
}
}
ViewModel:
public class ViewModelRoutesFragment extends AndroidViewModel {
private RemoteRepository remoteRepository;
private LiveData<List<RoutesResponse>> routes;
public ViewModelRoutesFragment(#NonNull Application application) {
super(application);
}
public void init() {
remoteRepository = new RemoteRepository();
routes = remoteRepository.getRoutes();
}
public LiveData<List<RoutesResponse>> getRoutes() {
return routes;
}
}
Currently getting a null error. How can I avoid it properly?
java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.lifecycle.LiveData.observe(androidx.lifecycle.LifecycleOwner, androidx.lifecycle.Observer)' on a null object reference
in Fragment
Use
viewModelRoutesFragment = new ViewModelProvider(requireActivity()).get(ViewModelRoutesFragment.class);
instead of
viewModelRoutesFragment = new ViewModelProvider(this).get(ViewModelRoutesFragment.class);
Basically, we are trying to share the viewmodel across the activity and fragment.
so while during the activity creation we have to create the instance of viewmodel
viewModelRoutesFragment = new ViewModelProvider(requireActivity()).get(ViewModelRoutesFragment.class);
viewModelRoutesFragment.init();
In fragment also we need to reuse the ViewModelRoutesFragment so in onViewCreated()
get the instance of the ViewModel and observe the live data
viewModelRoutesFragment = new ViewModelProvider(requireActivity()).get(ViewModelRoutesFragment.class);
viewModelRoutesFragment.getRoutes().observe(getActivity(), new Observer<List<RoutesResponse>>() {
#Override
public void onChanged(List<RoutesResponse> routes) {
// updation of UI
}
});
You don't need your view model reference in the activity. You should have an instance of fragments inside the activity. Your fragment already holding a reference to the ViewModel. Delete these line from the activity -> :
viewModelRoutesFragment = new ViewModelProvider(this).get(ViewModelRoutesFragment.class);
viewModelRoutesFragment.init();
Make sure you initialize your fragment in activity. Your activity is just a holder block, which actually replace the fragment using fragment manager. It doenst required any viewmodel if you are using a fragment with it.
Also, call this method inside your fragment viewModelRoutesFragment.init();
below this line
viewModelRoutesFragment = new ViewModelProvider(this).get(ViewModelRoutesFragment.class);

Updating list view periodically

I currently have a List View that takes data from an ArrayList that gets update every certain amount of time in the MainActivity. I want to update the List View every certain amount of time, let say 2 seconds. The problem is that I have this Adapter on a fragment and if I call the notifyDataSetChanged(); from the Adapter, the View only gets update if I switch between the MainActivity and the fragment. I want that this List view refreshes every 2 seconds while I'm having the fragment in View. I have tried to run a TimerTask on the Adapter class, but I get an exception:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the
original thread that created a view hierarchy can touch its views.
I guess you can only update the view from the MainActivity that has the fragment attached.
This how I was running the timer:
public void setTimer(int seconds) {
timer = new Timer();
timer.schedule(new RemindTask(), seconds * 1000);
}
public class RemindTask extends TimerTask {
//Refreshing list view
public void run() {
notifyDataSetChanged();
timer.cancel();
setTimer(2);
}
}
Would it be safe to create an instance of the adapter on the MainActivity and call notifyDataSetChanged() every certain amount of time?
You can Create one broadcast receiver in Fragment. with certain action name.
Lets say "refresh_data".
Then send this broadcast from MainActivity.
Example Code.
public class MyFragment extends Fragment{
private BroadcastReceiver refreshData = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
myAdapter.notifyDataSetChanged();
}
};
#Override
public void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(getContext()).registerReceiver(refreshData,
new IntentFilter("refresh_Data"));
}
#Override
public void onPause() {
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(refreshData);
super.onPause();
}
}
In Activity call following method whenever your list is updated
Intent intent = new Intent("refresh_data");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

Adapter leaks memory?

I have an adapter what uses the activity context to register and unregister a listener.
Activity mActivity;
MyBroadcastReceiver mReceiver;
#Override
public void onAttachedToRecyclerView (RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mActivity.registerReceiver(mReceiver, ...);
}
#Override
public void onDetachedFromRecyclerView (RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
mActivity.unregisterReceiver(mReceiver);
mActivity = null;
}
Although the onAttachedToRecyclerView always gets called, the detach method never, so the adapter leaks a lot of memory even after closing the activity.(running is only noticeable in the Settings app)
What do I have to do?
To be safe, you could register the receiver in your Activity. You shouldn't hold on to an Activity reference anywhere really...
If you really want to register the receiver from your adapter use an interface.
public interface Registerer {
void register();
void unregister();
}
In Activity:
mRecyclerView.setAdapter(new RecyclerAdapter(someDataSet,
new Registerer() {
public void register() {
registerReceiver(mReceiver, ...);
}
public void unRegister() {
unregisterReceiver(mReceiver);
}
});
Then you can call the interface methods from your adapter. I don't really see the point of cramming this into your view adapter though.

SwipeRefreshLayout trigger programmatically

Is there any way to trigger the SwipeRefreshLayout programmatically? The animation should start and the onRefresh method from the OnRefreshListener interface should get called.
if you are using the new swipeRefreshLayout intoduced in 5.0
As the image shown above you just need to add the following line to trigger the swipe refresh layout programmatically
Work
in Java:
mSwipeRefreshLayout.post(new Runnable() {
#Override
public void run() {
mSwipeRefreshLayout.setRefreshing(true);
}
});
on in Kotlin:
mSwipeRefreshLayout.post { mSwipeRefreshLayout.isRefreshing = true }
NOT work
if you simply call in Java:
mSwipeRefreshLayout.setRefreshing(true);
or in Kotlin
mSwipeRefreshLayout.isRefreshing = true
it won't trigger the circle to animate, so by adding the above line u just make a delay in the UI thread so that it shows the circle animation inside the ui thread.
By calling mSwipeRefreshLayout.setRefreshing(true) the OnRefreshListener will NOT get executed
In order to stop the circular loading animation call mSwipeRefreshLayout.setRefreshing(false)
In order to trigger SwipeRefreshLayout I tried this solution:
SwipeRefreshLayout.OnRefreshListener swipeRefreshListner = new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
Log.i(TAG, "onRefresh called from SwipeRefreshLayout");
// This method performs the actual data-refresh operation.
// The method calls setRefreshing(false) when it's finished.
loadData();
}
};
Now key part:
swipeLayout.post(new Runnable() {
#Override public void run() {
swipeLayout.setRefreshing(true);
// directly call onRefresh() method
swipeRefreshListner.onRefresh();
}
});
Simply create a SwipeRefreshLayout.OnRefreshListener and call its function onRefresh() whenever needed:
SwipeRefreshLayout srl;
SwipeRefreshLayout.OnRefreshListener refreshListener;
srl = (SwipeRefreshLayout)v.findViewById(R.id.swipeRefreshLayout);
refreshListener = new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
//Do your stuff here
}
};
srl.setOnRefreshListener(refreshListener);
Now, whenever you want to call it manually, just call it through the listener
refreshListener.onRefresh();
Bit late to the thread, but you do not need to launch a Runnable to do this. You can simply trigger the refresh and call your onRefresh method directly in onCreate, or wherever you want this to happen:
class MyFragment: SwipeRefreshLayout.OnRefreshListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
}
fun initViews() {
swipe_refresh_layout?.apply {
setOnRefreshListener(this#MyFragment)
isRefreshing = true
onRefresh()
}
}
override fun onRefresh() {
// Do my refresh logic here
}
}
binding.swipeRefreshLayout.setRefreshing(true); // show loading
binding.swipeRefreshLayout.post(this::updateUI); // call method
binding.swipeRefreshLayout.setOnRefreshListener(this::updateUI); // call method
You can call onRefresh() method programmatically and then inside the method start the animation if it is not already started. See the following:
#Override
public void onRefresh() {
if (!mSwipeRefreshLayout.isRefreshing()) mSwipeRefreshLayout.setRefreshing(true);
//TODO
}
Just to force in add this two to ennable swipe gesture
swipeRefreshLayout.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
#Override
public void onRefresh() {
Log.i(TAG, "onRefresh called from SwipeRefreshLayout");
// This method performs the actual data-refresh operation.
// The method calls setRefreshing(false) when it's finished.
FetchData();
}
}
);

Categories

Resources