나만의 BaseActivity 만들기

KEH·2023년 2월 20일
1
post-thumbnail

프로젝트를 진행하며 BaseActivity 에 대해 알게 되었습니다.

BaseActivity 는 각 Activity 마다 반복적으로 작성되는 코드를 줄임으로써 액티비티별로 중요한 기능을 구현하는데만 집중할 수 있고, 주요 기능 파악이 수월 해진다는 장점이 있습니다.

지금까진 연합동아리 활동을 통해 받았던 샘플 BaseActivity 를 활용하여 프로젝트를 진행했었습니다.
하지만 프로젝트를 진행하며 기존 BaseActivity 가 적합하지 않는 경우가 발생했고, 제 프로젝트에 맞는 BaseActivity 를 작성하게 되었습니다.

공통 기능

Data Binding 을 활용한 레이아웃

abstract class BaseActivity<T: ViewDataBinding>(private val layoutResId: Int): AppCompatActivity() {
    protected lateinit var binding: T
    
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, layoutResId)
        binding.lifecycleOwner = this

        imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager

        initAfterBinding()
    }

    protected abstract fun initAfterBinding()
    
    ...
    
}

제 프로젝트에서는 Data Binding 을 활용하여 액티비티와 레이아웃을 연결해 주었습니다.
BaseActivity 를 추상클래스로 정의하고, T: ViewDataBindinglayout ID 를 parameter 로 받아 Data Binding 을 통해 모든 액티비티의 레이아웃 연결을 편리하게 할 수 있도록 구현했습니다.

binding.lifecycleOwner = this

Data Binding 에서 Viewmodel 연결 시 액티비티의 lifecycle 을 지정해줘야 합니다. 해당 코드도 반복적으로 사용되기 때문에 BaseActivity 에 작성했습니다.

추상 메서드인 initAfterBinding 을 통해 onCreate 생명주기에서 액티비티별로 커스텀해야하는 코드를 작성할 수 있도록 합니다.


Activity 에서 활용되는 다양한 기능

BaseActivity 에 공통 기능을 정의할 때 최대한 Activity 에서 사용되는 기능만 을 정의하려 노력했습니다.
즉, Activity 와 Fragment 두 곳에서 사용되는 기능은 Util 클래스로 빼고, 그 이외의 나머지 기능만을 정의했습니다.

토스트 메시지 띄우기

fun showToast(message: String, duration: Int) {
	Toast.makeText(this, message, duration).show()
}

토스트 메시지를 Util 클래스로 뺄지 고민이 많았습니다. Util 클래스로 뺄 경우 기존 코드와 비슷하게 context, 메시지, duration 을 모두 인자로 넘겨줘야 하는 반면 BaseActivity 에 뺄 경우 인자 수를 줄일 수 있어 BaseActivity 에 작성하기로 결정했습니다.
(사실 각각 액티비티에 넣어놔도 될 것 같네요. 전 이미 습관이 들어버려서 이게 편하지만 ㅎㅎ!)

액티비티 이동

fun startNextActivity(activity: Class<*>?) {
	val intent = Intent(this, activity)
	startActivity(intent)
}

현재 액티비티 종료 후 다음 액티비티 이동

fun startNextActivity(activity: Class<*>?) {
	val intent = Intent(this, activity)
	intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
	startActivity(intent)
}

키보드 보이기 / 숨기기

abstract class BaseActivity<T: ViewDataBinding>(private val layoutResId: Int): AppCompatActivity() {
    private lateinit var imm: InputMethodManager

	...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ...

        imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager

        initAfterBinding()
    }
    
    // 키보드 보이기
    fun showKeyboard(v: View){
        imm?.showSoftInput(v, 0)
    }

    // 키보드 숨기기
    fun hideKeyboard(v: View){
        imm?.hideSoftInputFromWindow(v.windowToken, 0)
    }
}

상태바 텍스트 보여주기

fun showStatusBarText() {
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
		window.insetsController?.setSystemBarsAppearance(
			WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
            WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS)
    } else {
		@Suppress("DEPRECATION")
		window.decorView.systemUiVisibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
			View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
		} else {
			View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
		}
	}
}


BaseActivity - BaseViewModel

BaseViewModel 에서는 공통적으로 로딩 처리, JWT 유효성 체크, HTTP Status Code 체크 등을 수행하고 있습니다. 이에 따라 토스트메시지와 JWT 유효성 데이터를 각 Activity 에서 데이터를 observe 하고, 처리해 줘야 합니다. 반복을 줄이고자 이 기능을 BaseActivity 에서 진행했습니다.

abstract class BaseActivity<T: ViewDataBinding>(private val layoutResId: Int): AppCompatActivity() {
	...
    
    private lateinit var refreshTokenErrorDialog: OneBtnDialogFragment

    ...

    private fun initRefreshTokenErrorDialog() {
        refreshTokenErrorDialog = OneBtnDialogFragment()
        refreshTokenErrorDialog.setMyCallback(object : OneBtnDialogFragment.MyCallback {
            override fun end() {
                SpfUtils.clear()
                SpfUtils.writeSpf("onBoarding", true)

                val intent: Intent = Intent(this@BaseActivity, OnBoardingActivity::class.java)
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                intent.putExtra("currentItem", 3)
                startActivity(intent)
            }
        })

        val oneBtnDialog: OneBtnDialog = OneBtnDialog("재로그인이 필요합니다.", "토큰에 문제가 발생해 재로그인이 필요합니다.\n로그인 화면으로 이동합니다.", "확인", listOf(46, 10, 46, 12))
        val bundle: Bundle = Bundle()
        bundle.putParcelable("data", oneBtnDialog)
        refreshTokenErrorDialog.arguments = bundle
    }

    ...

    fun setCommonObserver(vmList: List<BaseViewModel>) {
        vmList.forEach { vm ->
            vm.toast.observe(this, Observer {
                val msg = it.getContentIfNotHandled()

                if (msg!=null) {
                    hideKeyboard(binding.root)
                    showToast(msg)
                }
            })

            vm.tokenExpired.observe(this, Observer {
                if (it && !refreshTokenErrorDialog.isAdded) {
                    refreshTokenErrorDialog.show(supportFragmentManager, null)
                }
            })
        }

        initRefreshTokenErrorDialog()
    }
}

setCommonObserver 메서드를 통해 각 ViewModel 의 toast 데이터와 tokenExpired 데이터를 observe 합니다.
이때 list 형태인 vmList 를 인자로 받는 이유는 하나의 액티비티 당 여러 개의 ViewModel 을 가질 수 있기 때문에 다음과 같이 정의합니다.
만약 다음 메서드를 정의하지 않았다면, 각 액티비티 별로 다음과 같이 정의해야 했을 겁니다.

class BadgeActivity : BaseActivity<ActivityBadgeBinding>(R.layout.activity_badge) {
    private val badgeVm: BadgeViewModel by viewModels()

	...

    override fun initAfterBinding() {
        
        ...
        
        observe()
    }

	...
    
    private fun observe() {
    	badgeVm.toast.observe(this, Observer {
			val msg = it.getContentIfNotHandled()

			if (msg!=null) {
				hideKeyboard(binding.root)
				showToast(msg)
			}
		})

		badgeVm.tokenExpired.observe(this, Observer {
			if (it && !refreshTokenErrorDialog.isAdded) {
				refreshTokenErrorDialog.show(supportFragmentManager, null)
			}
		})
    }
}

하지만 setCommonObserver 메서드를 정의한다면 다음과 같이 코드가 굉장히 짧아집니다!

class BadgeActivity : BaseActivity<ActivityBadgeBinding>(R.layout.activity_badge) {
    private val badgeVm: BadgeViewModel by viewModels()

	...

    override fun initAfterBinding() {
        
        ...
        
        setCommonObserver(listOf(badgeVm))
    }

	...
}

또한 initRefreshTokenErrorDialog 메서드를 통해 토큰 만료 시 다이얼로그 화면이 나타나는 기능을 각 액티비티 별로 작성하지 않을 수 있도록 진행했습니다.


마무리

코드를 작성할 때 최소한 반복을 줄이고, 간결하게 작성하고자 고민하고 고민합니다.
하지만 이 과정은 항상 어려운 것 같습니다.
쉽게 와닿지 않는 경우가 많아 제 옆에서 척척 대답해줄 수 있는 최고의 멘토님이 있으면 얼마나 좋을까! 란 생각을 한답니다 :)
그럼에도 불구하고 더 좋은 방법이 생각난다면 꾸준히 고치고 더 좋은 코드를 만들어보겠습니다.
아래 제가 작성했던 BaseActivity 깃허브 링크를 첨부합니다!

BaseActivity

profile
개발을 즐기고 잘하고 싶은 안드로이드 개발자입니다 :P

0개의 댓글