Reputation: 370
Im concerned regarding the information coming back from leak canary. Its showing that all variables declared on the UI such as material buttons, material card views, textviews, imageviews etc in the fragment are causing memory leaks. I'm not sure why this is happening.
For example leak canary will pick up 1 memory leak say in the Material button. When I declare that material button as null in the onDestroyView() that fixes it. But then leak canary will bring up the next UI variable and I literally have to declare every variable in the UI as null in the onDestroyView() in order to stop that fragment leaking.
Surely it wouldnt be normal practice to have to null all declared variables in the ondestroyView() method, that would be so tideous. I though android would take care of these things when we navgiate to a new fragment.
private Window mWindow;
private Toolbar mToolbar;
private FloatingActionButton btnBack, btnNext;
private Button btnStart;
private TextView tvSetUpNotifications, tvTitle, tvDescription;
private ImageView ivToolbar;
private CardView cvNotification;
private Bundle args = new Bundle();
private NavController mNavController;
private View view;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
((BaseApplication) getActivity().getApplication()).getAppComponent().inject(this);
}
public WorkoutCheckInIntro() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
view = inflater.inflate(R.layout.fragment_workout_checkin_intro, container, false);
// configure the Window variable to enable the colour to be set
mWindow = getActivity().getWindow();
mWindow.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
mWindow.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
mWindow.setStatusBarColor(ContextCompat.getColor(getActivity(), R.color.calm));
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Instantiate the Navigation Controller.
mNavController = Navigation.findNavController(view);
// Configure the bottom navbar
BottomNavigationView navBar = getActivity().findViewById(R.id.bottom_navigation);
navBar.setVisibility(View.VISIBLE);
// Method calls
assignVariables();
setOnClickListeners();
}
/**
* Method which assigns variables to elements in the XML file.
*/
private void assignVariables() {
// Initialising variables from the xml file.
btnStart = view.findViewById(R.id.checkInDailyButtonStart);
tvSetUpNotifications = view.findViewById(R.id.setUpNotificationsTextView);
cvNotification = view.findViewById(R.id.notification_cardView2);
// Configure the top toolbar
mToolbar = view.findViewById(R.id.toolbar_check_in_intro);
btnBack = mToolbar.findViewById(R.id.toolbar_back_button);
btnNext = mToolbar.findViewById(R.id.toolbar_next_button);
btnNext.hide();
tvTitle = mToolbar.findViewById(R.id.toolbar_workout_title);
tvTitle.setVisibility(View.VISIBLE);
tvTitle.setTextColor(ContextCompat.getColor(getActivity(), R.color.white));
tvTitle.setText(getString(R.string.check_in));
tvDescription = mToolbar.findViewById(R.id.toolbar_workout_description);
tvDescription.setVisibility(View.GONE);
tvDescription.setTextColor(ContextCompat.getColor(getContext(), R.color.white));
ivToolbar = mToolbar.findViewById(R.id.toolbar_workout_background);
ivToolbar.setVisibility(View.VISIBLE);
ivToolbar.setImageResource(R.drawable.check_in_intro);
}
/**
* Method which sets onClickListeners to buttons
*/
private void setOnClickListeners() {
btnStart.setOnClickListener(this);
btnBack.setOnClickListener(this);
tvSetUpNotifications.setOnClickListener(this);
cvNotification.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.toolbar_back_button:
if (mNavController.getCurrentDestination().getId() == R.id.workoutCheckInIntro) {
mNavController.navigate(R.id.action_workoutCheckInIntro_to_workoutFragment);
}
break;
case R.id.checkInDailyButtonStart:
// Boolean used in the WorkoutCheckInDailyMood.Class so when the user clicks
// the back button it will return them to this activity.
args.putBoolean(WORKOUT_CHECKIN_DAILY_MOOD, true);
if (mNavController.getCurrentDestination().getId() == R.id.workoutCheckInIntro) {
mNavController.navigate(R.id.action_workoutCheckInIntro_to_workoutMood, args);
}
break;
case R.id.notification_cardView2:
// Boolean used in the NotificationsSetUp.Class so when the user clicks
// the back button it will return them to this activity.
args.putBoolean(RETURN_TO_CHECKIN_WORKOUT, true);
if (mNavController.getCurrentDestination().getId() == R.id.workoutCheckInIntro) {
mNavController.navigate(R.id.action_workoutCheckInIntro_to_notificationsSetUp, args);
}
break;
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mToolbar = null;
view = null;
btnBack = null;
btnNext = null;
tvDescription = null;
tvTitle = null;
ivToolbar = null;
btnStart = null;
cvNotification = null;
tvSetUpNotifications = null;
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:fillViewport="true"
tools:context=".ui.workouts.checkin.WorkoutCheckInIntro">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<include
android:id="@+id/toolbar_check_in_intro"
layout="@layout/toolbar_workout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="20dp"
android:text="Workout Length"
android:textSize="@dimen/text_size_heading_16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_check_in_intro" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="20dp"
android:text="3 MIN - 5 MIN"
android:textSize="@dimen/text_size_timer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<TextView
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="20dp"
android:lineSpacingExtra="5sp"
android:text="We recommend to do this workout daily to develop a habit to check in with yourself each day. This is helpful as many of us are so busy that we become disconnected from our own thoughts and emotions."
android:textSize="@dimen/text_size_normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/checkInDailyButtonStart"
style="@style/btnStyleRed"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="Start"
app:icon="@drawable/ic_dumb_bell_white_16dp"
app:iconGravity="textStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notification_cardView2"
app:layout_constraintVertical_bias="1.0"
/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/notification_cardView2"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:clickable="true"
app:cardCornerRadius="10dp"
app:cardElevation="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/setUpNotificationsTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="Set up daily notifications to remind you to check-in"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/text_size_normal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/appCompatImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:adjustViewBounds="true"
app:srcCompat="@drawable/brain_insight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/setUpNotificationsTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ Leaking: NO (BaseApplication↓ is not leaking and a class is never leaking)
│ ↓ static FontsContract.sContext
├─ com.example.BaseApplication instance
│ Leaking: NO (WorkoutFragment↓ is not leaking and Application is a singleton)
│ ↓ BaseApplication.appComponent
├─ com.example.di.DaggerAppComponent instance
│ Leaking: NO (WorkoutFragment↓ is not leaking)
│ ↓ DaggerAppComponent.provideWorkoutAdapterProvider
├─ dagger.internal.DoubleCheck instance
│ Leaking: NO (WorkoutFragment↓ is not leaking)
│ ↓ DoubleCheck.instance
├─ com.example.adapters.RvAdapterWorkout instance
│ Leaking: NO (WorkoutFragment↓ is not leaking)
│ ↓ RvAdapterWorkout.mOnWorkoutListener
├─ com.example.ui.workouts.WorkoutFragment instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ WorkoutFragment.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ HashMap$Node[].[3]
├─ java.util.HashMap$Node instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (WorkoutCheckInIntro↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ com.example.ui.workouts.checkin.WorkoutCheckInIntro instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ WorkoutCheckInIntro.view
│ ~~~~
╰→ androidx.core.widget.NestedScrollView instance
Leaking: YES (ObjectWatcher was watching this because com.example.ui.workouts.checkin.WorkoutCheckInIntro received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = 53dc4388-0db6-402a-9ee3-2db6617c98f5
watchDurationMillis = 192734
retainedDurationMillis = 187732
mContext instance of com.example.ui.main.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.
Analysis duration: 22667 ms
23 Leaks
┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│ Leaking: NO (BaseApplication↓ is not leaking and a class is never leaking)
│ ↓ static FontsContract.sContext
├─ com.example.BaseApplication instance
│ Leaking: NO (Application is a singleton)
│ ↓ BaseApplication.appComponent
│ ~~~~~~~~~~~~
├─ com.example.di.DaggerAppComponent instance
│ Leaking: UNKNOWN
│ ↓ DaggerAppComponent.viewModelUsersProvider
│ ~~~~~~~~~~~~~~~~~~~~~~
├─ dagger.internal.DoubleCheck instance
│ Leaking: UNKNOWN
│ ↓ DoubleCheck.instance
│ ~~~~~~~~
╰→ com.example.persistence.viewmodel.ViewModelUsers instance
Leaking: YES (ObjectWatcher was watching this because com.example.persistence.viewmodel.ViewModelUsers received ViewModel#onCleared() callback)
key = 45b732d9-f6e0-4852-9b3c-8397c587f29f
watchDurationMillis = 223094
retainedDurationMillis = 218094
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: Google
LeakCanary version: 2.4
App process name: com.
Analysis duration: 22667 ms
Upvotes: 0
Views: 1485
Reputation: 410
There is a much more convenient way than to have to write all this boilerplate code to first get all the view references and later set them to null
It is called view binding. You could as well use data binding which allows you to interact with the data of your views much more easily.
It is stated though, however:
Note: Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's
onDestroyView()
method.
So still you could save a lot of boiler plate code and in the worst case only need to clear one reference.
View binding (as well as data binding), unlike findViewById
, ensure Null Safety and Type Safety.
In Kotlin you have one more option. You can use Kotlin extensions (just another dependency in your gradle file) and you can directly access views by their ID name without the need for findViewById
. This approach is compared to the view/data binding approach in this good answer.
I want to point out again that view/data binding can be used in both Java and Kotlin.
This is the way android tries to make it easy to minimize your development efforts. Especially when moving between many fragments (and avoiding/reduce boiler plate code in all of them)!
Upvotes: 2