본 게시글은 이전 게시글에 이어서 작성된 부분입니다.
2022.07.04 - [Android/Architecture] - [Android] Single Activity Architecture (SAA) + Navigation
이전 게시글에서 SAA에 대한 기본적인 샘플 프로젝트를 만들어 보았다.
이번 게시글에서는 이어서 SAA 구조의 샘플을 만들면서 시간이 걸렸던 이슈들에 대해서 살펴보고자 한다.
우선, 가장 처음 발생했던 이슈는
ViewModel의 주입 타이밍이다.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = getViewModel()
permissionCheck()
}
viewModel에 대한 의존성 주입은 onActivityCreated에서 해주어야 한다.
onCreateView나 onViewCreated에서 선언하게 되면 정상적인 시점에 viewModel 주입이 이루어지지 않아 에러가 발생하고 앱이 죽게 된다.
단, DI를 사용하지 않는 경우에는 onCreateView에서 선언해도 상관없다.
이 부분에 대해서는 Activity와 Fragment의 LifeCylce에 관련된 이슈가 아닐까 하여 LifeCycle에 대하여 확인해 보았다.
Activity에서 onCreate > onAttachFragment가 호출된 다음에 Fragment가 생성되고 있음을 볼 수 있다.
이런 흐름대로 진행이 된다면, Fragment의 onCreatedView에서 viewModel에 대한 의존성 주입을 시켜도 문제가 없어야 하지 않는가 생각하여 조금 더 자세히 확인해 보았다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("callCheck", "MAIN onCreate 1")
setContentView(R.layout.activity_main)
Log.d("callCheck", "MAIN onCreate 2")
supportActionBar?.hide()
loadKoinModules(
listOf( ... )
)
Log.d("callCheck", "MAIN onCreate 3")
}
호출 순서는 위와 같을지 몰라도, 작업이 끝나기 전에 다른 프로세스가 호출된다면 viewModel에 대한 의존성 주입이 onCreateView에 했을 때 문제가 생길 수 있다고 생각하였기 때문에 위처럼 중간중간에 로그를 찍어서 확인해 보았다.
맨 처음 onCreate가 호출되고,
setContentView(R.layout.activity_main)
setContentView를 통해 layout을 설정해 주는데, Fragment가 선언되어 있으므로 이때 AttachFragment를 호출하여 Fragment의 생성 프로세스를 태우게 된다.
따라서 Fragment의 생성이 끝났을 때 다시 Activity의 onCreate로 돌아와서 나머지 프로세스를 끝내게 되고 그 후 Fragment에서 onActivityCreated가 호출되게 된다.
이러한 흐름으로 진행되기 때문에, Koin을 사용한 Module설정 코드가 Fragment 생성이 끝난 후에 수행되게 되어 onActivityCreated 이전의 위치에서 viewModel에 대한 DI를 선언하게 되면 앱이 죽게 되는 것이다.
그렇다면, 어떻게 처리하는 것이 좋을까?
첫 번째로, 지금의 구조와 마찬가지로, DI 모듈에 대한 설정이 끝난 후인 onActivityCreated에서 설정해주는 것이다.
두 번째로, setContentView의 위치를 onCreate의 마지막으로 옮기는 것이다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("callCheck", "MAIN onCreate 1")
supportActionBar?.hide()
loadKoinModules(
listOf( ... )
)
Log.d("callCheck", "MAIN onCreate 2")
setContentView(R.layout.activity_main)
Log.d("callCheck", "MAIN onCreate 3")
}
이처럼 setContentView를 가장 마지막에 호출하도록 변경하고,
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
Log.d("callCheck", "INTRO onCreateView")
_binding = FragmentIntroBinding.inflate(inflater, container, false)
viewModel = getViewModel()
Log.d("callCheck", "INTRO Set getViewModel()")
return binding.root
}
이와 같이 onCreateView에서 호출하도록 변경한 후에 실행을 해보자.
정상적으로 onCreateView에서 viewModel에 대한 의존성 주입을 수행할 수 있다.
이런 방법이 있지만, 필자는 별 다른 문제가 없다면 첫 번째와 마찬가지로 ActivityCreated에서 설정을 해주는 것이 바람직하지 않을까 생각한다. Activity를 생성할 때 필요한 작업을 모두 끝낸 후에 이러한 부가적인 설정을 해주는 것이 순서상 맞지 않나 생각한다.
다음으로,
ChildFragment에서 ParentFragment의 이동을 컨트롤하는 부분이다.
위에 기본적인 설명을 진행할 때, IntroFragment에서 사용한 코드와 동일한 형태로 호출하여 이동할 수 있다고 작성을 했다. 하지만, 막상 ChildFragment에서 ParentFragment에 대한 함수를 호출하려고 하니 방법이 떠오르지 않았었다.
만들어진 Fragment 객체를 사용하여 함수를 호출해야 하는데, instance를 가져올 수 있는 방법을 찾아보게 되었고, 결과적으로 Companion object를 사용하여 해결하였다.
companion object {
private lateinit var mainFragment: MainFragment
fun getInstance(): MainFragment = mainFragment
}
MainFragment에 다음과 같이 선언해주고, onCreateView를 실행할 때 lateinit로 선언된 mainFragment를 this로 초기화시켜주었다.
이처럼 선언을 하고, childFragment에서는
MainFragment.getInstance().moveSearchFragment()
간단하게 ParentFragment의 함수를 사용할 수 있다.
해당 방법을 적용하기 전에, 구글링을 통하여 ParentFragment의 Instance를 가져올 수 있는 방법에 대하여 찾아보았는데,
// 1
val parentFrag: MainFragment =
this@MoveFragment.parentFragment as MainFragment
parentFrag.moveSearchFragment()
// 2
(parentFragment as MainFragment).moveSearchFragment()
이처럼 상속을 받아서 부모 역할을 하는 Fragment에서 강제 형 변환을 통해서 함수를 호출하는 방법만 확인할 수 있었다.
현재 예제의 구조는 모듈로 각 Fragment가 나뉘어 있지만, Activity 측면에서 보면 이런 구조를 보이고 있다.
따라서, ParentFragment를 MainActivity가 컨트롤하는 Fragment로, ChildFragment를 MainFragment가 컨트롤하는 Fragment로 얘기했을 뿐이지 어떠한 상속 관계가 있는 것은 아니다.
그렇기 때문에 위와 같은 방법을 사용할 수 없고, instance를 받아와서 함수를 호출하는 방식으로 해결할 수 있다.
마지막으로,
DataBinding 시 lifecycle에 대한 문제이다.
dataBinding에 viewModel을 사용하기 때문에, 첫 번째 이슈를 해결하면서 viewModel에 대한 Databinding 설정은 onActivityCreated로 옮김으로 해결하긴 했다.
하지만, 정상적인 동작 로그가 보이는 반면 제대로 화면에 보이지 않고, 재 접근 시 이전에 작업했던 내용이 보이는 이슈가 있어서 찾아보던 와중에 lifecycleOwner 값을 설정하지 않았다는 것을 발견하였다.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = getViewModel()
_binding!!.lifecycleOwner = this
_binding!!.vm = viewModel
...
}
따라서 이와 같이 lifecycleOwner = this를 넣어서 문제를 해결하였다.
사실 해당 문제는 SAA를 처음 적용하기 때문에 발생했던 이슈가 아니라, LiveData를 사용하기 위해선 lifecycleOwner를 현재 사용하는 뷰로 지정해주고 데이터 변화 시 Binding 된 뷰에 갱신하도록해야하는데 해당 부분을 놓쳤기 때문이다.
따라서, 이처럼 수정을 해도 해결은 되는데 조금 더 디테일하게 들어가면 fragment에서 사용할 수 있는 lifeCycle은 Activity와 Fragment 2개로 나누어진다.
위처럼 this를 통해 Fragment 자체의 lifecycle을 사용하거나,
_binding!!.lifecycleOwner = this.viewLifecycleOwner
이처럼 viewLifecycle을 사용하는 경우이다.
두 가지의 lifecycle을 비교해보면, fragment의 lifecycle이 viewLifecycle보다 더 큰 범위로 존재하는 라이프사이클이라고 생각하면 된다.
- Lifecycle : onCreate ~ onDestroy
- ViewLifeclcle : onViewCreate ~ onDestroyView
이처럼 Fragment의 lifecycle 내부에 viewLifecycle이 들어있는 느낌이라고 생각하면 편하다.
따라서, 해당 라이프사이클을 정확히 인지하지 않은 상태에서 liveData에 대한 lifecycle을 지정하다 보면 의도치 않게 오류가 발생할 수 있다.
GooGle I/O 2019를 보면, Fragment 사용 시 데이터 갱신에 대한 Lifecycle은 Fragment Lifecycle보다 View Lifecycle을 사용하는 것이 올바르다고 나와있기 때문에, 필자가 수정한 것처럼 this를 넣는 것이 아니라, this.viewLifecycleOwner를 사용하여 lifecycle을 설정해주도록 변경해야 한다.
SAA를 적용하면서 발생한 이슈와 처리한 방법에 대하여 작성해 보았다.
사실, Single Activity이기 때문에 발생한 문제라기보다는 Fragment에 대한 이해도가 부족해서 발생했던 문제들로 생각된다.
지금까지 Fragment를 많이 사용하긴 했지만, 생각보다 깊지 않게 확인하지 않고 사용했던 면이 많은 것 같다.
특히, Lifecycle에 대한 경우 이번에 SAA를 적용하면서 처음으로 알게 됐으니 말이다.
새로운 기술을 습득하기에 앞서, 지금까지 알고 있다고 생각했던 기술들에 대해서도 복습하고 좀 더 깊게 공부하는 시간이 필요할 것으로 보인다.
'Android > Architecture' 카테고리의 다른 글
[Android] Single Activity Architecture (SAA) + Navigation (3) | 2022.07.04 |
---|---|
[Android] MVP Pattern을 적용해보자. (0) | 2022.05.05 |
[Android] MVI Pattern을 적용해보자. (2) | 2022.05.03 |
[Android] MVI Pattern ? (0) | 2022.04.30 |
[Android] Repository Pattern (15) | 2022.03.21 |