I have an app that uses the single activity and multiple fragments approach and I use the NavController for navigating. Unfortunately, when navigating to a Fragment that contains a Runnable
in an anymous class, two identical instances of this Fragment are being created and I don't understand why.
Here is the code of the main activity:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
public static DB_SQLite_Helper sqLite_DB;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
getSupportActionBar().hide();
setContentView(view);
sqLite_DB = new DB_SQLite_Helper(this);
}
}
The Home-Fragment in the nav_graph is the Fragment FR_Menu
that you can see here:
public class FR_Menu extends Fragment implements View.OnClickListener{
private FragmentMenuBinding binding;
public FR_Menu() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMenuBinding.inflate(inflater, container, false);
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
binding.buttonExit.setOnClickListener(this);
binding.buttonTest.setOnClickListener(this);
return binding.getRoot();
}
@Override
public void onClick(View view) {
if(view.getId() == R.id.button_test) {
Navigation.findNavController(getView()).navigate(FR_MenuDirections.actionFRMenuToTest());
}
if(view.getId() == R.id.button_exit) {
getActivity().finishAndRemoveTask();
}
}
}
Here I just have a OnClickListener
and navigate to the Fragment with the Runnables by using the navController in the line Navigation.findNavController(getView()).navigate(FR_MenuDirections.actionFRMenuToTest());
. So far, so good. Now the Fragment with the Runnable, called Test
is created. Here you see the code of this Fragment:
public class Test extends Fragment {
private Handler handler = new Handler();
int helpCounterRun =0;
private boolean viewHasBeenCreated = false;
private FragmentTestBinding binding;
public Test() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentTestBinding.inflate(inflater, container, false);
viewHasBeenCreated = true;
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
countDownTime();
return binding.getRoot();
}
private void updateScreen() {
Log.e("LogTag", "Method updateScreen - this: " this);
}
private void countDownTime(){
handler.postDelayed(new Runnable() {
@Override
public void run() {
helpCounterRun ;
Log.e("LogTag", "Method run - helpCounterRun: " helpCounterRun);
Log.e("LogTag", "Method run - this: " this);
if(viewHasBeenCreated) {
countDownTime();
}
}
}, 100);
updateScreen();
}
}
Next to the onCreate
and onCreateView
method this Fragment has 2 basic methods. In the updateScreen
method, the current Fragment is printed to the console. And in the countDownTime
method a Runnable is created and an auxilliary variable helpCounterRun
is incremented. The value of the auxillary variable and the current instance of the Runnable are printed to the console. The output looks like this:
2022-04-18 10:01:33.742 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 1
2022-04-18 10:01:33.743 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@78ea3f9
2022-04-18 10:01:33.745 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.277 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 1
2022-04-18 10:01:34.278 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@9c893ee
2022-04-18 10:01:34.278 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.294 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 2
2022-04-18 10:01:34.305 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@f8db08f
2022-04-18 10:01:34.306 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 2
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@8b9ef1c
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.414 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 3
2022-04-18 10:01:34.414 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@9ad8725
2022-04-18 10:01:34.415 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.503 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 3
2022-04-18 10:01:34.503 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@9ae00fa
2022-04-18 10:01:34.504 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.531 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 4
2022-04-18 10:01:34.562 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@ebec6ab
2022-04-18 10:01:34.562 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 4
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test$1@b04e108
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
What you can see from the output of the updateScreen
method, that 2 instances of this Fragment are created and are running simultaneously. One has the id Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)
and the other Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
and thus the auxillary variable helpCounter
is printed out 2 times with the same value before being incremented.
My question is why is this happening. I don't see any part of my code that explicity created 2 instances of the Fragment Test
. Do you have any idea what the cause of this strange behaviour might be and how I can tackle it?
Reminder: Does nobody have an idea why this is happening?
CodePudding user response:
When you call setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
, you force your activity to go through a configuration change - changing from portrait to landscape orientation. By default, Android is going to destroy your activity (and all fragments in it) and recreate it in your requested orientation.
This is why you are getting a message Method updateScreen - getActivity(): null
- that Fragment has been completely destroyed along with the activity it is in because of your configuration change.
However, you never stop calling countDownTime()
over and over again even after your fragment's view is destroyed. This means you've created a permanent memory leak.
You're already tracking whether the fragment's view is created via your viewHasBeenCreated
, but you never set it back to false
- you'd want to override onDestroyView()
and use that as the signal that your View has been destroyed. This is also the appropriate place to remove any postDelayed
calls that haven't yet run by using removeCallbacksAndMessages()
@Override
public void onDestroyView() {
super.onDestroyView();
// Reset your variable to false
viewHasBeenCreated = false;
// And clean up any postDelayed callbacks that are waiting to fire
handler.removeCallbacksAndMessages(null);
}
Note that you don't need to manually track viewHasBeenCreated
- you can use getView() != null
to do this same check, but by making sure you actually clean up your Handler
in onDestroyView()
, you won't need to do this check at all as you'll be guaranteed to only be running while your view is created.