배경
이번에는 Multi-Module구조에서 Navigation을 적용하는 방법에 대해 정리해볼까 합니다. 제가 모듈화를 적용하면서 feature 단위로 모듈을 나눴을 때 2개의 feature 간의 의존성이 없기 때문에 어떻게 이동해야하는지 막막했던 경험이 있습니다.
다른 포스팅을 참고하려고 해도 이게 뭔소린가 싶은 내용도 있었고, 따라 적용해봤지만 좀 더 좋은 구조를 찾고 싶다는 마음이 생긴 적도 있었기에 제가 직접 구현하고 정리해서 이 글을 통해 다른 분들이 조금이나마 쉽게 Navigation을 적용하셨으면 합니다!
※ Compose가 아닌 일반 XML을 활용한 방식입니다. 만약에 Compose Navigation을 원하시면 댓글로 남겨주세요 ㅎㅎ 빠른 시일 내로 정리해보도록 하겠습니다!
Navigation을 적용하려면 무엇이 필요할까?
우선 각 모듈별로 navigation graph가 이미 적용되어 있는 상황이라고 가정하고 진행하겠습니다. 만약에 적용이 안되신다면 하단에 레포 링크를 참고하여 구현해주세요!
다음 그림처럼 모듈화가 적용되어 있다고 가정하겠습니다.
Android 공식 홈페이지에는 멀티모듈의 Navigation을 다음과 같이 적용하라고 안내하고 있습니다.
When multiple feature modules need to reference a common set of destinations, such as a login graph, you
should not include those common destinations into each feature module's navigation graph. Instead, add those common destinations to your app module's navigation graph
멀티 feature 모듈이 login graph와 같이 많이 사용되는 목적이를 참조해야할 때, 그런 자주 사용되는 목적지를 각 feature 모듈의 navigation graph에 포함시키는 것이 아닌 app 모듈의 navigation graph에 추가해야한다.
제가 생각하기에도 feature 모듈의 navigation graph에 포함시키는 것은 해당 feature에 대한 의존성을 가지는 것이기에 적절치 않다고 생각합니다. 그렇다면 어떻게 적용시키면 좋을까요??
방법은 navigation을 처리할 수 있는 하위 모듈을 생성하는 것입니다. 최근 드로이드 나이츠 2023을 위해 만드신 안드 레포를 참고하더라도 core-navigation이 있는 것을 확인하실 수 있습니다.
저는 navigation 이외에도 다른 여러 기능을 추가하고자 common-ui라고 이름을 정했습니다.
다음과 같이 모듈을 생성하게 되면 featureA, featureB가 common-ui에 대한 의존성을 가지고 참조할 수 있게 됩니다.
Home B -> Search A로 가는 일반적인 Navigation 구현
아래 그림과 같이 화면을 이동하고 싶다면 쉬운 방법은 인터페이스를 하위 모듈에 생성하고 App에 존재하는 MainActivity에서 상속받아서 이동하는 것입니다.
다음과 같이 NavGraph가 설정되어 있다고 생각합니다.
// AppModule NavGraph
<include app:graph="@navigation/home_navigation" />
<include app:graph="@navigation/search_navigation" />
<action
android:id="@+id/action_home_to_search"
app:destination="@id/search_navigation" />
1. 하위 모듈에 Navigation Util Interface를 생성합니다.
interface NavigationUtil {
fun navigateToSearchNavigation()
}
2. App 모듈에 있는 MainActivity가 해당 Interface를 상속받습니다.
class MainActivity : AppCompatActivity(), NavigationUtil {
// navController가 생성되어 있다고 가정
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
}
override fun navigateToSearchNavigation() {
navController.navigate(R.action.action_home_to_search)
}
}
3. feature 모듈들은 하위 모듈의 interface를 참조할 수 있으므로 interface의 함수를 호출합니다.
class HomeFragment : Fragment() {
//...
binding.btnNavigateToSearch.setOnClickListener {
(activity as NavigationUtil).navigateToSearchNavigation()
}
}
Search A -> Home B로 가는 내부 id를 필요로 하는 Navigation 구현
하지만 다음 그림처럼 NavGraph 내부에 존재하는 Fragment에는 어떻게 접근해야할까요?
안드로이드 공식 홈페이지에서는 딥링크를 사용하라고 안내해줍니다.
At compile time, independent feature modules cannot see each other, so you can't use IDs to navigate to destinations in other modules. Instead, use a deep link to navigate directly to a destination that is associated with an
implicit deep link.
컴파일 시점에 의존성이 없는 feature 모듈들은 서로를 알지 못하므로 각 모듈에 포함된 id를 통해 목적지로 이동할 수 없습니다. 따라서 딥링크를 사용해서 implicit deep link와 연관된 목적지에 직접 접근해야 합니다.
따라서 저희는 최종적으로 다양한 화면 이동을 위해 딥링크를 사용하는 Navigation을 구현해야 합니다.
1. 하위 모듈(common-ui)의 리소스 values 파일에 navigation_deeplink.xml 파일을 만듭니다.
(strings.xml에 하셔도 무방하지만 다른 문자열과 분리하여 파악하기 위해 저는 따로 분리했습니다)
리소스 파일에 deep link를 작성하는 이유는 각 모듈에서도 해당 딥링크를 동일하게 사용해야 하는데 직접 입력할 경우 링크가 동일하지 않아 문제가 발생할 가능성이 존재합니다.
<!-- Home -->
<string name="home_deeplink_url">lab://home</string>
<!-- Serialization -->
<string name="serialization_gson_deeplink_url">lab://serialization/gson</string>
2. 작성된 DeepLink를 각 모듈의 NavGraph에 정의해줍니다.
// Home NavGraph
<fragment
android:id="@+id/homeFragment"
android:name="com.jgeun.feature.home.HomeFragment"
android:label="fragment_home"
tools:layout="@layout/fragment_home" >
<deepLink
app:uri="@string/home_deeplink_url" />
</fragment>
3. 하위 모듈(common-ui)에 DeepLinkDestination과 NavExtensions를 만들어줍니다.
// DeepLinkDestination.kt
sealed class DeepLinkDestination(val addressRes: Int) {
object Serialization {
object Gson : DeepLinkDestination(R.string.serialization_gson_deeplink_url)
}
object Home : DeepLinkDestination(R.string.home_deeplink_url)
}
fun DeepLinkDestination.getDeepLink(context: Context) = context.getString(this.addressRes)
// NavExtensions.kt
fun NavController.deepLinkNavigateTo(
context: Context,
deepLinkDestination: DeepLinkDestination,
popUpTo: Boolean = false
) {
val builder = NavOptions.Builder()
if (popUpTo) {
builder.setPopUpTo(graph.startDestinationId, true)
}
navigate(
buildDeppLink(context, deepLinkDestination),
builder.build()
)
}
private fun buildDeppLink(context: Context, destination: DeepLinkDestination) =
NavDeepLinkRequest.Builder
.fromUri(destination.getDeepLink(context).toUri())
.build()
이렇게 정의가 끝났다면 각 모듈에서 딥링크를 통해 화면을 이동할 수 있습니다.
마무리
이렇게 정리하다보니 그렇게 어려운 내용이 아니였던 것 같기도 하지만 그 당시 저에게는 정말 큰 벽이었습니다. 여러 아티클 (아티클1, 아티클2, 아티클3 등)들을 참고하면서 한글로 되어있지 않아 불편했고 제 프로젝트에 적합한 형태가 아니라고도 생각이 되었습니다.
이 글을 통해 다른 분들이 쉽게 구현하셨으면 좋겠습니다 :)
전체 코드를 보고 싶으시다면 레포 링크를 참고해주세요!
'Android' 카테고리의 다른 글
navHostController vs navController (0) | 2023.09.20 |
---|---|
[Android] Compose랑 Hot Stream은 어울릴까? (0) | 2023.09.19 |
[Android] 'excludes' is deprecated (0) | 2023.09.19 |
[Android] implementation과 testImplementation의 차이 (1) | 2023.09.02 |
[Android] Compose Navigation + Stateflow를 쓰는데 왜 리컴포지션이 계속 발생하지..? (1) (0) | 2023.08.07 |
댓글