1

Background & Problem:

I have a custom Fragment class. I reuse this fragment class for multiple status displays. In the parent Activity, I load the fragment and call a method on the fragment to set some text, etc. When I do this, I hit a NullPointerException. The reason is onCreateView() has not yet been called, i.e. the view doesn't exist, thus accessing a TextView to call .setText(String) will ofcourse run into a NullPointerException.

Fragment Class

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.example.app.R;

public class CopyStaticFragment extends Fragment {

    private String initialActionText, initialSecondaryText, initialTitleText;
    private TextView textViewTitle, textViewSecondary;
    private Button btnAction;
    private View.OnClickListener actionCallback;

    public void setActionButtonListener(View.OnClickListener actionCallback) {
        this.actionCallback = actionCallback;
    }

    public CopyStaticFragment() {
    }

    public CopyStaticFragment(View.OnClickListener actionCallback) {
        this.actionCallback = actionCallback;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View layout = inflater.inflate(R.layout.fragment_main_status, container, false);

        textViewTitle = layout.findViewById(R.id.textViewTitle);
        textViewTitle.setText(initialTitleText);
        textViewSecondary = layout.findViewById(R.id.textViewSecondary);
        textViewSecondary.setText(initialSecondaryText);
        btnAction = layout.findViewById(R.id.btnAction);
        btnAction.setText(initialActionText);
        btnAction.setOnClickListener(v -> {
            if(actionCallback != null) {
                actionCallback.onClick(v);
            }
        });

        return layout;
    }

    public void setPrimaryText(String text) {
        textViewTitle.setText(text);
    }

    public void setSecondaryText(String text) {
        textViewSecondary.setText(text);
    }

    public void setActionText(String text) {
        btnAction.setText(text);
    }

    public void setInitialActionText(String text) {
        this.initialActionText = text;
    }

    public void setInitialSecondaryText(String text) {
        this.initialSecondaryText = text;
    }

    public void setInitialTitleText(String text) {
        this.initialTitleText = text;
    }

}

Somewhere in my main activity, as a callback response from a Service status, I will do something like:

if (instance.getUnbackedUpCount() > 0) {
    // Load first fragment - i.e. we can backup content
    getSupportFragmentManager().beginTransaction()
            .replace(R.id.frameCurrentStatus, copyStartFragment)
            .commit();
    capacityControlFragment.setInfoIconVisible(true);
} else {
    getSupportFragmentManager().beginTransaction()
            .replace(R.id.frameCurrentStatus, copyFinishFragment)
            .commit();
}

where copyStartFragment & copyFinishFragment are instances of the above mentioned fragment.

Note:

Depending on the Service status update interval, the call to replace() fragments can be almost instantaneous - which I figure is the actual problem.

My workaround:

Is to have initial setters for all views and their properties. e.g. initialTitleText, initialMessageText, initialActionButtonText, initialActionButtonVisibility, etc. I feel this is hack'ish and not inline with the intended design philisophy of Fragment use.

My Question

Is this a design problem on my part for encountering such an error or is there a workaround to call the onCreateView() sooner?

CybeX
  • 2,060
  • 3
  • 48
  • 115
  • 1
    `public CopyStaticFragment(View.OnClickListener actionCallback) {` unless you are also using `FragmentFactory` which you probably aren't, you should be getting a lint error that your app will crash in horrible ways if you do this. All of your `set*` methods should be passed in with `Bundle arguments` using `fragment.setArguments(Bundle)`. – EpicPandaForce Dec 22 '19 at 19:57
  • @EpicPandaForce thanks for the advice. The set* methods were only put as a temporary measure - will implement bundle if no solution to the main problem is given. Also, would you kindly explain the callback issue in the constructor? – CybeX Dec 22 '19 at 20:01
  • See https://stackoverflow.com/a/10450535/2413303 – EpicPandaForce Dec 22 '19 at 20:03
  • @EpicPandaForce that is very helpful, thank you. Also, it appears the solution mentioned in your comment above also includes this 'workaround' I am using. i.e. Setting these values before hand - so the `onCreateView()` delay appears to be an actual issue. – CybeX Dec 22 '19 at 20:08
  • @EpicPandaForce would you mind posting this as a solution with a bit more detail - need to give credit where it is due. – CybeX Dec 22 '19 at 20:23
  • 1
    you can setText in onViewCreated. – Haider Saleem Dec 22 '19 at 21:16

2 Answers2

2

You are using .commit() as part of replacing fragments.
This is done async. No way around it, it's just how that function works.

You can use .commitNow(), and it will be done sync.
By doing it like this, you should be able to access the views instantly after the function returns.

https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction#commitNow()

Moonbloom
  • 7,738
  • 3
  • 26
  • 38
  • ah yes, `commitNow()`. It seems like this is not recommended if there are other pending transactions for the fragment manager. I will rather settle for @EpicPandaForce's solution in the question comment section as a safer option. – CybeX Dec 22 '19 at 20:22
0

Pass the initial strings to the fragment using the recommended way to initialise a fragment:

public static CopyStaticFragment newInstance(String initialString....) {
    CopyStaticFragment fragment = new CopyStaticFragment();
    Bundle args = new Bundle();
    args.putString(INITIAL_STRING, initialString);

    fragment.setArguments(args);
    return fragment;
}

Then in onCreate:

 @Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        mInitialString = getArguments().getString(INITIAL_STRING);
    }
}

Then when you set your TextView text in OnViewCreated the string and any other parameters are available.

Initialise your fragment in the activity using:

CopyStaticFragment.newInstance("initial string",....);

Side note: onCreateView should only be used to inflate your layout resource, use OnViewCreated to initialise your views after the layout has been inflated.

Stay as far away as you can from commitNow() and popBackStackImmediate() unless you love IllegalStateException

MichaelStoddart
  • 5,571
  • 4
  • 27
  • 49