프로젝트를 진행하며 BaseActivity 에 대해 알게 되었습니다.
BaseActivity 는 각 Activity 마다 반복적으로 작성되는 코드를 줄임으로써 액티비티별로 중요한 기능을 구현하는데만 집중할 수 있고, 주요 기능 파악이 수월 해진다는 장점이 있습니다.
지금까진 연합동아리 활동을 통해 받았던 샘플 BaseActivity 를 활용하여 프로젝트를 진행했었습니다.
하지만 프로젝트를 진행하며 기존 BaseActivity 가 적합하지 않는 경우가 발생했고, 제 프로젝트에 맞는 BaseActivity 를 작성하게 되었습니다.
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: ViewDataBinding
와 layout ID
를 parameter 로 받아 Data Binding 을 통해 모든 액티비티의 레이아웃 연결을 편리하게 할 수 있도록 구현했습니다.
binding.lifecycleOwner = this
Data Binding 에서 Viewmodel 연결 시 액티비티의 lifecycle 을 지정해줘야 합니다. 해당 코드도 반복적으로 사용되기 때문에 BaseActivity 에 작성했습니다.
추상 메서드인 initAfterBinding
을 통해 onCreate 생명주기에서 액티비티별로 커스텀해야하는 코드를 작성할 수 있도록 합니다.
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
}
}
}
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 깃허브 링크를 첨부합니다!