0

Before marking the question as duplicate, read my issue first.

I have map fragment that execute a lot of process using firebase realtime database and the issue that I'm facing is that some callbacks do intensive processing which makes the map very laggy and sometimes crashes the app, so I made a new thread to execute that callback and inside it there's some code that update the UI, so I run it using runOnUiThread.

Everything worked fine once I open the fragment for the first time, after I press back and reopen it again, getActivity keeps coming null always!

I tried this famous workaround

FragmentActivity mActivity;

 @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    mActivity = (FragmentActivity) context;
  }

  @Override
  public void onDetach() {
    super.onDetach();
    mActivity = null;
  }

And used mActivity instead of getActivity , but it didn't work..! mActivity keeps coming as null also.!

I don't know why this error happens! when I open the fragment again it added again on the backstack and attached again so the activity shouldn't be null, and why it worked when added on the first time launch only?!

The Code

  void updateStudents() {

    if (isRecursionEnable) {
      return;
    }

    isRecursionEnable = true;
    thread = new Thread(new Runnable() {
      @Override
      public void run() {

        if (!thread.isInterrupted()) {
          studentQuery.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(final DataSnapshot snapshot, String s) {
              Student_User.add(snapshot.getValue(TestUserStudent.class));
              if (getActivity() != null) { // NPE
                getActivity().runOnUiThread(new Runnable() {
                  @Override
                  public void run() {
                    showAllMarkers(snapshot);  // show custom markers on the map
                  }
                });
              }
            }

            @Override
            public void onChildChanged(final DataSnapshot snapshot, String s) {
              Student_User.add(snapshot.getValue(TestUserStudent.class));
              if (getActivity() != null) {
                getActivity().runOnUiThread(new Runnable() {
                  @Override
                  public void run() {
                    showAllMarkers(snapshot);
                  }
                });
              }
            }

            @Override
            public void onChildRemoved(DataSnapshot snapshot) {
            }

            @Override
            public void onChildMoved(DataSnapshot snapshot, String s) {
            }

            @Override
            public void onCancelled(DatabaseError error) {
            }

          });
        }

      }

    }, "");
    thread.start();

  }
ReyAnthonyRenacia
  • 17,219
  • 5
  • 37
  • 56
Alaa AbuZarifa
  • 1,171
  • 20
  • 39
  • From attached code I can only conclude that you must be running it inside the fragment that is not attached to anything yet. But overall there is not enough information to say anything else. – M. Prokhorov Apr 10 '18 at 14:40
  • Please check this answer. I think it can be the best solution. Please try https://stackoverflow.com/questions/11631408/android-fragment-getactivity-sometimes-returns-null/32757648#32757648 – Däñish Shärmà Apr 10 '18 at 14:43
  • well, do you need any more info.! I call this method inside `onActivityCreated`. – Alaa AbuZarifa Apr 10 '18 at 14:43
  • Hard to tell what exactly went wrong but I'm certain that if `getActivity()` returns null, that means the fragment is detached. It's either not yet attached, or already detached. Instead of trying to synchronize the data loading with the fragment attachment it's better to separate them. Load data when you need to and store it where it's convenient to do so. Then throw an event that the loading has finished. When fragment is attached, it will catch the event and update the UI. If not, it can take the data from where you stored it. Thus, you avoid this callback dependency which causes problems. – Gennadii Saprykin Apr 10 '18 at 14:51
  • @DäñishShärmà didn't work also, once the thread gets executed, the activity just be become null always.! – Alaa AbuZarifa Apr 11 '18 at 09:45
  • @GennadiiSaprykin hmmm.. well, I almost get what you said, but the data keeps updating and changing,, i don't think storing them locally is the best idea here. – Alaa AbuZarifa Apr 11 '18 at 09:47
  • @AlaaAbuZarifa ok, I got it. If the data changes often then you can use a listener mechanism. Store the data into some state and create a listener interface. Make your fragment implement that interface. In Fragment's `onResume` register it as a listener of the state, in `onPause` unregister it. This way, every time the data changes, the fragment will get the event and update the UI accordingly. There will be neither performance cost nor memory cost because the new data will override the old one. – Gennadii Saprykin Apr 11 '18 at 10:26
  • @GennadiiSaprykin this reminds me a lot of eventbus, but can you please add an simple example of what you suggesting using the code that I posted. It would be greatly appreciated. – Alaa AbuZarifa Apr 11 '18 at 10:33
  • getActivity() is null only when fragment is detached from activity. – Khemraj Sharma Apr 11 '18 at 11:01

2 Answers2

1

The issue is quite difficult to solve because the UI code is entangled with the data loading logic. I think the best solution here would be to make those things independent. This will most likely solve the problem, or at least make it much easier to find and resolve. Also will improve the feature design in general.

Here is a short code snipit as requested, just to understand my idea. I won't write the whole thing though because this is beyond the question.

Create a state class, e.g. StudentState. The class will also provide a listener interface that will send updates to the fragment.

public class StudentState {
    // You might want to store Student_User here instead
    // I'm not sure how your data is currently represented.
    private DataSnapshot dataSnapshot;
    // Can also be a list of listeners if needed.
    private StudentStateListener studentStateListener;

    public void registerListener(StudentStateListener listener) {
        studentStateListener = listener;
    }

    public void unregisterListener() {
        studentStateListener = null;
    }

    ...

    public void setDataSnapshot(DataSnapshot dataSnapshot) {
        this.dataSnapshot = dataSnapshot;
        if (studentStateListener != null) {
         studentStateListener.onDataSnapshotLoadFinished(dataSnapshot);
        }
    }

    public interface StudentStateListener {
        void onDataSnapshotLoadFinished(DataSnapshot dataSnapshot);
    }
}

Implement it in your Fragment:

public class StudentFragment implements StudentStateListener {

    // The state can be initialized in multiple ways.
    // It can be a singleton, or stored in Application class, or
    // defined in your base activity, etc. Up to you. Ideally
    // should be injected via dependency injection if you use one, such as Dagger.
    private StudentState state;

    @Override
    public void onCreate() {
       // initialize state
       // ...
    }

    @Override
    public void onResume() {
        state.registerListener(this);
        // If the data has already been loaded you might also want the following.
        // It's up to you.
        if (state.getDataSnapshot() != null) {
           showAllMarkers(state.getDataSnapshot());
        }
    }

    @Override
    public void onPause() {
        state.unregisterListener();
    }

    ...

    @Override
    public void onDataSnapshotLoadFinished(DataSnapshot dataSnapshot) {
        if (!isAdded()) {
            return;
        }
        showAllMarkers(snapshot);
    }

    public void updateStudents() {
        // I'm not quite sure how to work with your API but basically the idea is to load a new data snapshot and store it in the state
        // This should happen in background thread.
        studentQuery.loadDataSnapshot(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot) {
                state.setDataSnapshot(dataSnapshot);
            }
        });
    }
}

I hope this helps.. Good luck!

Gennadii Saprykin
  • 4,505
  • 8
  • 31
  • 41
0

This problem shows some dependency issues in your app. Perhaps you should find some other way to implment what you want.

Anyway, take a look at:

.setRetainInstance(true)

https://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)

This is not ideal, and you should avoid it. But it might solve your problem if nothing else works.

Hope it helps.

Luís Henriques
  • 604
  • 1
  • 10
  • 30
  • I called this inside `onActivityCreated` but nothing changed.. once the thread gets executed, the activity just be become null always.! – Alaa AbuZarifa Apr 11 '18 at 09:48
  • Why are you setting mActivity to null onDetach()? Is there a special reason for it? If not, try removing it. – Luís Henriques Apr 11 '18 at 10:43
  • no that's just a workaround that I've tried to solve the problem , but didn't work. https://stackoverflow.com/a/18078475/7801361 – Alaa AbuZarifa Apr 11 '18 at 10:46
  • What if you getActivity()? Does it return anything? If so, just (FragmentActivity) getActivity(). – Luís Henriques Apr 11 '18 at 11:09
  • if it's null it means that fragment is detached from activity and I can't use it to update the UI, so my question is how to make the getActivity come not null always! – Alaa AbuZarifa Apr 11 '18 at 11:20
  • Yeah, it was a stupid question. I'm kinda between things and trying to help you at the same time. I'm out of ideas though. Sorry. – Luís Henriques Apr 11 '18 at 11:25