[Jetpack Compose] KSP 를 활용하여 코드 자동 생성하기 (2/2)

오규성·4일 전
0

KSP

목록 보기
2/2

기존에 작성한 티스토리 글을 옮겨온 것입니다.


7. NavGraphComposable 생성하기

Processor 모듈에서 파일들을 모두 생성하였다면, 다시 NavHost 가 존재하는 모듈로 돌아와 아래의 코드들을 추가해준다.

inline fun <reified T : DevGyuNavGraph> NavGraphBuilder.devGyuNavGraphComposable(
    navController: NavController
) {
    val clazz = T::class
    check(clazz.isSealed) { "${T::class.simpleName} is not Sealed class" }
    clazz.getSealedSubclasses().forEach { type ->
        composable(
            route = type.route,
            arguments = type.arguments.map { it.namedNavArgument },
            content = { entry ->
                Scaffold(
                    modifier = Modifier.fillMaxSize(),
                    content = {
                        Box(
                            Modifier
                                .animateContentSize()
                                .padding(it)
                                .systemBarsPadding(),
                        ) {
                            type.NavigateScreen(navController, entry)
                        }
                    },
                )
            },
        )
    }
}

/**
 * KClass 형식의 Instance 를 T 형식으로 변환해준다
 * */
fun <T : Any> KClass<T>.getObjectInstance(): T = this.objectInstance as T

/**
 * T class 의 서브 클래스들을 추출한다.
 *
 * ```
 * sealed class Nav: WamooNavGraph() {
 *   data object A: Nav()
 *   data object B: Nav()
 * }
 *
 * return [A, B]
 * ```
 * @see DevGyuNavGraph
 * */
fun <T : DevGyuNavGraph> KClass<T>.getSealedSubclasses(): List<T> =
    this.sealedSubclasses.map { it.getObjectInstance() }

devGyuNavGraphComposable 의 경우 KSP 로 자동 생성되는 코드에서 사용할 함수이므로 함수명과 내부 코드들은 알아서 설정해도 된다.

8. SymbolProcessor 클래스 추가

7번 코드들을 모두 추가해주었다면 다시 Processor 모듈로 돌아가 클래스를 하나 생성해주자.

NavGraphRegisterProvidercreate 함수에서 return 하던 Processor 클래스를 추가해줄 것이다.

class NavGraphRegisterProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {
    // 패키지명은 자유롭게 설정.
    private val packageName = "devgyu.blogproject.presentation.navigation"
    private val fileAndFunctionName = "generateNavGraphComposable"
    private val navGraphComposableList = mutableListOf<String>()
    private val containingFileList = mutableListOf<KSFile>()
    override fun finish() {
        super.finish()

        val file = codeGenerator.createNewFile(
            Dependencies(true, sources = containingFileList.toTypedArray()),
            packageName,
            fileAndFunctionName
        )

        file.writer().use { writer ->
            val stringBuilder = StringBuilder()
            navGraphComposableList.forEachIndexed { index, s ->
                if(index >= 1){ stringBuilder.append("\n\t") }
                stringBuilder.append(s)
            }
            writer.write(autoNavigationFunctionFileString(stringBuilder))
        }
    }

    override fun onError() {
        super.onError()
        logger.error("NavGraphRegisterProcessor error")
    }

    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver.getSymbolsWithAnnotation(annotationName)
        val symbolGroups = symbols
            .filterIsInstance<KSClassDeclaration>()
            .groupBy { it.validate() }

        symbolGroups[true]?.forEach { classDeclaration ->
                classDeclaration.containingFile?.let { it1 -> containingFileList.add(it1) }
                classDeclaration.accept(NavGraphRegisterVisitor(
                    logger = logger,
                    annotationName = annotationName,
                    generateComposableString = { str ->
                        // 내부에서 sealed class 인지 파악 후 코드 구현 String 되돌려받음
                        if(str.isNotEmpty()){ navGraphComposableList.add(str) }
                    }
                ), Unit)
            }

        return symbolGroups[false] ?: emptyList()
    }

    private fun autoNavigationFunctionFileString(navComposableListString: StringBuilder): String{
        return """
            |package $packageName
            |    
            |import androidx.navigation.NavController
            |import androidx.navigation.NavGraphBuilder
            |import androidx.compose.material3.SnackbarHostState
            |import devgyu.blogproject.presentation.navigation.*
            |import devgyu.blogproject.presentation.navigation.graph.*
            |
            |/**
            |* @see devgyu.blogproject.processor.NavGraphRegisterProcessor
            |*/
            |fun NavGraphBuilder.${fileAndFunctionName}(navController: NavController) {
            |    ${navComposableListString}
            |}
        """.trimMargin()
    }

    companion object {
        private val annotationName = NavGraphRegister::class.qualifiedName!! // 패키지명을 포함한 어노테이션 클래스 명
    }
}

autoNavigationFunctionFileString 함수에서 | 와 trimMargin 을 넣은 이유는 저것을 넣지 않으면 자동 생성되는 코드들 앞에 공백이 생겨 예뻐보이지 않기 때문에 추가한 것이다. 중요하지는 않으므로 삭제해도 무방하다.

9. KSVisitorVoid 클래스를 상속하는 커스텀 클래스 구현

NavGraphRegisterProcessorProcess 코드가 실행될 때, KSClassDeclaration (클래스 정보) 에 접근하면 해당 클래스의 메타데이터를 추출할 수 있도록 NavGraphRegisterVisitor 라는 클래스를 구현해주자.

class NavGraphRegisterVisitor(
    private val logger: KSPLogger,
    private val annotationName: String,
    private val generateComposableString: (String) -> Unit
) : KSVisitorVoid() {
    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit){
        // 실드 클래스에서만 사용할 수 있게 설정
        if(!classDeclaration.modifiers.contains(Modifier.SEALED)){
            logger.error("${annotationName}`s target is only Sealed [class or Interface]", classDeclaration)
            return
        }

        logger.warn("generate [${classDeclaration}]'s navComposable code")
        generateComposableString("devGyuNavGraphComposable<${classDeclaration}>(navController)")
        logger.warn("generated code !!")
    }
}

우리는 이를 통해서 NavGraphRegister 어노테이션이 붙은 클래스가 Sealed class 인지 확인이 가능하고, 자동 생성될 코드 내부에 실제 클래스 이름도 전달해줄 수 있다.

10. Build.gradle.kts 에 KTS Project 추가

이제 파일 자동 생성을 위한 모든 작업은 끝이 났다.

NavHost 를 구현할 모듈의 Build.gradle.kts (나의 경우 Presentation Module) 에 다음과 같이 KSP 를 추가해준다.

11. NavHost 구현

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

        setContent {
            enableEdgeToEdge()
            GyuDefaultTheme {
                DevGyuNavigator()
            }
        }
    }
}

@Composable
fun DevGyuNavigator(){
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = NavGraphMain.MainTest.route,
    ){
        generateNavGraphComposable(navController)
    }
}

Presentation Module 로 돌아와 NavHost 를 구현하고 NavGraphRegisterProvider 에서 설정했던 fileAndFunctionName 을 추가해준다.

generacteNavGraphComposable 한 번 빌드가 진행되어야 파일이 생성되므로 참고 !! (빨간색으로 뜨더라도 그대로 빌드하면 정상 실행됨)

12. 결과물

NavGraphMain 만 존재시 NavGraphTest 라는 실드 클래스 추가 후 빌드 시 파일 변경

Navigate 시 navigate(NavGraphMain.Main.route) 로 사용.

만약 파라미터를 필요로 하는 경우 위처럼 함수를 구현하고 navigate(NavGraphMain.MainTest.parameterRoute(2)) 로 사용하면 된다 !

profile
안드로이드 개발자 Gyu 의 개발 블로그 !

0개의 댓글