Developing Myself Everyday
article thumbnail

 

이 게시글은 아래의 게시글에서 이어지는 내용입니다.

 

클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (2)

이 게시글은 아래의 게시글에서 이어지는 내용입니다. 클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기 (1) 도대체 이놈의 클린 아키텍처가 뭐길래 이렇게 저를 괴롭히는지 모르겠습니다.

everyday-develop-myself.tistory.com

 

 

Feature


이제부터는 UI에 관한 Feature 모듈을 구현해볼 예정입니다. 지금까지 만들었던 모듈들은 다 Feature 모듈에서 사용하기 위해서 만들었다고 해도 과언이 아닐것 같습니다. 그러니금까지 만들었던 모듈들이 어떻게 사용되는지 중점으로 보시면 좋을것 같습니다.

 

Feature 모듈의 기능은 사실 구현하시는 내용에 따라 많이 달라질 것이라 생각합니다. 그렇기에 여기서는 Feature 모듈의 기능에 대해서는 자세히 다루진 않고 Domain 모듈의 Usecase를 어떻게 사용하고 Feature 모듈간 화면을 어떻게 이동하는 지를 다뤄보고자 합니다.

 

제가 구현할 Feature 모듈은 메인, 로그인, 회원가입으로 나눠집니다.

 

 

 

Feature 폴더의 모듈 생성

Feature 폴더의 모듈은 Android Library 템플릿을 사용합니다.

 

 

 

 

Main 모듈


Main 모듈은 MainActivity가 있는 모듈입니다. 이 말은 사용자가 앱을 실행하면 가장 먼저 마주치게 되는 모듈이라는 말입니다. 

 

Main 모듈은 아래와 같은 구조를 가집니다.

 

 

 

`build.gradle` 파일

아래는 Main 모듈에 필요한 Gradle입니다.

plugins {
    id("jun.android.feature")
}

android {
    namespace = "com.jun.main"
}

dependencies {
    implementation(projects.feature.signin)
    implementation(projects.feature.login)

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.androidx.activity.compose)
    implementation(libs.androidx.lifecycle.runtimeCompose)
    implementation(libs.androidx.lifecycle.viewModelCompose)
    implementation(libs.kotlinx.immutable)
}

 

 

`AndroidManifest` 파일

액티비티가 있기에 당연히 Manifest에 이를 등록해줘야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">


    <application>
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:configChanges="uiMode"
            android:theme="@style/Theme.loginCAApp.TransparentSystemBar">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>

그리고 액티비티의 테마를 지정해줍니다. 테마의 경로는 우리가 저번 게시글에서 만들었던 designsystem 모듈에 있는 theme 입니다.

 

 

`MainActivity` 파일

저는 MainActivitiy에서는 따로 작업이 없고 바로 LoginScreen으로 이동시켜 주었습니다.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            Surface(
                color = MaterialTheme.colorScheme.background
            ) {
                val navController = rememberNavController()
                NavHost(
                    navController = navController,
                    startDestination = LoginRoute.route
                ) {
                    loginNavGraph(
                        onSignInClick = { navController.navigateSignIn() }
                    )
                    signInNavGraph(
                        onLoginClick = { navController.navigateLogin() }
                    )
                }
            }
        }
    }
}

 

화면 전환을 하는 방법은 바로 아래에서 설명합니다.

 

 

 

 

Feature 모듈에서의 화면 전환


멀티 모듈 프로젝트에서의 화면 전환은 앱의 각 화면 또는 Navigation Graph를 모듈별 Navigation 파일에 매핑해야 합니다.

 

기본적으로 각 화면은 화면을 그리는 Composable 함수와 네비게이션의 특정 로직간의 다리 역할을 하는 NavGraphBuilder 확장 함수가 있어야 합니다.

 

아래와 같은 로그인 UI를 그리는 LoginScreen Composable 함수가 있다고 가정해 보겠습니다.

@Composable
fun LoginScreen(
    onSignInClick: (Int) -> Unit,
    viewModel: LoginViewModel = hiltViewModel()
) {
	...
}

 

 

다음 NavGraphBuilder 확장 함수는 LoginScreen 컴포저블을 NavGraph의 대상으로 추가합니다. 그리고 해당 화면에서 다른 화면으로의 이동을 처리하기 위해 람다를 사용해서 매개변수로 이를 넘겨줍니다.

fun NavGraphBuilder.loginNavGraph(
    onSignInClick: (Int) -> Unit
) {
    composable(route = LoginRoute.route) {
        LoginScreen(
            onSignInClick = onSignInClick
        )
    }
}

 

 

또한 각 대상은 다른 대상이 안전하게 이동할 수 있도록 NavController 확장 함수를 노출해야 합니다.

fun NavController.navigateLogin() {
    navigate(LoginRoute.route)
}

object LoginRoute {
    const val route = "login"
}

 

 

이렇게 Navigation Graph를 추가하면 앱 수준의 NavHost에 포함됩니다. 

val navController = rememberNavController()
NavHost(
    navController = navController,
    startDestination = LoginRoute.route
) {
    loginNavGraph(
        onSignInClick = { navController.navigateSignIn() }
    )
    signInNavGraph(
        onLoginClick = { navController.navigateLogin() }
    )
}

 

 

이렇게 하면 모듈간에 안전하게 화면 전환을 할 수 있게 됩니다. 다만, 각 화면으로 이동하는 확장 함수는 화면이 있는 모듈에 정의되어 있습니다. 그렇기에 각 화면에 있는 확장 함수를 사용하기 위한 의존성이 생기게 됩니다.

 

 

 

 

Usecase 사용


Feature 모듈에서는 각각의 ViewModel에서 Usecase를 생성자로 주입받습니다. 이렇게 주입받은 Usecase를 통해 작업을 할 수 있습니다.

@HiltViewModel
class SignInViewModel @Inject constructor(
    private val signInUseCase: SignInUsecase
) : ViewModel() {

    private val _userEmail = mutableStateOf("")
    val userEmail: State<String> = _userEmail

    private val _userPassword = mutableStateOf("")
    val userPassword: State<String> = _userPassword

    private val _eventFlow = MutableSharedFlow<UiEvent>()
    val eventFlow = _eventFlow.asSharedFlow()

    fun onEvent(event: SignInEvent) {
        when (event) {
            is SignInEvent.EnteredEmail -> {
                _userEmail.value = event.value

            }
            is SignInEvent.EnteredPassword -> {
                _userPassword.value = event.value
            }
            is SignInEvent.SignIn -> {
                viewModelScope.launch {
                    try {
                        signInUseCase(
                            User(
                                email = userEmail.value,
                                password = userPassword.value
                            )
                        )
                        _eventFlow.emit(UiEvent.SignIn)
                    } catch (e: InvalidUserException) {
                        _eventFlow.emit(
                            UiEvent.ShowSnackBar(
                                message = e.message ?: "회원가입할 수 없습니다."
                            )
                        )
                    }
                }
            }
        }
    }

    sealed class UiEvent {
        data class ShowSnackBar(val message: String) : UiEvent()
        object SignIn : UiEvent()
    }
}

 

 

 

 

마무리


 

 

이렇게 해서 위와 같은 의존성을 가지는 안드로이드가 권장하는 멀티 모듈에 로그인 기능을 구현해 봤습니다. 현재 구현한 프로젝트의 기능은 아직 많지 않습니다. 하지만 이렇게 전체적인 구조를 잘 구축해 놓으면 새로운 기능을 추가하는 것은 매우 쉽습니다.

 

만약 로그인 기능 다음에 무언가를 추가한다면, 그냥 Feature 모듈에 새로운 모듈을 추가하고 필요한 데이터가 있다면 core 폴더에서 추가하면 됩니다. 

 

이는 팀 프로젝트에서도 매우 유용합니다. 여러명의 개발자들이 하나의 Feature를 맡아서 작업한다고 가정해 보겠습니다. 개발자들은 다른 Feature에 대해 잘 몰라도 자신의 Feature를 개발할 수 있습니다. 다른 Feature의 스크린으로 이동하거나 데이터를 넘겨주기만 하면 다른 개발자가 그걸 받아서 알아서 처리할 것입니다.

 

이러한 장점 때문에 많은 기업들이 멀티 모듈 프로젝트를 사용하고 있지 않나 생각이 듭니다. 

 

이번에 작성한 3개의 "클린 아키텍처와 안드로이드 권장 멀티 모듈 적용하기" 게시글은 안드로이드 개발자를 목표로 하고 있는 취준생이 공부를 목적으로 작성한 글입니다. 그렇기에 만약 틀린 부분이 있다면 꼭 말씀해주시면 감사하겠습니다.

 

 

 

전체 코드

 

GitHub - Iwillbeagood/Login-CleanArchitecture: CleanArchitecture Multi Module Login App

CleanArchitecture Multi Module Login App. Contribute to Iwillbeagood/Login-CleanArchitecture development by creating an account on GitHub.

github.com

 

 

 

 

Reference

 

Kotlin DSL 및 Navigation Compose의 유형 안전성  |  Android 개발자  |  Android Developers

Kotlin DSL 및 Navigation Compose의 유형 안전성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에는 Navigation Kotlin DSL과 Navigation Compose에 런타임 유형 안전

developer.android.com

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!