다양한 레퍼런스를 확인하던 와중 Single Activity Architecture (Application)에 대한 글을 보았다.
단일 혹은 아주 적은 개수의 Activity만 사용하고 모두 Fragment로 구현한 구조인데, 상당히 흥미로운 구조라고 생각하여 Jetpack Navigation을 사용하여 구현해 보았다.
이번 글에서는 SAA 구조로 샘플 코드를 만들어 보면서 발생했던 이슈와 처리 과정에 대하여 기술해볼 생각이다.
우선, SAA란.
Single Activity Architecture는 Google I/O 2018에서 언급된 개념으로,
하나 혹은 적은 갯수의 Activity만을 사용하고 나머지 화면은 Fragment로 구성한 구조로, 주로 JetPack Navigation과 함께 사용되는 구조이다.
어느 게시글에서는 2019년도 Google I/O에서 언급되었다고 하지만, 2018년도 Google I/O에서 Single Activity에 대한 설명을 들을 수 있었다. 그것 외에도 17년도 글에서도 Single Activity에 대한 글이 있는 것으로 보아 예전부터 해당 구조에 대하여 얘기가 나왔던 것 같다.
그렇다면 Single Activity를 사용하는 이유는 무엇일까?
Google I/O 2018에서는 Actvitiy간의 데이터 공유, UI 변경에 이점 등을 이유로 설명을 이어 나갔는데, 이것들 외에도 다양한 이유로 Activity 보다 Fragment를 사용하는 것을 볼 수 있었다.
Activity는 Fragment에 비하여 상대적으로 무겁기 때문에 메모리나 속도 방면에서 Fragment를 사용하는 것이 훨씬 더 이득이다. 또한, 비즈니스 로직을 Fragment 단위로 분리하여 의존성을 줄인다던지, Activity보다 유연한 UI 디자인을 지원하는 등 Fragment를 사용하는 이유는 상당히 많다고 볼 수 있다.
상대적으로 가벼우며, 데이터 공유, UI의 이점, 관심사의 분리가 편한 것 외에도 다양한 장점을 찾아볼 수 있는데 이것들이 Single Activity를 사용하는 큰 이유들이라고 볼 수 있다.
하지만, Lifecycle이 Activity 하나만 있을 때에 비하여 더 복잡해지고, Fragment 간의 동작이 비동기로 처리되기 때문에 동기/비동기에 따른 이슈가 발생할 수 있다는 단점을 가지고 있다.
이러한 장,단점이 존재하기 때문에 SAA를 사용하는 것이 모든 프로젝트에서 좋은 것은 아니고, 프로젝트의 상황에 따라서 선택할 수 있는 하나의 Architecture라고 생각한다.
그렇다면, 샘플 예제를 만들어 보면서 어떻게 구현되는지 확인해보자.
우선 전체적인 모듈의 구조부터 확인해 보자.
MoularArchitecture Sample Project와 비교해보면, navigation이라는 모듈이 하나 추가된 것 외에는 동일한 구조로 되어있다.
이렇게 구조가 되는 이유는 Diagram을 그려보면 쉽게 이해할 수 있을 것이다.
이처럼, 기존 구조에서 가장 하단에 navigation 모듈이 있고, 그것을 참조하여 Fragment끼리의 이동을 시켜주는 것이다.
Fragment의 이동이 필요한 모듈의 경우 Navigation 모듈을 참조하도록 하면 되는데, 보통 Navigation 모듈이 필요한 경우 Core 모듈도 높은 확률로 참조하고 있기 때문에 Core 모듈로 통합하여 사용해도 무관하다.
필자는 단순히 좀 더 명확하게 navigation 모듈을 확인하고자 이처럼 따로 빼내어서 샘플을 만들었다.
navigation 모듈에는 별 다른 코드 없이 nav_graph.xml 파일 하나만 존재하고 있다.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/introFragment">
<fragment
android:id="@+id/introFragment"
android:name="com.example.saa_modular.IntroFragment"
android:label="fragment_intro">
<action
android:id="@+id/action_intro_to_main"
app:destination="@id/mainFragment" />
</fragment>
<fragment
android:id="@+id/mainFragment"
android:name="com.example.main.MainFragment"
android:label="fragment_main">
<action
android:id="@+id/action_main_to_search"
app:destination="@id/searchFragment" />
</fragment>
<fragment
android:id="@+id/searchFragment"
android:name="com.example.search.SearchFragment"
android:label="fragment_search">
<action
android:id="@+id/action_search_to_main"
app:destination="@id/mainFragment"/>
</fragment>
</navigation>
코드를 보면 기존 navigation을 사용하는 것과 동일하게 되어있다.
모듈만 나눠두었을 뿐이지, navigation을 사용하여 Fragment를 변경하는 것은 단일 모듈로 작성했을 때와 다른 점은 없다.
activity_main.xml에서 fragment를 선언하고, app:navGraph 속성 값이 navigation 모듈에 생성한 navigation의 id를 넣어주면 된다.
App 모듈의 구조이다.
MainActivity를 만들어두고, 그 이외의 화면은 Fragment로 사용하도록 하였다.
각 Fragment는 기존과 동일하게 사용하면 되며
findNavController().navigate(com.example.navigation.R.id.mainFragment)
introFragment에서는 다음과 같은 방법으로 mainFragment로 이동할 수 있다.
mainFragment는 introFragment와 다른 Main Module에 위치하고 있기 때문에 navigation 모듈을 사용하여 이동해야 한다.
nav_graph에서 mainFragment라는 목적지를 설정해두었기 때문에 해당 fragment에 대한 id 값을 호출해줌으로 이동이 가능하다.
Main 모듈에서는 모듈간 Fragment이동에 사용하는 ParentFragment로 MainFragment를 사용하고, 그 안에 ChildFragment로 Home, Move, Text를 선언하여 사용하고 있다.
MainFragment에서 다른 Module로의 이동은 IntroFragment에서 사용했던 것처럼
fun moveSearchFragment() {
findNavController().navigate(com.example.navigation.R.id.searchFragment)
}
다음과 같이 호출하여 이동하면 된다.
그렇다면, ChildFragment간의 이동은 어떻게 처리해야 할까?
필자는 ChildFragment에는 BottomNavigationView를 사용하여 Fragment를 변경할 수 있게 구현하였다.
해당 Module에서만 사용할 navigation을 만들어주고 해당 navigation을 사용하여 Fragment를 이동할 수 있게 하였다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navHostFragment =
childFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment
navController = navHostFragment.navController
binding.navView.setupWithNavController(navController!!)
binding.navView.setOnItemSelectedListener {
it.onNavDestinationSelected(navController!!)
}
}
ParentFragment에 대한 NavController가 설정되어있기 때문에 ChildFragment를 사용하기 위해서는 NavController를 재설정해주어야 한다.
따라서, childFragmentManager를 사용하여 ChildFragment를 명시해주고, ChildFragment에서의 NavController를 설정하여 BottomNavigation을 클릭했을 때 Fragment를 이동할 수 있도록 해주었다.
마지막으로, search Module에서는 지금까지 Fragment처럼 ViewBind만 사용하는 것이 아닌 DataBind를 사용하여 ViewModel을 사용하고 있다.
따라서 gradle에서도 Databinding에 대한 처리를 해주어야 하는데, 단순히 search Module에만 추가하는 것이 아니라 Activity가 존재하는 App Module에도 DataBinding에 대한 처리를 해주어야 한다.
buildFeatures {
dataBinding = true
viewBinding true
}
각각의 Module의 gradle에 다음과 같이 ViewBinding과 더불어 DataBinding에 대한 사용 여부 처리를 추가해주고 사용하도록 한다.
이것을 제외하고는 기존의 Activity에서 사용하던 형식과 동일하게 사용이 가능하다.
지금까지 Single Activity Architecture의 기본적인 구현 방법을 알아보았는데,
Single Activity Architecture을 구현함에 있어서 navigation 모듈을 따로 빼내어 사용하면 된다는 생각을 하기가 어려웠던 것 같다. Fragment는 Activity 위에 존재해야 하는데, 모듈을 나누면 각 모듈에 Activity를 하나씩 생성해서 사용해야하는 것이 아닌가? 라는 생각을 했기 때문이다.
아주 간단한 샘플 예제를 만들면서도, ParentFragment와 ChildFragment의 컨트롤 방법이라던지, Activity와 Fragment 사이의 lifecycle 문제로 인한 이슈 등 문제가 있었는데, 생각보다 해결하는 것에 시간이 오래걸렸던 것 같다.
생각보다 글이 길어져서 Single Activity Architecture를 구현하면서 발생했던 문제와 해결 방법에 대해서는 다음 글에 이어서 작성하도록 하겠다.
아직 부족한 부분이 많은 샘플이지만, 앞으로 계속해서 개선해 나가면서 쓸만한 샘플을 만들어보고자 한다.
해당 게시글에 사용한 예제는 Github에 올려두었다.
https://github.com/HeeGyeong/SAA-Compose
'Android > Architecture' 카테고리의 다른 글
[Android] Jetpack Compose 환경에서 MVI Pattern 구현하기 (1) | 2024.12.05 |
---|---|
[Android] Single Activity Architecture (SAA) + Navigation - 이슈 해결 (0) | 2022.07.08 |
[Android] MVP Pattern을 적용해보자. (0) | 2022.05.05 |
[Android] MVI Pattern을 적용해보자. (2) | 2022.05.03 |
[Android] MVI Pattern ? (0) | 2022.04.30 |