1

I recently replaced all the deprecated AsyncTask code in my apps with handlers and newSingleThreadExecutors. After retrieving response data from a remote server, I update the UI in the handler.post section of the code.

I've never personally been able to reproduce any problems with this, but on some devices (mostly oppo's, redmi's, vivo's, etc) under some real-world conditions, getView() returns null and my stop-gap attempt to re-inflate the view fails. The number of crashes has increased by a lot:

Exception java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object android.content.Context.getSystemService(java.lang.String)' on a null object reference

Rough outline of my code:

public class ResultFragment extends Fragment {
    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.result, container, false);
    }

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        Bundle bundle = this.getArguments();
        assert bundle != null;
        String query_url = bundle.getString("query_url");
        send_request(query_url);
    }

    void send_request(String... urls) {
        Handler handler = new Handler(Looper.getMainLooper());
        Executors.newSingleThreadExecutor().execute(() -> {
        .....
            handler.post(() -> {
                context = getContext();

                final TextView mTextView;
                final WebView mWebView;

                if (getView() != null) {
                    mTextView = getView().findViewById(R.id.count);
                    mWebView = getView().findViewById(R.id.result);
                } else {
                    View view = LayoutInflater.from(context).inflate(R.layout.result, null); <-- crash
                    mTextView = view.findViewById(R.id.count);
                    mWebView = view.findViewById(R.id.result);
                }

My understanding from the lifecycle documentation is that I should be able to get the view with this code. And I do understand that trying to re-inflate the code like this is a dangerous proposition (crashes might occur!). But how do I do so when getView() returns null?

As I say, I've never been able to replicate these crashes. So I'm open to trying anything that might work.

For general information, I'm targeting sdk version 33.

2 Answers2

1

Your code will crash if the fragment view (or the entire Fragment instance) is destroyed as a result of user leaving your app or screen. A quick fix for your issue is to cancel the handler runnable execution when the fragment view is destroyed.

@Override
public void onDestroyView() {
    super.onDestroyView();
    handler.removeCallbacksAndMessages(null);
}

For future development of your app you should look over more advance API to deal with this sort of issue. Ex: RxJava, Kotlin coroutines to name a few.

Small tip that will probably help you to reproduce the crash on any device/emulator - activate the developer option: "Don't keep activity", press the button that makes the network request and then immediately close the application. The thread will post a runnable that will execute after the fragment/view-fragment is destroyed -> NPE crash.

elvisrusu
  • 378
  • 5
  • 18
  • Thanks for all the help! You are right about this, and I was finally able to make the app crash. Adding the onDestroyView() and handler.removeCallbacksAndMessages(null); seems to prevent crashes. I don't have enough points to vote officially, but I still voted up your answer. – user2271541 Feb 07 '23 at 15:25
0

You wouldn't do this at all. If you don't have a view, reinflating it isn't going to do what you expect. It would, at best, create a new set of views that are in memory only and not displayed on the screen. In other words it would be pointless.

Also, that's not what your problem is. You problem is that the context is null. Your fragment isn't attached to any. In this case, what you probably want to do is update any persisted state (if any) and skip updating the UI.

Also, if you're inflating your UI normally on an excutor that then posts to a handler- stop. THat's not how it works. THe inflation should happen in the onCreateView function. You can fill in the views like that, but you would NEVER inflate them like that.

Gabe Sechan
  • 90,003
  • 9
  • 87
  • 127
  • Thanks for the reply. In my code example, I left out the onAttach where I do in fact attach the context to the fragment. Note the ellipses. I have verified that, in general, the context is not in fact null and that the getView() statement works. But as I say, sometimes getView() is null. I don't know why. What do I do in that case? – user2271541 Feb 06 '23 at 21:12
  • getView() will return null if the Fragment view was destroyed, there is not much you can do at this point, the view is no longer visible. what you should do is to avoid the crash using ugly null checks or by canceling the runnable execution from Handler. Any other attempt on performing layout inflation at this point will fail as it should - why recreate view when User clearly left your app/screen. – elvisrusu Feb 06 '23 at 22:03