최근 새로운 프로젝트에서 기존 View System과 같이 컴포즈를 사용하고 있습니다. 해당 프로젝트에서 Jetpack Navigation을 사용해 BottomNavigation을 구현한 화면에서 Fragment를 컴포즈로 대체해야 하는 상황이 발생했습니다. 바텀 네비게이션의 프래그먼트 화면이 전환되어도 BottomNavigation을 유지해야 했기 때문에 Activity -> Parent Fragment -> 두 개의 Child Fragment로 이루어진 환경에서 첫 번째 Child Fragment(바텀 네비게이션이 짠! 하고 화면에 띄워질 때 첫 번째 화면)을 컴포저블로 전환해야 하는 상황이 되었습니다.
ComposeView
공식 문서에서 안내하는 ComposeView를 사용하면 View 시스템에 Jetpack Compose를 쉽게 통합할 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".main.view.fragment.HomeConceptFragment">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment에 ComposeView를 추가합니다. Fragment의 OnCreateView( ) 에서 ComposeView의 참조를 얻은 후 setContent를 사용해 컴포저블을 호출할 수 있습니다.
@AndroidEntryPoint
class HomeConceptFragment : Fragment(R.layout.fragment_home_concept) {
private val viewModel: HomeConceptViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.fragment_home_concept, container, false)
val composeView = view.findViewById<ComposeView>(R.id.compose_view)
composeView.setContent {
ConceptScreen(
viewModel = viewModel,
onClickConcept = {
viewModel.onChangeScreenState(it)
}
)
}
return view
}
}
@HiltViewModel
class HomeConceptViewModel @Inject constructor(
private val studioConceptRepository: StudioConceptRepository
) : ViewModel() {
private var _uiState = MutableStateFlow(HomeConceptState.create())
val uiState: StateFlow<HomeConceptState> get() = _uiState
private var _event = MutableSharedFlow<Concept>()
val event: SharedFlow<Concept> get() = _event.asSharedFlow()
}
@AndroidEntryPoint
class HomeConceptMainFragment : Fragment(R.layout.fragment_home_concept_main) {
private val viewModel: HomeConceptViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FragmentHomeConceptMainBinding.bind(view)
val navController = NavHostFragment.findNavController(
childFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment
)
repeatOnViewStarted {
viewModel.event.collect { event ->
val action = HomeConceptFragmentDirections.actionToResultViewFragment(event.id)
navController.navigate(action)
}
}
}
}
ViewCompositionStrategy
ViewCompositionStrategy는 Compose의 Composition LifeCycle을 제어하기 위한 전략으로 언제 composition을 dispose해야 하는지 정의합니다.
기본적으로 ViewCompositionStrategy.Default가 적용되며 DisposeOnDetachedFromWindowOrReleasedFromPool 전략을 사용합니다.이 전략은 ComposeView가 윈도우에서 분리될 때 또는 RecyclerView와 같은 풀링 컨테이너에서 해제될 때 Composition을 폐기합니다.
- DisposeOnDetachedFromWindow
- View가 Screen에서 벗어나 사용자에게 표시되지 않을 때.
- View가 윈도우에서 분리되 detach 됐을 때 Composition이 dispose 됩니다.
- 주로 단일 Activity Compose 앱에서 적합합니다.
- DisposeOnDetachedFromWindowOrReleasedFromPool (기본값)
- 풀링 컨테이너(RecyclerView 등) 내에서 Composition이 과도하게 생성/폐기되지 않도록 최적화되었습니다.
- 스크롤 성능을 개선하고, RecyclerView에서의 효율적인 재사용을 보장합니다.
- DisposeOnLifecycleDestroyed
- 사용자가 지정한 Lifecycle이 종료될 때 Composition이 dispose 됩니다.
- Fragment와 같이 명확한 LifecycleOwner와 1:1 관계를 맺는 경우 적합합니다.
class MyFragment : Fragment() {
// …
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnLifecycleDestroyed(
lifecycle = this@MyFragment.lifecycle // <== Lifecycle 지정
)
)
// …
}
}
- DisposeOnViewTreeLifecycleDestroyed
- ViewTreeLifecycleOwner를 기반으로 Composition이 dispose 됩니다.
- LifecycleOwner를 사전에 알 수 없는 상황에서 사용합니다.
ComposeView는 Fragment가 전환될 때 ComposeView가 일시적으로 화면에서 분리됩니다. 이때 컴포지션이 바로 삭제되면 화면 전환 중 UI가 깨지거나 데이터 상태를 잃어버릴 수 있습니다.
이를 방지하기 위해 DisposeOnViewTreeLifecycleDestroyed를 사용해 ComposeView가 Fragment의 LifeCycle을 따르도록 하면 Fragment가 완전히 파괴될 때만 컴포지션을 삭제하도록 만들 수 있습니다.
@AndroidEntryPoint
class HomeConceptFragment : Fragment(R.layout.fragment_home_concept) {
private val viewModel: HomeConceptViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.fragment_home_concept, container, false)
val composeView = view.findViewById<ComposeView>(R.id.compose_view)
composeView.apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
ConceptScreen(
viewModel = viewModel,
onClickConcept = {
viewModel.onChangeConcept(it)
}
)
}
}
return view
}
}
Reference
https://medium.com/androiddevelopers/viewcompositionstrategy-demystefied-276427152f34