Handling configuration changes when there is a background thread running - Android - java

I have a fragment that launches a background thread to do some work and uses a handler callback to update some UI when the thread finishes its task.
public class CodeFragment extends Fragment implements View.OnClickListener, Handler.Callback {
private CodeDataViewModel codeDataViewModel;
private Handler uiHandler;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container,
#Nullable Bundle savedInstanceState){
View view = inflater.inflate(R.layout.fragment_code, container, false);
uiHandler = new Handler(Looper.getMainLooper(), this);
return view;
}
#Override
public boolean handleMessage(Message msg){
if(msg.what == 0){
BFResult result = (BFResult)msg.obj;
codeDataViewModel.setOutput(result);
runButton.setIcon(ResourcesCompat.getDrawable(requireActivity().getResources(), R.drawable.ic_baseline_play_arrow_24, null));
runButton.setText(getString(R.string.run));
codeDataViewModel.setShouldRun(true);
if(result.errorCode != BFErrorCodes.UserAbort){
ViewPager2 viewPager2 = getActivity().findViewById(R.id.view_pager);
viewPager2.setCurrentItem(1);
}
}
codeDataViewModel.setInterpreterThread(null);
return true;
}
The issue I have is when the screen rotates while the thread is still running, then, after the thread finishes executing and when handleMessage starts being executed I get the following exception in handleMessage at the line runButton.setIcon(...)
FATAL EXCEPTION: main
java.lang.IllegalStateException: Fragment CodeFragment{7f397b9} (ea45dd9d-5419-4d22-8f56-11c7fed9f3bd) not attached to an activity.
at androidx.fragment.app.Fragment.requireActivity(Fragment.java:928)
at com.sandeept.brainfkinterpreter.fragments.CodeFragment.handleMessage(CodeFragment.java:117)
Now, I know this is because the activity and the fragments are recreated when a configuration change occurs. And because the handleMessage function being executed is of the old fragment that is now no longer attached to the activity there is a run time error.
What I don't know is how to fix this. I thought about using setRetainInstance(true), but this has been deprecated since the introduction of view models. I don't know how view models are going to be helpful in this case.
So, how do I handle this issue?

Related

Prevent DialogFragment from sending the app into energy save mode

I'm creating an App for an Android Wearable Device.
When showing a custom layout as a dialogue fragment, the app will get minimized (getting replaced by the watch face) after about 30 seconds. I know that this is supposed to be the default behavior of apps out of touch with their users, but in my case, the app needs to stay visible even if not touched four minutes.
The Activities calling it did get these instructions inside their onCreate implementation.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
that works like a charm and in the way I want it. However, the DialogFragment that serves as an error notification (and may need to get observed without touching it for some time) does not obey this setting.
I tried to get the flag inside the DialogFragment too by placing it inside the onViewCreated calls, but it does not have the getWindow Method. While the next code segment is valid, is does not work either.
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
The way the dialog fragment is called looks like this. That's the code to call it from the FragmentActivity needing to show it. As you might notice, there is a field "activity", which is because the whole call is done from a static class outside the activity. I want to call the same DialogFragment from multiple activities, with only the text and the title being different.
public static void showDialogCuston(String title, String message, FragmentActivity activity){
ErrorDialogFragment edf = ErrorDialogFragment.newInstance(title,message);
FragmentManager fm = activity.getSupportFragmentManager();
edf.show(fm, "TAG");
}
And that's what the DialogFragment does look like inside. I kicked out all TextView text assignments because I doubt that they could offer any kind of information about that request, and just making the code fragment less readable.
public class ErrorDialogFragment extends DialogFragment {
//some private text views
public ErrorDialogFragment(){
}
public static ErrorDialogFragment newInstance(String title, String text){
ErrorDialogFragment edf = new ErrorDialogFragment();
Bundle args = new Bundle();
args.putString("title",title);
args.putString("text",text);
edf.setArguments(args);
return edf;
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_error, container, false);
// R.layout.fragment_error is the layout that serves as my custom dialog
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
//assigning layout elements to private fields
//assigning stuff from the bundle inside some textViews
closeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dismiss();
//playing a sound from SoundPool and doing haptic feedback on button press
}
});
}
}
So, all I want is that the app won't disappear by itself when one is staring too long on the DialogFragment without touching it.
In your case, I would like to suggest keeping the screen-on flag in the dialog layout file so that when the dialog view is visible, it keeps the screen on. Check the developer's documentation for more information. However, I am adding the code from the documentation for convenience.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
...
</RelativeLayout>
Set the android:keepScreenOn to true in the root element of your dialog layout. And I hope you also have the following permission in your AndroidManifest.xml file.
<uses-permission android:name="android.permission.WAKE_LOCK" />

Is there a way to call a method after building a fragment?

I want to change a Fregment and as soon as the Gui is built a method should be executed.
I have solved it so far with a thread. Only I thought that there would have to be a better way.
OnResume () is executed before the fragment is formed.
Does anyone know a way that without a thread to solve?
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
fragmentRoot = inflater.inflate(R.layout.fragment_l2c_printersettings, container, false);
fragmentRoot.setFocusableInTouchMode(true);
fragmentRoot.requestFocus();
...
...
...
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
loadPrinter();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
return fragmentRoot;
}
I think you may be looking for public void onActivityCreated(Bundle savedInstanceState) in your fragment.
Fragment lifecycle.
From the documentation
Called when the fragment's activity has been created and this fragment's view hierarchy instantiated. It can be used to do final initialization once these pieces are in place, such as retrieving views or restoring state. It is also useful for fragments that use setRetainInstance(boolean) to retain their instance, as this callback tells the fragment when it is fully associated with the new activity instance. This is called after onCreateView(LayoutInflater, ViewGroup, Bundle) and before onViewStateRestored(Bundle).
You can call the function in onViewCreated. By the time onViewCreated is called, the fragment is completely loaded.

java.lang.IllegalStateException: Fragment not attached to a context

I have a tablayout with a viewpager in my MainActivity.
My PagerAdapter looks like this:
public class MainActivityPagerAdapter extends PagerAdapter {
public MainActivityPagerAdapter(FragmentManager fm, int numOfTabs) {
super(fm, numOfTabs);
}
#Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StoresFragment();
case 1:
return new OrdersFragment();
default:
return null;
}
}
}
I am coming back from another activity like this:
Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish(); //finishAffinity();
But then I get an java.lang.IllegalStateException in one of my Fragments in the viewpager of the MainActivity.
I read many related questions and tried to solve this. It is said, that this happens when one keeps references to Fragments outside of the PagerAdapter. But I am not doing this, as you can see in my code.
Does anyone know what I am doing wrong?
Edit - Stacktrace
FATAL EXCEPTION: main
Process: com.lifo.skipandgo, PID: 23665
java.lang.IllegalStateException: Fragment OrdersFragment{42c2a740} not attached to a context.
at android.support.v4.app.Fragment.requireContext(Fragment.java:614)
at android.support.v4.app.Fragment.getResources(Fragment.java:678)
at com.lifo.skipandgo.activities.fragments.OrdersFragment$1.results(OrdersFragment.java:111)
at com.lifo.skipandgo.connectors.firestore.QueryResult.receivedResult(QueryResult.java:37)
at com.lifo.skipandgo.controllers.UserController$2.onUpdate(UserController.java:88)
at com.lifo.skipandgo.connectors.firestore.QuerySubscription.onEvent(QuerySubscription.java:59)
at com.lifo.skipandgo.connectors.firestore.QuerySubscription.onEvent(QuerySubscription.java:18)
at com.google.firebase.firestore.zzg.onEvent(Unknown Source)
at com.google.firebase.firestore.g.zzh.zza(SourceFile:28)
at com.google.firebase.firestore.g.zzi.run(Unknown Source)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:146)
at android.app.ActivityThread.main(ActivityThread.java:5653)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
at dalvik.system.NativeStart.main(Native Method)
Edit:
Interesting is, that the view has defenitely loaded when the error occurs. Because the error occurs about 10-15 seconds later after the fragment is shown again. I this in my orderFragment, where the error occurs:
orders = new QueryResult<UserOrder>(UserOrder.class) {
#Override
public void results(List<UserOrder> results) {
orderLoadingMessage.setBackgroundColor(getResources().getColor(R.color.green));
}
}
I do this in onCreateView and this result comes about 10-15 seconds after the view loaded.
The problem seems to be, that your fragment is listening to some events (via UserController and QueryResult) and these are fired before the fragment is attached to context.
Try to unregister the fragment when it becomes detached and to them again after attaching (LiveData can also help with this). Another way could be to receive and store the event while detached and only process it after attaching.
Use this before update your Activity UI :
if(isAdded())// This {#link androidx.fragment.app.Fragment} class method is responsible to check if the your view is attached to the Activity or not
{
// TODO Update your UI here
}
viewPager.offscreenPageLimit = (total number of fragments - 1)
viewPager.adapter = Adapter
Use this if your are using viewpager
And if you are using bottom navigation just simply check if(context != null)
But i suggest to use max 3 fragments in offscreenPageLimit
Some of your callbacks are being fired after your fragment is detached from activity. To resolve this issue you need to check whether your fragment is added before acting upon any callbacks. For example, change your orders object's initialization to this:
orders = new QueryResult<UserOrder>(UserOrder.class) {
#Override
public void results(List<UserOrder> results) {
if(isAdded()) {
orderLoadingMessage.setBackgroundColor(
getResources().getColor(R.color.green));
}
}
}
In my case this exception happened when I showed a DialogFragment and called it's methods. Because the fragment hasn't attached to a FragmentManager (this operation completes asynchronously) before calling methods, an application crashed.
val fragment = YourDialogFragment.newInstance()
fragment.show(fragmentManager, YourDialogFragment.TAG)
// This will throw an exception.
fragment.setCaptions("Yes", "No")
If you add the fragment with FragmentManager, you will get another exception: java.lang.IllegalArgumentException: No view found for id 0x1020002 (android:id/content) for fragment (or similar, if you use another id).
You can call fragment methods via post (or postDelayed), but it is a bad solution:
view?.post {
fragment.setCaptions("Yes", "No")
}
Currently I use childFragmentManager instead of fragmentManager:
val fragment = YourDialogFragment.newInstance()
fragment.show(childFragmentManager, YourDialogFragment.TAG)
fragment.setCaptions("Yes", "No")
I don't remember what I did, but now it works.
I had similar problem. I have solved it by following ferini's recommendation. I was using a live data which was firing before the context was attached.
Here is my full implementation
public class PurchaseOrderFragment extends Fragment {
FragmentPurchaseOrderBinding binding;
CurrentDenominationViewModel currentDenominationViewModel;
#Inject
ViewModelFactory viewModelFactory;
CurrentVoucherChangedObserver observer;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
currentDenominationViewModel = new ViewModelProvider(requireActivity(),viewModelFactory).get(CurrentDenominationViewModel.class);
observer = new CurrentVoucherChangedObserver();
}
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_purchase_order, container, false);
return binding.getRoot();
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
currentDenominationViewModel.getCurrentVoucherStatisticsLiveData().observe(requireActivity(), observer);
}
#Override
public void onAttach(#NonNull Context context) {
AndroidSupportInjection.inject(this);
super.onAttach(context);
}
#Override
public void onDetach() {
super.onDetach();
currentDenominationViewModel.getCurrentVoucherStatisticsLiveData().removeObserver(observer);
}
final class CurrentVoucherChangedObserver implements Observer<VoucherStatistics> {
#Override
public void onChanged(VoucherStatistics x) {
String denomination = x.getDenomination()+"";
binding.tvDenomination.setText(denomination);
String stockAmount = requireContext().getResources().getString(R.string.StockAmount);
String text= "("+String.format(stockAmount,x.getQuantity()+"")+")";
binding.tvInStock.setText(text);
}
}
}
Your Solution
change your getItem() method to
switch (position) {
case 0:
return new StoresFragment();
case 1:
return new OrdersFragment();
default:
return null;
}

Proper way of communication between fragments

Is this a proper way of communication between fragments ?
public class MainActivity extends AppCompatActivity implements IFragmentsHandler {
#Override
protected void onCreate(Bundle savedInstanceState) {
//...
}
#Override
protected void startFragment1() {
Fragment1 f1 = new Fragment1();
f1.setFragmentsHandler(this);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, f1)
.commit();
}
#Override
protected void startFramgment2() {
Fragment1 f2 = new Fragment1();
f2.setFragmentsHandler(this);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, f2)
.commit();
}
}
public class Fragment1 {
private IFragmentsHadnler fragmentsHandler;
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment1, container, false);
//...Code...
fragmentsHandler.startFragment1();
}
public void setFragmentsHandler(IFragmentsHandler fragmentsHandler) {
this.fragmentsHandler = fragmentsHandler;
}
}
public class Fragment2 {
private IFragmentsHadnler fragmentsHandler;
#Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment2, container, false);
//...Code...
fragmentsHandler.startFragment2();
}
public void setFragmentsHandler(IFragmentsHandler fragmentsHandler) {
this.fragmentsHandler = fragmentsHandler;
}
}
[EDIT1] : Posted the Interface (though it was obvious)
public interface IFragmentsHandler {
public void startFragment1();
public void startFragment2();
}
From my Java perspective this will throw OutOfMemoryError but I'm not if it is the same for the Android. Anyway what is the preferred way of communication between fragments?
According to android developer guide, communication between fragments is done through the associated Activity.
A fragment use his interface to communicate with the Activity. And the Activity deliver a message by capturing the Fragment instance with findFragmentById() or creating one if needed, then directly call the other fragment's public methods.
Fragment1 wants to pass some data: uses his interface method implemented by Activity.
Activity executes that method receiving the data, create&replace or find (depending on your layout) the Fragment2 and pass this data or execute some public method on fragment2 class (depending on your layout).
Fragment2 extract data from bundle or execute (depending on your layout) his public method to receive the data.
I think the problem in your code is you are misunderstanding interface purpose. You are using for start the same fragment who is calling the method. Fragment1 is calling startFragment1() in his onCreateView(), but it is already started.
If you needed, in here there is a good tutorial.
To communicate between components consider app architecture MVP, VIPER, etc. On code side it may use event bus for communication or just plain callbacks.
Do navigation in one place
Do business logic in another place
Do present-view logic in presenter
Do view logic in views, fragments, adapters
As you started, you can use interfaces to communicate between Fragments as suggested by Google.
But an easy way to communicate between fragments is by using event bus (which implements the publish/subscribe pattern) like EventBus library.
You can also use RxJava to create your own event bus and thus make communications between components of your app (have a look to this Stackoverflow question: RxJava as event bus?)

How did I stop IllegalStateExceptions from being thrown with LayoutInflater().inflate(int resource, ViewGroup root, boolean attachToRoot)?

I recently recently ran into an issue while setting up a viewpager with 3 fragments. When the application ran it crashed with a
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first
Here is the code for creating list list of fragments to pass to the pageAdapter.
private Vector<Fragment> getFragments() {
Vector<Fragment> list = new Vector<Fragment>();
list.add(new Fragment1());
list.add(new Fragment2());
list.add(new Fragment3());
return list;
Each of the fragments were essentially the same except for a being created with different layouts. Here was the original code I had for one of the fragments.
public class Fragment1 extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = getActivity().getLayoutInflater().inflate(R.layout.fragment1, container);
return v;
}
}
But when I ran it like this it kept crashing with the IllegalStateException. I found that the issue was coming from the fragments being created. After some Googling I tried changing the code for the fragment to this.
public class Fragment1 extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = getActivity().getLayoutInflater().inflate(R.layout.fragment1, container, false);
return v;
}
}
This resolved the issue and I no longer got the IllegalStateException except I have no idea how this worked. What exactly does this boolean do? And what does this exception mean? I had tried adding the method call like it suggusted but that did not resolve it. Also I tried changing this boolean to true and I got the same error again. The android docs say its attachToRoot but isn't that what I want to do? Attach my 3 fragments to the rootview, which is the viewpager? If anyone could explain this it would be greatly appreciated.
The boolean parameter of the 3-arg version of LayoutInflater.inflate() determines whether the LayoutInflater will add the inflated view to the specified container. For fragments, you should specify false because the Fragment itself will add the returned View to the container. If you pass true or use the 2-arg method, the LayoutInflater will add the view to the container, then the Fragment will try to do so again later, which results in the IllegalStateException.

Categories