AndroidDev123
AndroidDev123

Reputation: 370

Android and memory leaks

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

Answers (1)

cewaphi
cewaphi

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

Related Questions