How to save web view state in fragment on orientation changed? - java

I save the web-view state in fragment with ViewModel in java. As this way:
public class PageViewModel extends ViewModel {
private MutableLiveData<CustomWebView> liveData = new MutableLiveData<>();
public void setWebView(CustomWebView webView) {
liveData.setValue(webView);
}
public LiveData<CustomWebView> getWebView() {
return liveData;
}
}
In onCreate in Fragment and save current web-view state:
pageViewModel = new ViewModelProvider(this).get(PageViewModel.class);
pageViewModel.setWebView((CustomWebView) view);
And finally in onConfigurationChanged in Fragment:
#Override
public void onConfigurationChanged(#NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
pageViewModel.getWebView().observe(requireActivity(), new Observer<CustomWebView>() {
#Override
public void onChanged(CustomWebView customWebView) {
webView = customWebView;
}
});
}
So, When orientation is change, the web-view not reloading. But when I use this way in kotlin, The Webview not save state and it reloaded.
PageViewModel:
class PageViewModel : ViewModel() {
val liveData = MutableLiveData<CustomWebView>()
fun setWebView(webView: CustomWebView?) {
liveData.value = webView
}
fun getWebView(): LiveData<CustomWebView?>? {
return liveData
}
}
In global variables: private lateinit var pageViewModel: PageViewModel and in onCreate method of fragment:
pageViewModel = ViewModelProvider(activity!!).get(PageViewModel::class.java)
pageViewModel.setWebView(view as CustomWebView)
And finally in onConfigurationChanged in Fragment:
pageViewModel.getWebView()?.observe(requireActivity(), Observer { customWebView ->
if (customWebView != null) {
webView = customWebView
}
})
Can you help me? Where did I go wrong?

Your way is fine and it works well. I tested your code but in my app, onConfigurationChanged not ran. I searched for this problem and add this line to the activity tag in the manifest:
android:configChanges="orientation|screenSize"
And then * onConfigurationChanged * was run.

Related

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);

Get activity onDestroy() in Recycler Adapter

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);
}

PagerAdapter changed adapters content with notifying

So first things first, here's the error that I'm getting:
java.lang.IllegalStateException: The application's PagerAdapter
changed the adapter's contents without calling
PagerAdapter#notifyDataSetChanged! Expected adapter item count: 18,
found: 28
I'm using a RecyclerView in my main activity and that has a List<Objects> as the dataset, that's all fine.
From that activity I call the second activity when a RecyclerView item is clicked that is basically a gallery implemented using a ViewPager using this code:
public void startSlideActivity(final int position) {
DataTransferer.get().storeItems(feed);
Intent i = new Intent(context, SlideActivity.class);
...
i.putExtra("POSITION", position);
context.startActivity(i);
}
My data is too large to transfer through an intent (using Parcelable or otherwise) so I'm using a singleton to hold and transfer my list, here's the code:
public class DataTransferer {
private static volatile DataTransferer singleton;
private List<Thing> items;
public static DataTransferer get(){
if (singleton == null) {
synchronized (DataTransferer.class) {
singleton = new DataTransferer();
}
}
return singleton;
}
private DataTransferer(){
}
public void storeItems(List<Thing> items){
this.items = items;
}
public List<Thing> getStoredItems(){
return items;
}
}
In the second activity I set the adapter and retrieve the list like so:
feed = DataTransferer.get().getStoredItems();
final int position = this.getIntent().getIntExtra("POSITION", 0);
adapter = new FeedPagerAdapter(SlideActivity.this, feed);
viewPager.setAdapter(adapter);
adapter.notifyDataSetChanged();
viewPager.setCurrentItem(position);
viewPager.setOffscreenPageLimit(4);
And finally here's in my PagerAdapter code:
public class FeedPagerAdapter extends PagerAdapter {
#BindView(R.id.item_image_view) ImageView image;
private final SlideActivity host;
private List<Thing> items;
public FeedPagerAdapter(SlideActivity host, List<Thing> items){
this.host = host;
this.items = items;
notifyDataSetChanged();
}
#Override
public Object instantiateItem(ViewGroup parent, int position) {
View view = LayoutInflater.from(host).inflate(R.layout.item, parent, false);
ButterKnife.bind(this, view);
...
parent.addView(view);
return view;
}
#Override
public int getCount() {
return items.size();
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
I've tried notifying the dataset in onResume and onPause and getItemCount in the adapter also, same problem.
Back to the main activity, this data is loaded over the network and adds items to the list when the load finishes. If I start my application and click on the item as soon they start to populate the RecyclerView, it opens the second activity and I get the crash. If I start the app and wait a second and click a RecyclerView item, it works as intended.
If anyone has any suggestions on how I can wait to make sure the list is stable when starting the second activity or a better way to implement a grid based RecyclerView gallery to open a viewpager type layout with the same dataset I would really appreciate it.
It's because you are using this line viewPager.setOffscreenPageLimit(4);, It's mean viewpager won't re-create the screen in all 4 pages. However, there is a function to detect if your screen has been visible completely, it's call setUserVisibleHint(). You just need to use like below:
//For the Fragment case
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
if (isVisibleToUser) {
//TODO notify your recyclerview data over here
}
}
EDIT:
For the Activity case: If targeting API level 14 or above, one can use
android.app.Application.ActivityLifecycleCallbacks
public class MyApplication extends Application implements ActivityLifecycleCallbacks {
private static boolean isInterestingActivityVisible;
#Override
public void onCreate() {
super.onCreate();
// Register to be notified of activity state changes
registerActivityLifecycleCallbacks(this);
....
}
public boolean isInterestingActivityVisible() {
return isInterestingActivityVisible;
}
#Override
public void onActivityResumed(Activity activity) {
if (activity instanceof MyInterestingActivity) {
isInterestingActivityVisible = true;
}
}
#Override
public void onActivityStopped(Activity activity) {
if (activity instanceof MyInterestingActivity) {
isInterestingActivityVisible = false;
}
}
// Other state change callback stubs
....
}
Register your application class in AndroidManifest.xml:
<application
android:name="your.app.package.MyApplication"
android:icon="#drawable/icon"
android:label="#string/app_name" >
Add onPause and onResume to every Activity in the project:
#Override
protected void onResume() {
super.onResume();
MyApplication.activityResumed();
}
#Override
protected void onPause() {
super.onPause();
MyApplication.activityPaused();
}
In your finish() method, you want to use isActivityVisible() to check if the activity is visible or not. There you can also check if the user has selected an option or not. Continue when both conditions are met.

Java error api level method

I get an error
java.lang.NoSuchMethodError: android.app.Activity.isDestroyed
It has something to do with only running on devices which are api 17 or higher, is there anyway around this?
private WeakReference<Activity> mActivityRef;
#Override
public void onFinish() {
Activity activity = mActivityRef.get();
// if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
//do your thing!
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
activity.startActivity(new Intent(activity, ListOfAlarms.class));
activity.finish();
}
mStarted = false;
// }
// Intent goBack = new Intent(CountDownAct.this, ListOfAlarms.class);
// startActivity(goBack);
// finish();
}
This API was added only in API Level 17 Check this!
To avoid the crash, you can verify with the following code
if(Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1){
//do your thing!
}
To get it working on lower API levels, you can create your own BaseActivity.java and add code like this
public class BaseActivity extends Activity {
private boolean mDestroyed;
public boolean isDestroyed(){
return mDestroyed;
}
#Override
protected void onDestroy() {
mDestroyed = true;
super.onDestroy();
}
}
Now, make all your activities extend this BaseActivity as follows
public class MainActivity extends BaseActivity {
...
}
Hope this helps! Please accept this answer if it worked for you :)
EDIT (After OP added code snippets)
First create a BaseActivity.java file as I have shown above.
Then, make all your activities extend BaseActivity instead of Activity.
Now, change
private WeakReference<Activity> mActivityRef;
to
private WeakReference<BaseActivity> mActivityRef;
And change
Activity activity = mActivityRef.get();
to
BaseActivity activity = mActivityRef.get();

Sensormanager on fragment (add if visible; remove if invisible)

I want to have a sensormanager on a fragment, which is only active when the fragment is active. If the user changes the fragment, the listener should be removed.
Adding and removing the listener is pretty simple. I'm not aware of any listeners / function on the fragment side, when the fragment appears / disappears. Also a problem was, that on almost all functions, this.getActivity() returned a null pointer.
That's my solution. I tried to cut it out of my Fragment. If there is anything wrong / syntax issues, please let me know.
public class MyFragment extends Fragment implements SensorEventListener {
private SensorManager mSensorManager;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSensorManager = (SensorManager) this.getActivity().getSystemService(Activity.SENSOR_SERVICE);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.mylayout, container, false);
return rootView;
}
#Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0], y = event.values[1];
}
#Override public void onAccuracyChanged(Sensor sensor, int accuracy) { }
#Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
// First starts (gets called before everything else)
if(mSensorManager == null) {
return;
}
if(menuVisible) {
this.registerSensorListener();
} else {
this.unregisterSensorListener();
}
}
#Override
public void onStart() {
super.onStart();
if(this.getUserVisibleHint()) {
this.registerSensorListener();
}
}
#Override
public void onStop() {
super.onStop();
this.unregisterSensorListener();
}
private void registerSensorListener() {
mSensorManager.registerListener(this, mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0), SensorManager.SENSOR_DELAY_FASTEST);
}
private void unregisterSensorListener() {
mSensorManager.unregisterListener(this);
}
}
Hold a reference of Activity in your fragment to handle that nullpointerexception.
Here is an example of a fragment.
public class YourFragment extends Fragment {
private Activity mActivity;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}
#Override
public void onResume() {
super.onResume();
// BIND sensor here with mActivity,
// could also be done in other fragment lifecycle events,
// depends on how you handle configChanges
}
#Override
public void onPause() {
super.onPause();
// UNBIND sensor here from mActivity,
// could also be done in other fragment lifecycle events,
// depends on how you handle configChanges
}
}
Debug that code do determine if you should handle the binding there or in another method e.g. onCreate of a fragment. I have not tested this code for your purpose.
Edit:
This is indeed as commented below a dirty fix and could easily resolve into exceptions in some cases. I just wanted to show how you can use fragment lifecycle methods to bind and unbind sensors with a reference to activity. I'm currently learning fragments for quite some time but still don't understand them thoroughly. I advice you to take a look at the source of Fragment and other components involved. This is the only place were fragments are documented thoroughly hence the documentation on reference in my opinion isn't that explanatory.
Some of the options regarding null value Activity:
If you want to be completely sure that getActivity doesn't return null you should wait for onActivityCreated to be called. This method tells the fragment that its activity has
completed its own Activity.onCreate(). After this getActivity() will not return null until initState() gets called by the FragmentManager.
// Called by the fragment manager once this fragment has been removed,
// so that we don't have any left-over state if the application decides
// to re-use the instance. This only clears state that the framework
// internally manages, not things the application sets.
void initState() {
mIndex = -1;
mWho = null;
mAdded = false;
mRemoving = false;
mResumed = false;
mFromLayout = false;
mInLayout = false;
mRestored = false;
mBackStackNesting = 0;
mFragmentManager = null;
mActivity = null;
mFragmentId = 0;
mContainerId = 0;
mTag = null;
mHidden = false;
mDetached = false;
mRetaining = false;
mLoaderManager = null;
mLoadersStarted = false;
mCheckedForLoaderManager = false;
}
Before you call getActivity you can always check if activity isn't null by calling isAdded() method. As you can see below this method checks if mActivity isn't null. Optionally you can create a recursive function with Handler.postDelayed that tries to return an non null Activity in intervalls (you should add a max try counter). But this is also a dirty trick.
//Return true if the fragment is currently added to its activity.
final public boolean isAdded() {
return mActivity != null && mAdded;
}

Categories

Resources