Search code examples
androidmvvmmemory-leaksviewmodel

ViewModel should never reference a view. If you do, then you'll get memory leaks. How to not violate this rule?


A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.

View model should never reference a view. If you do, then you'll get memory leaks.

enter image description here

public class FirstActivityViewModel extends ViewModel {

    private int displayWidth;
    private int displayHeight;

    private double widthMultiplier;
    private double heightMultiplier;

    private int testedWidth = 1200;
    private int testedHeight = 1920;

    public void setDisplayCorrectedSizes(RelativeLayout relativeLayout){
        displayWidth = relativeLayout.getWidth();
        widthMultiplier = ((double) displayWidth) / ((double) testedWidth);

        displayHeight = relativeLayout.getHeight();
        heightMultiplier = ((double) displayHeight) / ((double) testedHeight);
    }

    public double getWidthMultiplier(){
        return widthMultiplier;
    }

    public double getHeightMultiplier(){
        return heightMultiplier;
    }
}

Or a code like this is violating that rule?

public class FirstActivityViewModel extends ViewModel {

    private RelativeLayout relativeLayout;

    private int displayWidth;
    private int displayHeight;

    private double widthMultiplier;
    private double heightMultiplier;

    private int testedWidth = 1200;
    private int testedHeight = 1920;

    public void setDisplayCorrectedSizes(RelativeLayout relativeLayout){
        displayWidth = relativeLayout.getWidth();
        widthMultiplier = ((double) displayWidth) / ((double) testedWidth);

        displayHeight = relativeLayout.getHeight();
        heightMultiplier = ((double) displayHeight) / ((double) testedHeight);
        
        this.relativeLayout = relativeLayout;
    }

    public double getWidthMultiplier(){
        return widthMultiplier;
    }

    public double getHeightMultiplier(){
        return heightMultiplier;
    }
}

Or both of them are violating?

I just want to understand am I in a right way to understand it or not.

.......................................

Here is the whole code that I wanted to replace from view to ViewModel.

 void actionsForUISizesOptimizationProcess(){
        RelativeLayout relativeLayout2 = findViewById(R.id.rootLayoutSmallBoard);

       
        int displayWidth = relativeLayout2.getWidth();
        int displayHeight = relativeLayout2.getHeight();

        
        int testedWidth = 1200;
        double widthMultiplier = ((double) displayWidth) / ((double) testedWidth);
        int testedHeight = 1920;
        double heightMultiplier = ((double) displayHeight) / ((double) testedHeight);

//        firstActivityViewModel.setDisplayCorrectedSizes(relativeLayout2);
//        double widthMultiplier = firstActivityViewModel.getWidthMultiplier();
//        double heightMultiplier = firstActivityViewModel.getHeightMultiplier();

        if (limitOfOnWindowFocusChangedOperationForSmallBoard == 0) { /

            //Log.i("userTest2020", "display width = " + displayWidth + "\n" + "display height = " + displayHeight);

            int viewWidth;
            int viewHeight;

            viewWidth = (int) getResources().getDimension(R.dimen.tv10WidthSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.tv10HeightSmallBoard);

            tvSmallBoardResolutionInfoValue.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            tvSmallBoardResolutionInfoValue.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            viewWidth = (int) getResources().getDimension(R.dimen.tv11WidthSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.tv11HeightSmallBoard);

            tvSmallBoardScoreValue.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            tvSmallBoardScoreValue.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            viewWidth = (int) getResources().getDimension(R.dimen.btn1WidthSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.btn1HeightSmallBoard);

            btnSmallBoardGetTheResolutionInfo.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            btnSmallBoardGetTheResolutionInfo.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            viewWidth = (int) getResources().getDimension(R.dimen.btnNewRoundWidthSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.btnNewRoundHeightSmallBoard);

            btnNewRoundSmallBoard.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            btnNewRoundSmallBoard.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            ImageView ivBtnGreen = findViewById(R.id.ivNewRoundSmallBoard);
            viewWidth = (int) getResources().getDimension(R.dimen.ivNewRoundWidthSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.ivNewRoundHeightSmallBoard);

            ivBtnGreen.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            ivBtnGreen.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

           
            FrameLayout flNewBoard = findViewById(R.id.newRoundViewGroupSmallBoard);
            viewWidth = flNewBoard.getWidth();
            viewHeight = flNewBoard.getHeight();

            flNewBoard.getLayoutParams().width = (int) (viewWidth * widthMultiplier);
            flNewBoard.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            FrameLayout flBiggerBoard = findViewById(R.id.biggerBoardViewGroupSmallBoard);
            viewWidth = (int) getResources().getDimension(R.dimen.BiggerBoardViewGroupSmallBoard);

            flBiggerBoard.getLayoutParams().width = (int) (viewWidth * widthMultiplier);

            viewHeight = (int) getResources().getDimension(R.dimen.btnNextBoardHeightSmallBoard);

            btnNextBoardSmallBoard.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            ImageView ivNextBoard = findViewById(R.id.ivNextBoardSmallBoard);
            viewHeight = (int) getResources().getDimension(R.dimen.ivNextBoardHeightSmallBoard);

            ivNextBoard.getLayoutParams().height = (int) (viewHeight * heightMultiplier);

            limitOfOnWindowFocusChangedOperationForSmallBoard = 1; 
        }
    }

Why I thought that I must replace this code to ViewModel? See here https://android.jlelse.eu/mvvm-how-view-and-viewmodel-should-communicate-8a386ce1bb42 (The rule number one) And why I can't figure out how to do it? Cause on the other hand there is another rule: View model should never reference a view. If you do, then you'll get memory leaks.


Solution

  • Theoretically, the second one is violating the rule while the first one isn't, but ideally, viewModel should never care about the view. Using Relative layout as an argument will be problematic while writing unit tests. Our aim should be to separate viewModel from view as much as possible. I would suggest rewriting your second approach like this

    public void setDisplayCorrectedSizes(int displayWidth, int displayHeight){
        widthMultiplier = ((double) displayWidth) / ((double) testedWidth);
        heightMultiplier = ((double) displayHeight) / ((double) testedHeight);
    }

    Another suggestion would be replace getter methods with observables that emit values for view to observe. I would rewrite your viewModel like this

    public class FirstActivityViewModel extends ViewModel {
    
        private LiveData<Double> widthMultiplierLiveData = new MutableLiveData();
        private LiveData<Double> heightMultiplierLiveData = new MutableLiveData();
        
        private final int testedWidth = 1200;
        private final int testedHeight = 1920;
    
        public void setDisplayCorrectedSizes(int displayWidth, int displayHeight){
            double widthMultiplier = ((double) displayWidth) / ((double) testedWidth);
            double heightMultiplier = ((double) displayHeight) / ((double) testedHeight);
            
            widthMultiplierLiveData.value = widthMultiplier;
            heightMultiplierLiveData.value = heightMultiplier;
        }
    }