커스텀 뷰를 만들 때, 뷰에서 필요한 초기화 작업은 대부분 생성자(Constructor)에서 수행합니다. 때문에, 일반적으로 다음과 같이 init() 메서드를 추가하여 초기화 작업을 수행하는 부분을 구현한 후 이를 각 생성자에서 호출하는 형태로 구성합니다.

public class MyCustomView extends TextView {

    public MyCustomView(Context context) {
        super(context);
        init();
    }

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyCustomView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    
    private void init() {
        // Initializes a view
    }
}

위와 같은 구현은 init() 메서드만 각 생성자에 빼먹지 않고 추가한다면 큰 문제는 없습니다. 하지만, 실수로 init() 메서드를 특정 생성자에 넣지 않는다면 뷰 초기화 작업이 제대로 되지 않아 원치 않는 결과를 얻을 수 있습니다.

따라서, init() 메서드를 사용하는 방법 보다 특정 생성자 내부에 직접 초기화 코드를 구현하고, 커스텀 뷰의 각 생성자에서 초기화 코드를 구현한 생성자를 호출하게끔 구현하는 것을 추천합니다.

대략 다음과 같은 형태가 되겠네요.

public class MyCustomView extends TextView {

    public MyCustomView(Context context) {
        // 자신의 생성자를 호출합니다.
        this(context, null);
    }

    public MyCustomView(Context context, AttributeSet attrs) {
        // 자신의 생성자를 호출합니다.
        this(context, attrs, 0);
    }

    public MyCustomView(Context context, AttributeSet attrs,
            int defStyleAttr) {
        // 부모 클래스의 생성자를 호출합니다.
        super(context, attrs, defStyleAttr);
        
        // Initializes a view
    }
}

안드로이드 내부 소스에서도 대부분 위와 같은 패턴으로 커스텀 뷰를 구현합니다. 다음은 디자인 서포트 라이브러리에 있는 NavigationView 소스 코드의 일부입니다.

public class NavigationView extends ScrimInsetsFrameLayout {
    
    // ... 생략 ...

    public NavigationView(Context context) {
        this(context, null);
    }

    public NavigationView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        ThemeUtils.checkAppCompatTheme(context);
        // Create the menu
        mMenu = new NavigationMenu(context);
        // Custom attributes
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.NavigationView, defStyleAttr,
                R.style.Widget_Design_NavigationView);
        //noinspection deprecation
        setBackgroundDrawable(a.getDrawable(R.styleable.NavigationView_android_background));
        if (a.hasValue(R.styleable.NavigationView_elevation)) {
            ViewCompat.setElevation(this, a.getDimensionPixelSize(
                    R.styleable.NavigationView_elevation, 0));
        }
        
        // ... 생략 ...
    }

init() 메서드를 하는 방식 대비 위 방법의 장점으로는 XML 속성이나 스타일 값이 필요한 경우 적절히 해당 파라미터가 지원되는 생성자를 선택할 수 있다는 것입니다. XML 속성이 필요할 경우 init() 메서드를 사용하면 init(AttributeSet) 과 같은 형태로 메서드를 구현해야 하고, AtttributeSet 이 지원되지 않는 생성자에서는 init(null) 과 같이 초기화 메서드를 호출해야 하므로 보기에도 썩 좋지가 않죠.

물론, 이 방법을 사용할 때도 유의할 점은 있습니다. 초기화 코드를 작성하지 않는 생성자 (위에서는 NavigationView(Context)NavigationView(Context, AttributeSet))에서는 필히 초기화 코드를 작성한 생성자를 호출하도록 처리해 주어야 합니다.

이 부분만 주의한다면, 커스텀 뷰 코드를 훨씬 깔끔하고 오류 없이 작성하는데 많은 도움이 될 것입니다.

김태호 (Taeho Kim)

안드로이드와 오픈소스, 코틀린(Kotlin)에 관심이 많습니다. 전 한국 GDG 안드로이드 운영자 및 GDE 안드로이드로 활동했으며, 현재 구글에서 애드몹 기술 지원을 담당하고 있습니다.

kunny Androidhuman


Published