diff --git a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt index 337c4216..061a1d9b 100644 --- a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt +++ b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/GenerateVersionsYaml.kt @@ -30,6 +30,8 @@ class GenerateVersionsYaml { | activity: ${versions.android.activity} | constraintlayout: ${versions.android.constraintlayout} | work: ${versions.android.work} + | room: ${versions.android.room} + | roomDatabase: ${versions.android.roomDatabase} | hilt: ${versions.android.hilt} | hiltAandroidx: ${versions.android.hiltAandroidx} | metro: ${versions.android.metro} diff --git a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt index fc48cd51..55967664 100644 --- a/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt +++ b/cli/src/main/kotlin/io/github/cdsap/projectgenerator/cli/Main.kt @@ -58,13 +58,14 @@ class GenerateProjects : CliktCommand(name = "generate-project") { private val projectName by option() private val develocityUrl by option() private val agp9 by option().flag(default = false) + private val roomDatabase by option("--room-database").flag(default = false) override fun run() { val typeOfProjectRequested = TypeProjectRequested.valueOf(type.uppercase()) val shape = Shape.valueOf(shape.uppercase()) val dependencyInjection = DependencyInjection.valueOf(di.uppercase()) - val versions = getVersions(versionsFile, develocityUrl, agp9).copy(di = dependencyInjection) + val versions = getVersions(versionsFile, develocityUrl, agp9, roomDatabase).copy(di = dependencyInjection) val develocityEnabled = getDevelocityEnabled(develocity, develocityUrl) ProjectGenerator( modules, @@ -103,7 +104,7 @@ class GenerateProjects : CliktCommand(name = "generate-project") { } } - private fun getVersions(fileVersions: File?, develocityUrl: String?, agp9: Boolean): Versions { + private fun getVersions(fileVersions: File?, develocityUrl: String?, agp9: Boolean, roomDatabase: Boolean): Versions { val versions = if (fileVersions != null) { parseYaml(fileVersions) } else { @@ -111,16 +112,21 @@ class GenerateProjects : CliktCommand(name = "generate-project") { // if the version is provided by file this logic is not executed if (agp9) { val versions = Versions() - versions.copy(android = Android(agp = versions.android.agp9)) + versions.copy(android = versions.android.copy(agp = versions.android.agp9)) } else { Versions() } } - return if (develocityUrl != null) { - versions.copy(project = versions.project.copy(develocityUrl = develocityUrl)) + val withRoomDatabase = if (roomDatabase) { + versions.copy(android = versions.android.copy(roomDatabase = true)) } else { versions } + return if (develocityUrl != null) { + withRoomDatabase.copy(project = withRoomDatabase.project.copy(develocityUrl = develocityUrl)) + } else { + withRoomDatabase + } } } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/android/AndroidApplication.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/android/AndroidApplication.kt index f880a7e1..616af2c4 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/android/AndroidApplication.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/android/AndroidApplication.kt @@ -14,7 +14,8 @@ class AndroidApplication { node: ProjectGraph, lang: LanguageAttributes, di: DependencyInjection, - dictionary: MutableMap> + dictionary: MutableMap>, + roomDatabase: Boolean = false ) { val layerDir = NameMappings.layerName(node.layer) val moduleDir = NameMappings.moduleName(node.id) @@ -86,22 +87,69 @@ class AndroidApplication { ?.firstOrNull { it.type == ClassTypeAndroid.VIEWMODEL } ?.className ?: "Viewmodel${moduleNumber}_1" - """ - |package com.awesomeapp.$packageDir - | - |import android.app.Application - |import dev.zacsweers.metro.DependencyGraph - |import dev.zacsweers.metro.createGraph - | - |@DependencyGraph - |interface AppGraph { - | val viewModel: $viewModel - |} - | - |class MainApplication : Application() { - | val graph: AppGraph by lazy { createGraph() } - |} - """.trimMargin() + if (roomDatabase) { + val databaseClass = dictionary[moduleId] + ?.firstOrNull { it.type == ClassTypeAndroid.DATABASE } + ?.className + ?: "Database${moduleNumber}_1" + val daoClass = dictionary[moduleId] + ?.firstOrNull { it.type == ClassTypeAndroid.DAO } + ?.className + ?: "Dao${moduleNumber}_1" + """ + |package com.awesomeapp.$packageDir + | + |import android.app.Application + |import android.content.Context + |import androidx.room.Room + |import dev.zacsweers.metro.DependencyGraph + |import dev.zacsweers.metro.Provides + |import dev.zacsweers.metro.createGraphFactory + | + |@DependencyGraph + |interface AppGraph { + | val viewModel: $viewModel + | + | @DependencyGraph.Factory + | fun interface Factory { + | fun create(@Provides context: Context): AppGraph + | } + | + | @Provides + | fun provideDatabase(context: Context): $databaseClass { + | return Room.databaseBuilder(context, $databaseClass::class.java, "$moduleId.db") + | .fallbackToDestructiveMigration() + | .build() + | } + | + | @Provides + | fun provideDao(db: $databaseClass): $daoClass { + | return db.dao() + | } + |} + | + |class MainApplication : Application() { + | val graph: AppGraph by lazy { createGraphFactory().create(this) } + |} + """.trimMargin() + } else { + """ + |package com.awesomeapp.$packageDir + | + |import android.app.Application + |import dev.zacsweers.metro.DependencyGraph + |import dev.zacsweers.metro.createGraph + | + |@DependencyGraph + |interface AppGraph { + | val viewModel: $viewModel + |} + | + |class MainApplication : Application() { + | val graph: AppGraph by lazy { createGraph() } + |} + """.trimMargin() + } } DependencyInjection.NONE -> """ diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/buildfiles/BuildFilesGeneratorAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/buildfiles/BuildFilesGeneratorAndroid.kt index c61b72c9..054f238d 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/buildfiles/BuildFilesGeneratorAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/buildfiles/BuildFilesGeneratorAndroid.kt @@ -41,7 +41,7 @@ class BuildFilesGeneratorAndroid( } } - val deps = AndroidToml().tomlImplementations(versions, di) + val deps = AndroidToml().tomlImplementations(versions, di, versions.android.roomDatabase) return """ |plugins { | id("awesome.androidapp.plugin") @@ -59,7 +59,7 @@ ${testImplementations.joinToString("\n").prependIndent(" ")} private fun createAndroidLibBuildFile(node: ProjectGraph, generateUnitTests: Boolean): String { val implementations = mutableSetOf() val testImplementations = mutableSetOf() - val deps = AndroidToml().tomlImplementations(versions, di) + val deps = AndroidToml().tomlImplementations(versions, di, versions.android.roomDatabase) // Add direct dependencies first (only from different layers) node.nodes.forEach { dependency -> if (dependency.layer != node.layer) { diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt index a90d2311..77fc4108 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt @@ -69,96 +69,51 @@ class ClassGeneratorAndroid( ) { val packageName = "com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)}" val moduleName = "Module_${moduleDefinition.moduleNumber}" - // Create imports for this module's classes val classImports = StringBuilder() val provideMethods = mutableListOf() - moduleDefinition.classes.forEach { classDefinition -> - if (classDefinition.type != ClassTypeAndroid.STATE) { // Skip STATE type classes - val className = - "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" - classImports.appendLine("import $packageName.$className") - - when (classDefinition.type) { - ClassTypeAndroid.API -> { - provideMethods.add( - """ - | @Provides - | @Singleton - | fun provide$className(): $className { - | return $className() - | } - """.trimMargin() - ) - } - - ClassTypeAndroid.REPOSITORY -> { - // For repositories, we need to provide their API dependencies - if (classDefinition.dependencies.isNotEmpty()) { - val xa = mutableListOf() - classDefinition.dependencies.mapIndexed { index, dep -> - val depModuleId = dep.sourceModuleId - val s = a.filter { it.key == depModuleId } - if (s.isNotEmpty()) { - val x = s.values.flatten().first { it.type == ClassTypeAndroid.API } - if (x != null) { - - val apiClassName = x.className - classImports.appendLine( - "import com.awesomeapp.${ - NameMappings.modulePackageName( - dep.sourceModuleId - ) - }.$apiClassName" - ) - xa.add("api$index: $apiClassName = $apiClassName()") - - } - } - } - val constructorParams = xa.joinToString(",\n ") - - provideMethods.add( - """ - | @Provides - | @Singleton - | fun provide$className( - | $constructorParams - | ): $className { - | return $className(${ - constructorParams.split(",").map { it.split(":").first() }.joinToString(", ") - }) - | } - """.trimMargin() - ) - } else { - provideMethods.add( - """ - | @Provides - | @Singleton - | fun provide$className(): $className { - | return $className() - | } - """.trimMargin() - ) - } - } - - else -> { - } + val databaseClass = moduleDefinition.classes.firstOrNull { it.type == ClassTypeAndroid.DATABASE } + ?.let { "${it.type.className()}${moduleDefinition.moduleNumber}_${it.index}" } + val daoClass = moduleDefinition.classes.firstOrNull { it.type == ClassTypeAndroid.DAO } + ?.let { "${it.type.className()}${moduleDefinition.moduleNumber}_${it.index}" } + + if (databaseClass != null) { + classImports.appendLine("import $packageName.$databaseClass") + provideMethods.add( + """ + | @Provides + | @Singleton + | fun provide$databaseClass(@ApplicationContext context: Context): $databaseClass { + | return Room.databaseBuilder(context, $databaseClass::class.java, "${moduleDefinition.moduleId}.db") + | .fallbackToDestructiveMigration() + | .build() + | } + """.trimMargin() + ) + } - } - } + if (daoClass != null && databaseClass != null) { + classImports.appendLine("import $packageName.$daoClass") + provideMethods.add( + """ + | @Provides + | fun provide$daoClass(db: $databaseClass): $daoClass { + | return db.dao() + | } + """.trimMargin() + ) } - // Only create the module if we have something to provide if (provideMethods.isNotEmpty()) { val content = """ |package $packageName.di | + |import android.content.Context + |import androidx.room.Room |import dagger.Module |import dagger.Provides |import dagger.hilt.InstallIn + |import dagger.hilt.android.qualifiers.ApplicationContext |import dagger.hilt.components.SingletonComponent |import javax.inject.Singleton |${classImports.toString().trim()} @@ -183,7 +138,6 @@ class ClassGeneratorAndroid( File("$diPackagePath/$moduleName.kt").writeText(content) } } - private fun generateClassContent( classDefinition: ClassDefinitionAndroid, moduleDefinition: ModuleClassDefinitionAndroid, @@ -196,22 +150,28 @@ class ClassGeneratorAndroid( ClassTypeAndroid.VIEWMODEL -> generateViewModel( packageName, className, + moduleDefinition, classDefinition.dependencies, a ) ClassTypeAndroid.REPOSITORY -> generateRepository(packageName, className, classDefinition.dependencies, a) ClassTypeAndroid.API -> generateApi(packageName, className) + ClassTypeAndroid.ENTITY -> generateEntity(packageName, className) + ClassTypeAndroid.DAO -> generateDao(packageName, className, moduleDefinition, a) + ClassTypeAndroid.DATABASE -> generateDatabase(packageName, className, moduleDefinition, a) ClassTypeAndroid.WORKER -> generateWorker(packageName, className) - ClassTypeAndroid.ACTIVITY -> generateComposeActivity(packageName, className, moduleDefinition.moduleNumber) + ClassTypeAndroid.ACTIVITY -> generateComposeActivity(packageName, className, moduleDefinition, a) ClassTypeAndroid.FRAGMENT -> generateFragment(packageName, className) ClassTypeAndroid.SERVICE -> generateService(packageName, className) - ClassTypeAndroid.STATE -> generateState(packageName, className) + ClassTypeAndroid.STATE -> generateState(packageName, className, moduleDefinition, a) + ClassTypeAndroid.SCREEN -> generateScreen(packageName, className, moduleDefinition, a) ClassTypeAndroid.MODEL -> generateModel(packageName, className) ClassTypeAndroid.USECASE -> generateUseCase( packageName, className, - moduleDefinition.moduleNumber.toString() + moduleDefinition, + a ) else -> throw IllegalArgumentException("Unsupported class type: ${classDefinition.type}") @@ -221,6 +181,7 @@ class ClassGeneratorAndroid( private fun generateViewModel( packageName: String, className: String, + moduleDefinition: ModuleClassDefinitionAndroid, dependencies: List, a: MutableMap> ): String { @@ -229,9 +190,7 @@ class ClassGeneratorAndroid( appendLine("import androidx.lifecycle.ViewModel") appendLine("import androidx.lifecycle.viewModelScope") appendLine("import kotlinx.coroutines.launch") - appendLine("import kotlinx.coroutines.coroutineScope") - appendLine("import kotlinx.coroutines.async") - appendLine("import kotlinx.coroutines.awaitAll") + appendLine("import kotlinx.coroutines.flow.collectLatest") appendLine("import kotlinx.coroutines.flow.MutableStateFlow") appendLine("import kotlinx.coroutines.flow.StateFlow") appendLine("import kotlinx.coroutines.flow.asStateFlow") @@ -246,47 +205,13 @@ class ClassGeneratorAndroid( } DependencyInjection.NONE -> {} } - dependencies.forEach { dep -> - val depModuleId = dep.sourceModuleId - val s = a.filter { it.key == depModuleId } - if (s.isNotEmpty()) { - val x = s.values.flatten().firstOrNull { it.type == ClassTypeAndroid.REPOSITORY } - if (x != null) { - val repoClassName = x.className - appendLine("import com.awesomeapp.${NameMappings.modulePackageName(dep.sourceModuleId)}.$repoClassName") - } - } - } } - val constructorParams = dependencies.mapIndexed { index, dep -> - val s = a.filter { it.key == dep.sourceModuleId }.values.flatten() - .first { it.type == ClassTypeAndroid.REPOSITORY } - val repoClassName = s.className - "private val repository$index: $repoClassName" - }.joinToString(",\n ") - - // Step 1: Generate the data fetching logic as a clean, un-indented block using trimIndent(). - val dataAssignmentBlock = if (dependencies.isEmpty()) { - """ - val data = "Data from $className" - """.trimIndent() - } else { - val lambdaList = dependencies.indices.joinToString(",\n ") { - "{ repository$it.getData() }" - } - """ - val data = coroutineScope { - val fetchers = listOf String>( - $lambdaList - ) - val results = fetchers.map { fetcher -> - async { fetcher() } - }.awaitAll() - results.joinToString("") - } - """.trimIndent() - } + val moduleId = dependencies.firstOrNull()?.sourceModuleId ?: "" + val useCaseClass = findClassName(a, moduleId, ClassTypeAndroid.USECASE) ?: "Usecase${moduleDefinition.moduleNumber}_1" + val stateClass = findClassName(a, moduleId, ClassTypeAndroid.STATE) ?: "State${moduleDefinition.moduleNumber}_1" + val modelClass = findClassName(a, moduleId, ClassTypeAndroid.MODEL) ?: "Model${moduleDefinition.moduleNumber}_1" + val constructorParams = "private val useCase: $useCaseClass" val diAnnotation = when (di) { DependencyInjection.HILT -> "@HiltViewModel" @@ -306,16 +231,14 @@ class ClassGeneratorAndroid( |class $className ${injectAnnotation}constructor( | $constructorParams |) : ViewModel() { - | private val _state = MutableStateFlow("") - | val state: StateFlow = _state.asStateFlow() + | private val _state = MutableStateFlow($stateClass()) + | val state: StateFlow<$stateClass> = _state.asStateFlow() | | init { | viewModelScope.launch(Dispatchers.IO) { - | try { - |${dataAssignmentBlock.prependIndent(" ")} - | _state.emit(data) - | } catch (e: Exception) { - | _state.emit("Error: ${'$'}{e.message}") + | useCase.seedIfEmpty() + | useCase().collectLatest { items: List<$modelClass> -> + | _state.emit(_state.value.copy(items = items, isLoading = false)) | } | } | } @@ -332,9 +255,8 @@ class ClassGeneratorAndroid( val imports = buildString { appendLine("import kotlinx.coroutines.Dispatchers") appendLine("import kotlinx.coroutines.withContext") - appendLine("import kotlinx.coroutines.coroutineScope") - appendLine("import kotlinx.coroutines.async") - appendLine("import kotlinx.coroutines.awaitAll") + appendLine("import kotlinx.coroutines.flow.Flow") + appendLine("import kotlinx.coroutines.flow.map") when (di) { DependencyInjection.HILT -> { appendLine("import javax.inject.Inject") @@ -345,40 +267,13 @@ class ClassGeneratorAndroid( } DependencyInjection.NONE -> {} } - dependencies.forEach { dep -> - val depModuleId = dep.sourceModuleId - val matches = a.filter { it.key == depModuleId } - if (matches.isNotEmpty()) { - val apiClass = matches.values.flatten().firstOrNull { it.type == ClassTypeAndroid.API } - if (apiClass != null) { - appendLine("import com.awesomeapp.${NameMappings.modulePackageName(depModuleId)}.${apiClass.className}") - } - } - } } - val constructorParams = dependencies.mapIndexedNotNull { index, dep -> - val depModuleId = dep.sourceModuleId - val apiClass = a[depModuleId]?.firstOrNull { it.type == ClassTypeAndroid.API } - apiClass?.let { "private val api$index: ${it.className}" } - }.joinToString(",\n ") - - val dataFetchLogic = if (dependencies.isEmpty()) { - "\"Data from $className Repository\"" - } else { - val lambdaList = dependencies.indices.joinToString(",\n ") { "{ api$it.fetchData() }" } - """ - coroutineScope { - val apis = listOf String>( - $lambdaList - ) - val results = apis.map { fetcher -> - async { fetcher() } - }.awaitAll() - results.joinToString("") - } - """.trimIndent() - } + val moduleId = dependencies.firstOrNull()?.sourceModuleId ?: "" + val daoClass = findClassName(a, moduleId, ClassTypeAndroid.DAO) ?: "Dao1_1" + val entityClass = findClassName(a, moduleId, ClassTypeAndroid.ENTITY) ?: "Entity1_1" + val modelClass = findClassName(a, moduleId, ClassTypeAndroid.MODEL) ?: "Model1_1" + val constructorParams = "private val dao: $daoClass" val singletonAnnotation = if (di == DependencyInjection.HILT) "@Singleton" else "" val injectAnnotation = when (di) { @@ -395,40 +290,44 @@ class ClassGeneratorAndroid( |class $className ${injectAnnotation}constructor( | $constructorParams |) { - | suspend fun getData(): String = withContext(Dispatchers.IO) { - |${dataFetchLogic.prependIndent(" ")} + | fun observeItems(): Flow> = dao.observeAll() + | .map { entities -> entities.map { it.toModel() } } + | + | suspend fun seedIfEmpty() = withContext(Dispatchers.IO) { + | if (dao.count() == 0) { + | dao.upsertAll( + | listOf( + | $entityClass(id = 1, title = "Welcome", updatedAt = System.currentTimeMillis()), + | $entityClass(id = 2, title = "Getting started", updatedAt = System.currentTimeMillis()) + | ) + | ) + | } + | } + | + | private fun $entityClass.toModel(): $modelClass { + | return $modelClass(id = id, title = title) | } |} """.trimMargin() } - private fun generateFragment(packageName: String, className: String): String { val moduleId = packageName.split(".").last() - val moduleNumber = moduleId.split("_").last() + val layoutName = "fragment_feature_${NameMappings.modulePackageName(moduleId).lowercase()}" val imports = buildString { appendLine("import android.os.Bundle") appendLine("import android.view.LayoutInflater") appendLine("import android.view.View") appendLine("import android.view.ViewGroup") - appendLine("import androidx.compose.foundation.layout.Box") - appendLine("import androidx.compose.foundation.layout.fillMaxSize") - appendLine("import androidx.compose.material3.Text") - appendLine("import androidx.compose.runtime.Composable") - appendLine("import androidx.compose.runtime.collectAsState") - appendLine("import androidx.compose.runtime.getValue") - appendLine("import androidx.compose.ui.Alignment") - appendLine("import androidx.compose.ui.Modifier") - appendLine("import androidx.compose.ui.platform.ComposeView") + appendLine("import android.widget.TextView") appendLine("import androidx.fragment.app.Fragment") - appendLine("import androidx.fragment.app.viewModels") + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(moduleId)}.R") if (di == DependencyInjection.HILT) { appendLine("import dagger.hilt.android.AndroidEntryPoint") } } val entryPointAnnotation = if (di == DependencyInjection.HILT) "@AndroidEntryPoint" else "" - val viewModelClass = "Feature${moduleNumber}_1" return """ |package $packageName @@ -443,11 +342,12 @@ class ClassGeneratorAndroid( | container: ViewGroup?, | savedInstanceState: Bundle? | ): View { - | return ComposeView(requireContext()).apply { - | setContent { + | return inflater.inflate(R.layout.$layoutName, container, false) + | } | - | } - | } + | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + | super.onViewCreated(view, savedInstanceState) + | view.findViewById(R.id.text_feature)?.text = "Feature $moduleId" | } |} """.trimMargin() @@ -479,6 +379,119 @@ class ClassGeneratorAndroid( """.trimMargin() } + private fun generateEntity(packageName: String, className: String): String { + return """ + |package $packageName + | + |import androidx.room.Entity + |import androidx.room.PrimaryKey + | + |@Entity(tableName = "items") + |data class $className( + | @PrimaryKey val id: Long, + | val title: String, + | val updatedAt: Long + |) + """.trimMargin() + } + + private fun generateDao( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val entityClass = findClassName(a, moduleId, ClassTypeAndroid.ENTITY) ?: "Entity${moduleDefinition.moduleNumber}_1" + return """ + |package $packageName + | + |import androidx.room.Dao + |import androidx.room.Insert + |import androidx.room.OnConflictStrategy + |import androidx.room.Query + |import kotlinx.coroutines.flow.Flow + | + |@Dao + |interface $className { + | @Query("SELECT * FROM items ORDER BY updatedAt DESC") + | fun observeAll(): Flow> + | + | @Query("SELECT COUNT(*) FROM items") + | suspend fun count(): Int + | + | @Insert(onConflict = OnConflictStrategy.REPLACE) + | suspend fun upsertAll(items: List<$entityClass>) + |} + """.trimMargin() + } + + private fun generateDatabase( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val entityClass = findClassName(a, moduleId, ClassTypeAndroid.ENTITY) ?: "Entity${moduleDefinition.moduleNumber}_1" + val daoClass = findClassName(a, moduleId, ClassTypeAndroid.DAO) ?: "Dao${moduleDefinition.moduleNumber}_1" + return """ + |package $packageName + | + |import androidx.room.Database + |import androidx.room.RoomDatabase + | + |@Database(entities = [$entityClass::class], version = 1, exportSchema = false) + |abstract class $className : RoomDatabase() { + | abstract fun dao(): $daoClass + |} + """.trimMargin() + } + + private fun generateScreen( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val viewModelClass = findClassName(a, moduleId, ClassTypeAndroid.VIEWMODEL) ?: "Viewmodel${moduleDefinition.moduleNumber}_1" + val modelClass = findClassName(a, moduleId, ClassTypeAndroid.MODEL) ?: "Model${moduleDefinition.moduleNumber}_1" + return """ + |package $packageName + | + |import androidx.compose.foundation.layout.Box + |import androidx.compose.foundation.layout.fillMaxSize + |import androidx.compose.foundation.lazy.LazyColumn + |import androidx.compose.foundation.lazy.items + |import androidx.compose.material3.CircularProgressIndicator + |import androidx.compose.material3.Text + |import androidx.compose.runtime.Composable + |import androidx.compose.runtime.collectAsState + |import androidx.compose.runtime.getValue + |import androidx.compose.ui.Alignment + |import androidx.compose.ui.Modifier + | + |@Composable + |fun $className(viewModel: $viewModelClass) { + | val state by viewModel.state.collectAsState() + | + | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + | if (state.isLoading) { + | CircularProgressIndicator() + | } else { + | LazyColumn { + | items(state.items) { item: $modelClass -> + | Text(text = item.title) + | } + | } + | } + | } + |} + """.trimMargin() + } + + private fun generateWorker(packageName: String, className: String): String { if (di != DependencyInjection.HILT) { return """ @@ -531,23 +544,30 @@ class ClassGeneratorAndroid( """.trimMargin() } - private fun generateComposeActivity(packageName: String, className: String, moduleNumber: Int): String { - val moduleId = packageName.split(".").last() - val viewModelClass = "Viewmodel${moduleNumber}_1" + private fun generateComposeActivity( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val viewModelClass = findClassName(a, moduleId, ClassTypeAndroid.VIEWMODEL) ?: "Viewmodel${moduleDefinition.moduleNumber}_1" + val screenClass = findClassName(a, moduleId, ClassTypeAndroid.SCREEN) ?: "Screen${moduleDefinition.moduleNumber}_1" + val useCaseClass = findClassName(a, moduleId, ClassTypeAndroid.USECASE) ?: "Usecase${moduleDefinition.moduleNumber}_1" + val repositoryClass = findClassName(a, moduleId, ClassTypeAndroid.REPOSITORY) ?: "Repository${moduleDefinition.moduleNumber}_1" + val daoClass = findClassName(a, moduleId, ClassTypeAndroid.DAO) ?: "Dao${moduleDefinition.moduleNumber}_1" + val databaseClass = findClassName(a, moduleId, ClassTypeAndroid.DATABASE) ?: "Database${moduleDefinition.moduleNumber}_1" val imports = buildString { appendLine("import android.os.Bundle") appendLine("import androidx.activity.ComponentActivity") appendLine("import androidx.activity.compose.setContent") appendLine("import androidx.activity.viewModels") - appendLine("import androidx.compose.foundation.layout.Box") - appendLine("import androidx.compose.foundation.layout.fillMaxSize") - appendLine("import androidx.compose.material3.Text") - appendLine("import androidx.compose.runtime.Composable") - appendLine("import androidx.compose.runtime.collectAsState") - appendLine("import androidx.compose.runtime.getValue") - appendLine("import androidx.compose.ui.Alignment") - appendLine("import androidx.compose.ui.Modifier") + if (di == DependencyInjection.NONE) { + appendLine("import androidx.lifecycle.ViewModel") + appendLine("import androidx.lifecycle.ViewModelProvider") + appendLine("import androidx.room.Room") + } appendLine("import com.awesomeapp.${NameMappings.modulePackageName(moduleId)}.ui.theme.FeatureTheme") if (di == DependencyInjection.HILT) { appendLine("import dagger.hilt.android.AndroidEntryPoint") @@ -555,6 +575,33 @@ class ClassGeneratorAndroid( } val entryPointAnnotation = if (di == DependencyInjection.HILT) "@AndroidEntryPoint" else "" + val manualWiring = if (di == DependencyInjection.NONE) { + """ + | private val database: $databaseClass by lazy { + | Room.databaseBuilder(applicationContext, $databaseClass::class.java, "${moduleDefinition.moduleId}.db") + | .fallbackToDestructiveMigration() + | .build() + | } + | private val dao: $daoClass by lazy { database.dao() } + | private val repository: $repositoryClass by lazy { $repositoryClass(dao) } + | private val useCase: $useCaseClass by lazy { $useCaseClass(repository) } + | + | private val viewModelFactory: ViewModelProvider.Factory by lazy { + | object : ViewModelProvider.Factory { + | @Suppress("UNCHECKED_CAST") + | override fun create(modelClass: Class): T { + | if (modelClass.isAssignableFrom($viewModelClass::class.java)) { + | return $viewModelClass(useCase) as T + | } + | throw IllegalArgumentException("Unknown ViewModel class: ${'$'}modelClass") + | } + | } + | } + | private val viewModel: $viewModelClass by viewModels { viewModelFactory } + """.trimMargin() + } else { + " private val viewModel: $viewModelClass by viewModels()" + } return """ |package $packageName @@ -563,26 +610,17 @@ class ClassGeneratorAndroid( | |$entryPointAnnotation |class $className : ComponentActivity() { - | private val viewModel: $viewModelClass by viewModels() + |$manualWiring | | override fun onCreate(savedInstanceState: Bundle?) { | super.onCreate(savedInstanceState) | setContent { | FeatureTheme { - | FeatureScreen_${className}(viewModel) + | $screenClass(viewModel) | } | } | } |} - | - |@Composable - |fun FeatureScreen_${className}(viewModel: $viewModelClass) { - | val state by viewModel.state.collectAsState() - | - | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - | Text(text = state) - | } - |} """.trimMargin() } @@ -626,37 +664,41 @@ class ClassGeneratorAndroid( """.trimMargin() } - private fun generateState(packageName: String, className: String): String { + private fun generateState( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val modelClass = findClassName(a, moduleId, ClassTypeAndroid.MODEL) ?: "Model${moduleDefinition.moduleNumber}_1" return """ |package $packageName | - |sealed class $className { - | data object Loading : $className() - | data class Success(val data: String) : $className() - | data class Error(val message: String) : $className() - | - | companion object { - | fun loading() = Loading - | fun success(data: String) = Success(data) - | fun error(message: String) = Error(message) - | } - |} + |data class $className( + | val items: List<$modelClass> = emptyList(), + | val isLoading: Boolean = true, + | val error: String? = null + |) """.trimMargin() } - private fun generateModel(packageName: String, className: String): String { return """ |package $packageName | |data class $className( - | val id: String = "$className-${System.currentTimeMillis()}", - | val name: String = "Model for $className", - | val description: String = "Description for $className" + | val id: Long, + | val title: String |) """.trimMargin() } - private fun generateUseCase(packageName: String, className: String, moduleNumber: String): String { + private fun generateUseCase( + packageName: String, + className: String, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { val injectImport = when (di) { DependencyInjection.HILT -> "import javax.inject.Inject" DependencyInjection.METRO -> "import dev.zacsweers.metro.Inject" @@ -666,21 +708,35 @@ class ClassGeneratorAndroid( DependencyInjection.HILT, DependencyInjection.METRO -> "@Inject " DependencyInjection.NONE -> "" } + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val repositoryClass = findClassName(a, moduleId, ClassTypeAndroid.REPOSITORY) ?: "Repository${moduleDefinition.moduleNumber}_1" + val modelClass = findClassName(a, moduleId, ClassTypeAndroid.MODEL) ?: "Model${moduleDefinition.moduleNumber}_1" + return """ |package $packageName | |import kotlinx.coroutines.flow.Flow - |import kotlinx.coroutines.flow.flow |$injectImport | - |class $className ${injectAnnotation}constructor() { - | operator fun invoke(): Flow = flow { - | emit("Data from $className UseCase") - | } + |class $className ${injectAnnotation}constructor( + | private val repository: $repositoryClass + |) { + | operator fun invoke(): Flow> = repository.observeItems() + | + | suspend fun seedIfEmpty() = repository.seedIfEmpty() |} """.trimMargin() } + private fun findClassName( + classesDictionary: MutableMap>, + moduleId: String, + type: ClassTypeAndroid + ): String? { + if (moduleId.isBlank()) return null + return classesDictionary[moduleId]?.firstOrNull { it.type == type }?.className + } + private fun writeClassFile( content: String, classDefinition: ClassDefinitionAndroid, diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroidLegacy.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroidLegacy.kt new file mode 100644 index 00000000..d5bcfdbf --- /dev/null +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroidLegacy.kt @@ -0,0 +1,729 @@ +package io.github.cdsap.projectgenerator.generator.classes + +import io.github.cdsap.projectgenerator.model.* +import io.github.cdsap.projectgenerator.NameMappings +import io.github.cdsap.projectgenerator.writer.ClassGenerator +import java.io.File +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.text.appendLine + +class ClassGeneratorAndroidLegacy( + private val di: DependencyInjection +) : + ClassGenerator { + + + override fun obtainClassesGenerated( + moduleDefinition: ModuleClassDefinitionAndroid, + classesDictionary: MutableMap> + ): MutableMap> { + val a = CopyOnWriteArrayList() + moduleDefinition.classes.forEach { classDefinition -> + val className = + "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" + a.add( + GenerateDictionaryAndroid( + className, + classDefinition.type, + classDefinition.index, + classDefinition.dependencies + ) + ) + } + classesDictionary[moduleDefinition.moduleId] = a + return classesDictionary + + } + + override fun generate( + moduleDefinition: ModuleClassDefinitionAndroid, + projectName: String, + a: MutableMap> + ) { + + moduleDefinition.classes.forEach { classDefinition -> + val classContent = generateClassContent(classDefinition, moduleDefinition, a) + writeClassFile(classContent, classDefinition, moduleDefinition, projectName) + } + + if (di == DependencyInjection.HILT) { + createDaggerModule(moduleDefinition, projectName, a) + } + + } + + private fun createDaggerModule( + moduleDefinition: ModuleClassDefinitionAndroid, + projectName: String, + a: MutableMap> + ) { + val packageName = "com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)}" + val moduleName = "Module_${moduleDefinition.moduleNumber}" + // Create imports for this module's classes + val classImports = StringBuilder() + val provideMethods = mutableListOf() + + moduleDefinition.classes.forEach { classDefinition -> + if (classDefinition.type != ClassTypeAndroid.STATE) { // Skip STATE type classes + val className = + "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" + classImports.appendLine("import $packageName.$className") + + when (classDefinition.type) { + ClassTypeAndroid.API -> { + provideMethods.add( + """ + | @Provides + | @Singleton + | fun provide$className(): $className { + | return $className() + | } + """.trimMargin() + ) + } + + ClassTypeAndroid.REPOSITORY -> { + // For repositories, we need to provide their API dependencies + if (classDefinition.dependencies.isNotEmpty()) { + val xa = mutableListOf() + classDefinition.dependencies.mapIndexed { index, dep -> + val depModuleId = dep.sourceModuleId + val s = a.filter { it.key == depModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.API } + if (x != null) { + + val apiClassName = x.className + classImports.appendLine( + "import com.awesomeapp.${ + NameMappings.modulePackageName( + dep.sourceModuleId + ) + }.$apiClassName" + ) + xa.add("api$index: $apiClassName = $apiClassName()") + + } + } + } + val constructorParams = xa.joinToString(",\n ") + + provideMethods.add( + """ + | @Provides + | @Singleton + | fun provide$className( + | $constructorParams + | ): $className { + | return $className(${ + constructorParams.split(",").map { it.split(":").first() }.joinToString(", ") + }) + | } + """.trimMargin() + ) + } else { + provideMethods.add( + """ + | @Provides + | @Singleton + | fun provide$className(): $className { + | return $className() + | } + """.trimMargin() + ) + } + } + + else -> { + } + + } + } + } + + // Only create the module if we have something to provide + if (provideMethods.isNotEmpty()) { + val content = """ + |package $packageName.di + | + |import dagger.Module + |import dagger.Provides + |import dagger.hilt.InstallIn + |import dagger.hilt.components.SingletonComponent + |import javax.inject.Singleton + |${classImports.toString().trim()} + | + |@Module + |@InstallIn(SingletonComponent::class) + |object $moduleName { + |${provideMethods.joinToString("\n\n")} + |} + """.trimMargin() + + val layerDir = NameMappings.layerName(moduleDefinition.layer) + val moduleDir = NameMappings.moduleName(moduleDefinition.moduleId) + val diPackagePath = + "${projectName}/$layerDir/$moduleDir/src/main/kotlin/${ + packageName.replace( + ".", + "/" + ) + }/di" + File(diPackagePath).mkdirs() + File("$diPackagePath/$moduleName.kt").writeText(content) + } + } + + private fun generateClassContent( + classDefinition: ClassDefinitionAndroid, + moduleDefinition: ModuleClassDefinitionAndroid, + a: MutableMap> + ): String { + val packageName = "com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)}" + val className = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" + + return when (classDefinition.type) { + ClassTypeAndroid.VIEWMODEL -> generateViewModel( + packageName, + className, + classDefinition.dependencies, + a + ) + + ClassTypeAndroid.REPOSITORY -> generateRepository(packageName, className, classDefinition.dependencies, a) + ClassTypeAndroid.API -> generateApi(packageName, className) + ClassTypeAndroid.WORKER -> generateWorker(packageName, className) + ClassTypeAndroid.ACTIVITY -> generateComposeActivity(packageName, className, moduleDefinition.moduleNumber) + ClassTypeAndroid.FRAGMENT -> generateFragment(packageName, className) + ClassTypeAndroid.SERVICE -> generateService(packageName, className) + ClassTypeAndroid.STATE -> generateState(packageName, className) + ClassTypeAndroid.MODEL -> generateModel(packageName, className) + ClassTypeAndroid.USECASE -> generateUseCase( + packageName, + className, + moduleDefinition.moduleNumber.toString() + ) + + else -> throw IllegalArgumentException("Unsupported class type: ${classDefinition.type}") + } + } + + private fun generateViewModel( + packageName: String, + className: String, + dependencies: List, + a: MutableMap> + ): String { + + val imports = buildString { + appendLine("import androidx.lifecycle.ViewModel") + appendLine("import androidx.lifecycle.viewModelScope") + appendLine("import kotlinx.coroutines.launch") + appendLine("import kotlinx.coroutines.coroutineScope") + appendLine("import kotlinx.coroutines.async") + appendLine("import kotlinx.coroutines.awaitAll") + appendLine("import kotlinx.coroutines.flow.MutableStateFlow") + appendLine("import kotlinx.coroutines.flow.StateFlow") + appendLine("import kotlinx.coroutines.flow.asStateFlow") + appendLine("import kotlinx.coroutines.Dispatchers") + when (di) { + DependencyInjection.HILT -> { + appendLine("import dagger.hilt.android.lifecycle.HiltViewModel") + appendLine("import javax.inject.Inject") + } + DependencyInjection.METRO -> { + appendLine("import dev.zacsweers.metro.Inject") + } + DependencyInjection.NONE -> {} + } + dependencies.forEach { dep -> + val depModuleId = dep.sourceModuleId + val s = a.filter { it.key == depModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().firstOrNull { it.type == ClassTypeAndroid.REPOSITORY } + if (x != null) { + val repoClassName = x.className + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(dep.sourceModuleId)}.$repoClassName") + } + } + } + } + + val constructorParams = dependencies.mapIndexed { index, dep -> + val s = a.filter { it.key == dep.sourceModuleId }.values.flatten() + .first { it.type == ClassTypeAndroid.REPOSITORY } + val repoClassName = s.className + "private val repository$index: $repoClassName" + }.joinToString(",\n ") + + // Step 1: Generate the data fetching logic as a clean, un-indented block using trimIndent(). + val dataAssignmentBlock = if (dependencies.isEmpty()) { + """ + val data = "Data from $className" + """.trimIndent() + } else { + val lambdaList = dependencies.indices.joinToString(",\n ") { + "{ repository$it.getData() }" + } + """ + val data = coroutineScope { + val fetchers = listOf String>( + $lambdaList + ) + val results = fetchers.map { fetcher -> + async { fetcher() } + }.awaitAll() + results.joinToString("") + } + """.trimIndent() + } + + val diAnnotation = when (di) { + DependencyInjection.HILT -> "@HiltViewModel" + DependencyInjection.METRO, DependencyInjection.NONE -> "" + } + val injectAnnotation = when (di) { + DependencyInjection.HILT, DependencyInjection.METRO -> "@Inject " + DependencyInjection.NONE -> "" + } + + return """ + |package $packageName + | + |$imports + | + |$diAnnotation + |class $className ${injectAnnotation}constructor( + | $constructorParams + |) : ViewModel() { + | private val _state = MutableStateFlow("") + | val state: StateFlow = _state.asStateFlow() + | + | init { + | viewModelScope.launch(Dispatchers.IO) { + | try { + |${dataAssignmentBlock.prependIndent(" ")} + | _state.emit(data) + | } catch (e: Exception) { + | _state.emit("Error: ${'$'}{e.message}") + | } + | } + | } + |} + """.trimMargin() + } + + private fun generateRepository( + packageName: String, + className: String, + dependencies: List, + a: MutableMap> + ): String { + val imports = buildString { + appendLine("import kotlinx.coroutines.Dispatchers") + appendLine("import kotlinx.coroutines.withContext") + appendLine("import kotlinx.coroutines.coroutineScope") + appendLine("import kotlinx.coroutines.async") + appendLine("import kotlinx.coroutines.awaitAll") + when (di) { + DependencyInjection.HILT -> { + appendLine("import javax.inject.Inject") + appendLine("import javax.inject.Singleton") + } + DependencyInjection.METRO -> { + appendLine("import dev.zacsweers.metro.Inject") + } + DependencyInjection.NONE -> {} + } + dependencies.forEach { dep -> + val depModuleId = dep.sourceModuleId + val matches = a.filter { it.key == depModuleId } + if (matches.isNotEmpty()) { + val apiClass = matches.values.flatten().firstOrNull { it.type == ClassTypeAndroid.API } + if (apiClass != null) { + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(depModuleId)}.${apiClass.className}") + } + } + } + } + + val constructorParams = dependencies.mapIndexedNotNull { index, dep -> + val depModuleId = dep.sourceModuleId + val apiClass = a[depModuleId]?.firstOrNull { it.type == ClassTypeAndroid.API } + apiClass?.let { "private val api$index: ${it.className}" } + }.joinToString(",\n ") + + val dataFetchLogic = if (dependencies.isEmpty()) { + "\"Data from $className Repository\"" + } else { + val lambdaList = dependencies.indices.joinToString(",\n ") { "{ api$it.fetchData() }" } + """ + coroutineScope { + val apis = listOf String>( + $lambdaList + ) + val results = apis.map { fetcher -> + async { fetcher() } + }.awaitAll() + results.joinToString("") + } + """.trimIndent() + } + + val singletonAnnotation = if (di == DependencyInjection.HILT) "@Singleton" else "" + val injectAnnotation = when (di) { + DependencyInjection.HILT, DependencyInjection.METRO -> "@Inject " + DependencyInjection.NONE -> "" + } + + return """ + |package $packageName + | + |$imports + | + |$singletonAnnotation + |class $className ${injectAnnotation}constructor( + | $constructorParams + |) { + | suspend fun getData(): String = withContext(Dispatchers.IO) { + |${dataFetchLogic.prependIndent(" ")} + | } + |} + """.trimMargin() + } + + private fun generateFragment(packageName: String, className: String): String { + val moduleId = packageName.split(".").last() + val moduleNumber = moduleId.split("_").last() + + val imports = buildString { + appendLine("import android.os.Bundle") + appendLine("import android.view.LayoutInflater") + appendLine("import android.view.View") + appendLine("import android.view.ViewGroup") + appendLine("import androidx.compose.foundation.layout.Box") + appendLine("import androidx.compose.foundation.layout.fillMaxSize") + appendLine("import androidx.compose.material3.Text") + appendLine("import androidx.compose.runtime.Composable") + appendLine("import androidx.compose.runtime.collectAsState") + appendLine("import androidx.compose.runtime.getValue") + appendLine("import androidx.compose.ui.Alignment") + appendLine("import androidx.compose.ui.Modifier") + appendLine("import androidx.compose.ui.platform.ComposeView") + appendLine("import androidx.fragment.app.Fragment") + appendLine("import androidx.fragment.app.viewModels") + if (di == DependencyInjection.HILT) { + appendLine("import dagger.hilt.android.AndroidEntryPoint") + } + } + + val entryPointAnnotation = if (di == DependencyInjection.HILT) "@AndroidEntryPoint" else "" + val viewModelClass = "Feature${moduleNumber}_1" + + return """ + |package $packageName + | + |$imports + | + |$entryPointAnnotation + |class $className : Fragment() { + | + | override fun onCreateView( + | inflater: LayoutInflater, + | container: ViewGroup?, + | savedInstanceState: Bundle? + | ): View { + | return ComposeView(requireContext()).apply { + | setContent { + | + | } + | } + | } + |} + """.trimMargin() + } + + private fun generateApi(packageName: String, className: String): String { + val injectImport = when (di) { + DependencyInjection.HILT -> "import javax.inject.Inject" + DependencyInjection.METRO -> "import dev.zacsweers.metro.Inject" + DependencyInjection.NONE -> "" + } + val injectAnnotation = when (di) { + DependencyInjection.HILT, DependencyInjection.METRO -> "@Inject " + DependencyInjection.NONE -> "" + } + + return """ + |package $packageName + | + |import kotlinx.coroutines.Dispatchers + |import kotlinx.coroutines.withContext + |$injectImport + | + |class $className ${injectAnnotation}constructor() { + | suspend fun fetchData(): String = withContext(Dispatchers.IO) { + | "Data from $className API" + | } + |} + """.trimMargin() + } + + private fun generateWorker(packageName: String, className: String): String { + if (di != DependencyInjection.HILT) { + return """ + |package $packageName + | + |import android.content.Context + |import androidx.work.CoroutineWorker + |import androidx.work.WorkerParameters + | + |class $className( + | context: Context, + | params: WorkerParameters + |) : CoroutineWorker(context, params) { + | override suspend fun doWork(): Result { + | return try { + | Thread.sleep(100) + | Result.success() + | } catch (e: Exception) { + | Result.failure() + | } + | } + |} + """.trimMargin() + } + return """ + |package $packageName + | + |import android.content.Context + |import androidx.hilt.work.HiltWorker + |import androidx.work.CoroutineWorker + |import androidx.work.WorkerParameters + |import dagger.assisted.Assisted + |import dagger.assisted.AssistedInject + |import javax.inject.Inject + | + |@HiltWorker + |class $className @AssistedInject constructor( + | @Assisted private val context: Context, + | @Assisted private val params: WorkerParameters + |) : CoroutineWorker(context, params) { + | override suspend fun doWork(): Result { + | return try { + | Thread.sleep(100) + | Result.success() + | } catch (e: Exception) { + | Result.failure() + | } + | } + |} + """.trimMargin() + } + + private fun generateComposeActivity(packageName: String, className: String, moduleNumber: Int): String { + val moduleId = packageName.split(".").last() + val viewModelClass = "Viewmodel${moduleNumber}_1" + + val imports = buildString { + appendLine("import android.os.Bundle") + appendLine("import androidx.activity.ComponentActivity") + appendLine("import androidx.activity.compose.setContent") + appendLine("import androidx.activity.viewModels") + appendLine("import androidx.compose.foundation.layout.Box") + appendLine("import androidx.compose.foundation.layout.fillMaxSize") + appendLine("import androidx.compose.material3.Text") + appendLine("import androidx.compose.runtime.Composable") + appendLine("import androidx.compose.runtime.collectAsState") + appendLine("import androidx.compose.runtime.getValue") + appendLine("import androidx.compose.ui.Alignment") + appendLine("import androidx.compose.ui.Modifier") + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(moduleId)}.ui.theme.FeatureTheme") + if (di == DependencyInjection.HILT) { + appendLine("import dagger.hilt.android.AndroidEntryPoint") + } + } + + val entryPointAnnotation = if (di == DependencyInjection.HILT) "@AndroidEntryPoint" else "" + + return """ + |package $packageName + | + |$imports + | + |$entryPointAnnotation + |class $className : ComponentActivity() { + | private val viewModel: $viewModelClass by viewModels() + | + | override fun onCreate(savedInstanceState: Bundle?) { + | super.onCreate(savedInstanceState) + | setContent { + | FeatureTheme { + | FeatureScreen_${className}(viewModel) + | } + | } + | } + |} + | + |@Composable + |fun FeatureScreen_${className}(viewModel: $viewModelClass) { + | val state by viewModel.state.collectAsState() + | + | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + | Text(text = state) + | } + |} + """.trimMargin() + } + + private fun generateService(packageName: String, className: String): String { + val imports = buildString { + appendLine("import android.app.Service") + appendLine("import android.content.Intent") + appendLine("import android.os.IBinder") + if (di == DependencyInjection.HILT) { + appendLine("import dagger.hilt.android.AndroidEntryPoint") + } + appendLine("import kotlinx.coroutines.*") + } + + val entryPointAnnotation = if (di == DependencyInjection.HILT) "@AndroidEntryPoint" else "" + + return """ + |package $packageName + | + |$imports + | + |$entryPointAnnotation + |class $className : Service() { + | private val serviceJob = Job() + | private val serviceScope = CoroutineScope(Dispatchers.Default + serviceJob) + | + | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + | serviceScope.launch { + | delay(100) + | } + | return Service.START_STICKY + | } + | + | override fun onBind(intent: Intent?): IBinder? = null + | + | override fun onDestroy() { + | super.onDestroy() + | serviceJob.cancel() + | } + |} + """.trimMargin() + } + + private fun generateState(packageName: String, className: String): String { + return """ + |package $packageName + | + |sealed class $className { + | data object Loading : $className() + | data class Success(val data: String) : $className() + | data class Error(val message: String) : $className() + | + | companion object { + | fun loading() = Loading + | fun success(data: String) = Success(data) + | fun error(message: String) = Error(message) + | } + |} + """.trimMargin() + } + + private fun generateModel(packageName: String, className: String): String { + return """ + |package $packageName + | + |data class $className( + | val id: String = "$className-${System.currentTimeMillis()}", + | val name: String = "Model for $className", + | val description: String = "Description for $className" + |) + """.trimMargin() + } + + private fun generateUseCase(packageName: String, className: String, moduleNumber: String): String { + val injectImport = when (di) { + DependencyInjection.HILT -> "import javax.inject.Inject" + DependencyInjection.METRO -> "import dev.zacsweers.metro.Inject" + DependencyInjection.NONE -> "" + } + val injectAnnotation = when (di) { + DependencyInjection.HILT, DependencyInjection.METRO -> "@Inject " + DependencyInjection.NONE -> "" + } + return """ + |package $packageName + | + |import kotlinx.coroutines.flow.Flow + |import kotlinx.coroutines.flow.flow + |$injectImport + | + |class $className ${injectAnnotation}constructor() { + | operator fun invoke(): Flow = flow { + | emit("Data from $className UseCase") + | } + |} + """.trimMargin() + } + + private fun writeClassFile( + content: String, + classDefinition: ClassDefinitionAndroid, + moduleDefinition: ModuleClassDefinitionAndroid, + projectName: String + ) { + val layerDir = NameMappings.layerName(moduleDefinition.layer) + val moduleDir = NameMappings.moduleName(moduleDefinition.moduleId) + val packageDir = NameMappings.modulePackageName(moduleDefinition.moduleId) + val directory = + File("$projectName/$layerDir/$moduleDir/src/main/kotlin/com/awesomeapp/$packageDir/") + directory.mkdirs() + + val fileName = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}.kt" + File(directory, fileName).writeText(content) + } + + private fun createApplicationClass(moduleDefinition: ModuleClassDefinitionAndroid, projectName: String) { + val packageName = "com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)}" + val content = """ + |package $packageName + | + |import android.app.Application + |import androidx.hilt.work.HiltWorkerFactory + |import androidx.work.Configuration + |import dagger.hilt.android.HiltAndroidApp + |import javax.inject.Inject + | + |@HiltAndroidApp + |class MainApplication : Application() { + | @Inject + | lateinit var workerFactory: HiltWorkerFactory + | + | override val workManagerConfiguration: Configuration + | get() = Configuration.Builder() + | .setWorkerFactory(workerFactory) + | .build() + |} + """.trimMargin() + + val layerDirApp = NameMappings.layerName(moduleDefinition.layer) + val moduleDirApp = NameMappings.moduleName(moduleDefinition.moduleId) + val directory = File( + "$projectName/$layerDirApp/$moduleDirApp/src/main/kotlin/${ + packageName.replace( + ".", + "/" + ) + }/" + ) + directory.mkdirs() + File(directory, "MainApplication.kt").writeText(content) + } + + +} diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroid.kt index 217f8235..a7b20a9d 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroid.kt @@ -7,153 +7,59 @@ import io.github.cdsap.projectgenerator.writer.ModuleClassPlanner /** * Plans what classes each module should have and their dependencies */ -class ModuleClassPlannerAndroid( - private val maxLimitToCreateApiAndRepositories: Int = 1000 -) : ModuleClassPlanner { +class ModuleClassPlannerAndroid : ModuleClassPlanner { override fun planModuleClasses(projectGraph: ProjectGraph): ModuleClassDefinitionAndroid { val moduleId = NameMappings.moduleName(projectGraph.id) val layer = projectGraph.layer val moduleNumber = projectGraph.id.split("_").last().toInt() + val maxClasses = projectGraph.classes.coerceAtLeast(0) val classes = mutableListOf() var currentIndex = 1 - // First, add all core classes in the correct order - val coreClasses = mutableListOf() - - // Every module gets a ViewModel - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.VIEWMODEL, - index = currentIndex++, - dependencies = findDependenciesForClass(ClassTypeAndroid.VIEWMODEL, projectGraph).toMutableList() - ) - ) - - // If we have more than 1 class, add Activity - if (projectGraph.classes > 1) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.ACTIVITY, - index = currentIndex++, - dependencies = mutableListOf() // Activity doesn't need Fragment dependency yet - ) - ) - } - - // If we have more than 2 classes, add Compose Activity - if (projectGraph.classes > 2) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.ACTIVITY, - index = currentIndex++, - dependencies = mutableListOf() // Compose Activity doesn't need Fragment dependency yet - ) - ) - } - - // If we have more than 3 classes, add Fragment - if (projectGraph.classes > 3) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.FRAGMENT, - index = currentIndex++, - dependencies = findDependenciesForClass(ClassTypeAndroid.FRAGMENT, projectGraph).toMutableList() - ) - ) + val localDep = { type: ClassTypeAndroid -> + mutableListOf(ClassDependencyAndroid(type, moduleId)) } - - // If we have more than 4 classes and it's a multiple of 4, add Repository and API - // But limit based on concurrentLimit to avoid long Dagger compilation times - if (projectGraph.classes > 4 && moduleNumber % 4 == 0) { - if( moduleNumber <= maxLimitToCreateApiAndRepositories) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.REPOSITORY, - index = currentIndex++, - dependencies = findDependenciesForClass( - ClassTypeAndroid.REPOSITORY, - projectGraph - ).toMutableList() - ) - ) - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.API, - index = currentIndex++ - ) - ) - } else { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.MODEL, - index = currentIndex++ - ) - ) + val addClass = { type: ClassTypeAndroid, deps: MutableList -> + if (classes.size < maxClasses) { + classes.add(ClassDefinitionAndroid(type = type, index = currentIndex++, dependencies = deps)) } } - // If we have more than 6 classes and it's a multiple of 5, add Service and Worker - if (projectGraph.classes > 6 && moduleNumber % 5 == 0) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.SERVICE, - index = currentIndex++, - dependencies = findDependenciesForClass(ClassTypeAndroid.SERVICE, projectGraph).toMutableList() - ) - ) - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.WORKER, - index = currentIndex++ - ) - ) - } - - // If we have more than 8 classes and it's a multiple of 3, add UseCase - if (projectGraph.classes > 8 && moduleNumber % 3 == 0) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.USECASE, - index = currentIndex++, - dependencies = findDependenciesForClass(ClassTypeAndroid.USECASE, projectGraph).toMutableList() - ) - ) - } + val baseClasses = listOf( + ClassTypeAndroid.ENTITY, + ClassTypeAndroid.MODEL, + ClassTypeAndroid.DATABASE, + ClassTypeAndroid.DAO, + ClassTypeAndroid.REPOSITORY, + ClassTypeAndroid.USECASE, + ClassTypeAndroid.STATE, + ClassTypeAndroid.VIEWMODEL, + ClassTypeAndroid.SCREEN + ) - // If we have more than 9 classes, add State - if (projectGraph.classes > 9) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.STATE, - index = currentIndex++ - ) - ) + baseClasses.forEach { type -> + val deps = when (type) { + ClassTypeAndroid.REPOSITORY -> localDep(ClassTypeAndroid.DAO) + ClassTypeAndroid.USECASE -> localDep(ClassTypeAndroid.REPOSITORY) + ClassTypeAndroid.VIEWMODEL -> localDep(ClassTypeAndroid.USECASE) + ClassTypeAndroid.SCREEN -> localDep(ClassTypeAndroid.VIEWMODEL) + else -> mutableListOf() + } + addClass(type, deps) } - // If we have more than 10 classes, add Model - if (projectGraph.classes > 10) { - coreClasses.add( - ClassDefinitionAndroid( - type = ClassTypeAndroid.MODEL, - index = currentIndex++ - ) - ) + if (projectGraph.type == TypeProject.ANDROID_APP) { + addClass(ClassTypeAndroid.ACTIVITY, mutableListOf()) + addClass(ClassTypeAndroid.FRAGMENT, mutableListOf()) } - // Add core classes to the final list - classes.addAll(coreClasses) - - // Add additional classes to reach the requested number - val remainingClasses = projectGraph.classes - classes.size + val remainingClasses = maxClasses - classes.size if (remainingClasses > 0) { - // Create additional classes with simpler types that don't require dependencies for (i in 1..remainingClasses) { - val classType = when (i % 3) { - 0 -> ClassTypeAndroid.STATE - 1 -> ClassTypeAndroid.MODEL - else -> ClassTypeAndroid.ACTIVITY + val classType = when (i % 2) { + 0 -> ClassTypeAndroid.MODEL + else -> ClassTypeAndroid.STATE } - classes.add( ClassDefinitionAndroid( type = classType, @@ -164,16 +70,6 @@ class ModuleClassPlannerAndroid( } } - // Now update Activity dependencies to reference the correct Fragment - classes.forEach { classDef -> - if (classDef.type == ClassTypeAndroid.ACTIVITY) { - val fragment = classes.find { it.type == ClassTypeAndroid.FRAGMENT } - if (fragment != null) { - classDef.dependencies.add(ClassDependencyAndroid(ClassTypeAndroid.FRAGMENT, moduleId)) - } - } - } - // Extract the module IDs from node dependencies for proper Hilt module dependencies val dependencies = projectGraph.nodes.map { NameMappings.moduleName(it.id) } @@ -193,45 +89,6 @@ class ModuleClassPlannerAndroid( if (!classType.requiresDependency()) return emptyList() val dependencyType = classType.dependencyType() ?: return emptyList() - val availableModules = getAvailableModules(currentModule) - - return availableModules - .filter { module -> hasClassType(module, dependencyType) } - .map { module -> ClassDependencyAndroid(dependencyType, NameMappings.moduleName(module.id)) } - } - - private fun getAvailableModules(currentModule: ProjectGraph): List { - val currentModuleNumber = currentModule.id.split("_").last().toInt() - val currentLayer = currentModule.layer - - return currentModule.nodes.filter { node -> - val otherModuleNumber = node.id.split("_").last().toInt() - val otherLayer = node.layer - - // Only reference modules that: - // 1. Are in the same layer but have a lower module number - // 2. Are in a lower layer - // 3. Have the required class types based on their module number - ((node.layer == currentLayer && otherModuleNumber < currentModuleNumber) || - (otherLayer < currentLayer)) && - hasClassType( - node, - ClassTypeAndroid.REPOSITORY - ) // Only include modules that have the required class type - } - } - - private fun hasClassType(module: ProjectGraph, classType: ClassTypeAndroid): Boolean { - val moduleNumber = module.id.split("_").last().toInt() - - return when (classType) { - ClassTypeAndroid.REPOSITORY -> moduleNumber % 4 == 0 && moduleNumber > 0 && moduleNumber <= maxLimitToCreateApiAndRepositories - ClassTypeAndroid.API -> moduleNumber % 4 == 0 && moduleNumber > 0 && moduleNumber <= maxLimitToCreateApiAndRepositories - ClassTypeAndroid.WORKER -> moduleNumber % 5 == 0 && moduleNumber > 0 - ClassTypeAndroid.VIEWMODEL -> true // All modules have ViewModels - ClassTypeAndroid.SERVICE -> moduleNumber % 5 == 0 && moduleNumber > 0 - ClassTypeAndroid.USECASE -> moduleNumber % 3 == 0 && moduleNumber > 0 - else -> false - } + return listOf(ClassDependencyAndroid(dependencyType, NameMappings.moduleName(currentModule.id))) } } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroidLegacy.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroidLegacy.kt new file mode 100644 index 00000000..e7300951 --- /dev/null +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/planner/ModuleClassPlannerAndroidLegacy.kt @@ -0,0 +1,237 @@ +package io.github.cdsap.projectgenerator.generator.planner + +import io.github.cdsap.projectgenerator.model.* +import io.github.cdsap.projectgenerator.NameMappings +import io.github.cdsap.projectgenerator.writer.ModuleClassPlanner + +/** + * Plans what classes each module should have and their dependencies + */ +class ModuleClassPlannerAndroidLegacy( + private val maxLimitToCreateApiAndRepositories: Int = 1000 +) : ModuleClassPlanner { + override fun planModuleClasses(projectGraph: ProjectGraph): ModuleClassDefinitionAndroid { + val moduleId = NameMappings.moduleName(projectGraph.id) + val layer = projectGraph.layer + val moduleNumber = projectGraph.id.split("_").last().toInt() + val classes = mutableListOf() + var currentIndex = 1 + + // First, add all core classes in the correct order + val coreClasses = mutableListOf() + + // Every module gets a ViewModel + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.VIEWMODEL, + index = currentIndex++, + dependencies = findDependenciesForClass(ClassTypeAndroid.VIEWMODEL, projectGraph).toMutableList() + ) + ) + + // If we have more than 1 class, add Activity + if (projectGraph.classes > 1) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.ACTIVITY, + index = currentIndex++, + dependencies = mutableListOf() // Activity doesn't need Fragment dependency yet + ) + ) + } + + // If we have more than 2 classes, add Compose Activity + if (projectGraph.classes > 2) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.ACTIVITY, + index = currentIndex++, + dependencies = mutableListOf() // Compose Activity doesn't need Fragment dependency yet + ) + ) + } + + // If we have more than 3 classes, add Fragment + if (projectGraph.classes > 3) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.FRAGMENT, + index = currentIndex++, + dependencies = findDependenciesForClass(ClassTypeAndroid.FRAGMENT, projectGraph).toMutableList() + ) + ) + } + + // If we have more than 4 classes and it's a multiple of 4, add Repository and API + // But limit based on concurrentLimit to avoid long Dagger compilation times + if (projectGraph.classes > 4 && moduleNumber % 4 == 0) { + if( moduleNumber <= maxLimitToCreateApiAndRepositories) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.REPOSITORY, + index = currentIndex++, + dependencies = findDependenciesForClass( + ClassTypeAndroid.REPOSITORY, + projectGraph + ).toMutableList() + ) + ) + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.API, + index = currentIndex++ + ) + ) + } else { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.MODEL, + index = currentIndex++ + ) + ) + } + } + + // If we have more than 6 classes and it's a multiple of 5, add Service and Worker + if (projectGraph.classes > 6 && moduleNumber % 5 == 0) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.SERVICE, + index = currentIndex++, + dependencies = findDependenciesForClass(ClassTypeAndroid.SERVICE, projectGraph).toMutableList() + ) + ) + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.WORKER, + index = currentIndex++ + ) + ) + } + + // If we have more than 8 classes and it's a multiple of 3, add UseCase + if (projectGraph.classes > 8 && moduleNumber % 3 == 0) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.USECASE, + index = currentIndex++, + dependencies = findDependenciesForClass(ClassTypeAndroid.USECASE, projectGraph).toMutableList() + ) + ) + } + + // If we have more than 9 classes, add State + if (projectGraph.classes > 9) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.STATE, + index = currentIndex++ + ) + ) + } + + // If we have more than 10 classes, add Model + if (projectGraph.classes > 10) { + coreClasses.add( + ClassDefinitionAndroid( + type = ClassTypeAndroid.MODEL, + index = currentIndex++ + ) + ) + } + + // Add core classes to the final list + classes.addAll(coreClasses) + + // Add additional classes to reach the requested number + val remainingClasses = projectGraph.classes - classes.size + if (remainingClasses > 0) { + // Create additional classes with simpler types that don't require dependencies + for (i in 1..remainingClasses) { + val classType = when (i % 3) { + 0 -> ClassTypeAndroid.STATE + 1 -> ClassTypeAndroid.MODEL + else -> ClassTypeAndroid.ACTIVITY + } + + classes.add( + ClassDefinitionAndroid( + type = classType, + index = currentIndex++, + dependencies = mutableListOf() + ) + ) + } + } + + // Now update Activity dependencies to reference the correct Fragment + classes.forEach { classDef -> + if (classDef.type == ClassTypeAndroid.ACTIVITY) { + val fragment = classes.find { it.type == ClassTypeAndroid.FRAGMENT } + if (fragment != null) { + classDef.dependencies.add(ClassDependencyAndroid(ClassTypeAndroid.FRAGMENT, moduleId)) + } + } + } + + // Extract the module IDs from node dependencies for proper Hilt module dependencies + val dependencies = projectGraph.nodes.map { NameMappings.moduleName(it.id) } + + return ModuleClassDefinitionAndroid( + moduleId = moduleId, + layer = layer, + moduleNumber = moduleNumber, + classes = classes, + dependencies = dependencies + ) + } + + private fun findDependenciesForClass( + classType: ClassTypeAndroid, + currentModule: ProjectGraph + ): List { + if (!classType.requiresDependency()) return emptyList() + + val dependencyType = classType.dependencyType() ?: return emptyList() + val availableModules = getAvailableModules(currentModule) + + return availableModules + .filter { module -> hasClassType(module, dependencyType) } + .map { module -> ClassDependencyAndroid(dependencyType, NameMappings.moduleName(module.id)) } + } + + private fun getAvailableModules(currentModule: ProjectGraph): List { + val currentModuleNumber = currentModule.id.split("_").last().toInt() + val currentLayer = currentModule.layer + + return currentModule.nodes.filter { node -> + val otherModuleNumber = node.id.split("_").last().toInt() + val otherLayer = node.layer + + // Only reference modules that: + // 1. Are in the same layer but have a lower module number + // 2. Are in a lower layer + // 3. Have the required class types based on their module number + ((node.layer == currentLayer && otherModuleNumber < currentModuleNumber) || + (otherLayer < currentLayer)) && + hasClassType( + node, + ClassTypeAndroid.REPOSITORY + ) // Only include modules that have the required class type + } + } + + private fun hasClassType(module: ProjectGraph, classType: ClassTypeAndroid): Boolean { + val moduleNumber = module.id.split("_").last().toInt() + + return when (classType) { + ClassTypeAndroid.REPOSITORY -> moduleNumber % 4 == 0 && moduleNumber > 0 && moduleNumber <= maxLimitToCreateApiAndRepositories + ClassTypeAndroid.API -> moduleNumber % 4 == 0 && moduleNumber > 0 && moduleNumber <= maxLimitToCreateApiAndRepositories + ClassTypeAndroid.WORKER -> moduleNumber % 5 == 0 && moduleNumber > 0 + ClassTypeAndroid.VIEWMODEL -> true // All modules have ViewModels + ClassTypeAndroid.SERVICE -> moduleNumber % 5 == 0 && moduleNumber > 0 + ClassTypeAndroid.USECASE -> moduleNumber % 3 == 0 && moduleNumber > 0 + else -> false + } + } +} diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidApp.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidApp.kt index 145a7cf9..3b686664 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidApp.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidApp.kt @@ -32,10 +32,10 @@ class CompositeBuildPluginAndroidApp { | } | | extensions.configure { - | namespace = "com.awesome." + target.name.replace(":","_").replace("-", "") + | namespace = "com.awesomeapp." + target.name.replace(":","_").replace("-", "") | compileSdk = 36 | defaultConfig { - | applicationId = "com.awesome." + target.name.replace(":","_").replace("-", "") + | applicationId = "com.awesomeapp." + target.name.replace(":","_").replace("-", "") | minSdk = 24 | targetSdk = 36 | versionCode = 1 @@ -68,12 +68,13 @@ class CompositeBuildPluginAndroidApp { |} |""".trimMargin() - fun provideKotlinProcessor(versions: Versions, di: DependencyInjection) = if (versions.kotlin.kotlinProcessor.processor == Processor.KAPT) - """apply("kotlin-kapt")""" - else if( di == DependencyInjection.HILT) - """apply("com.google.devtools.ksp")""" - else - "" + fun provideKotlinProcessor(versions: Versions, di: DependencyInjection): String { + if (versions.kotlin.kotlinProcessor.processor == Processor.KAPT) { + return """apply("kotlin-kapt")""" + } + val shouldApplyKsp = di == DependencyInjection.HILT || versions.android.roomDatabase + return if (shouldApplyKsp) """apply("com.google.devtools.ksp")""" else "" + } fun provideKgpBasedOnAgp(versions: Versions) = if (!versions.android.agp.isAgp9()) """apply("org.jetbrains.kotlin.android")""" diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt index 7ff8158c..b04c60d9 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/plugins/android/CompositeBuildPluginAndroidLib.kt @@ -29,7 +29,7 @@ class CompositeBuildPluginAndroidLib { | } | | extensions.configure { - | namespace = "com.awesome." + target.name.replace(":","_").replace("-", "") + | namespace = "com.awesomeapp." + target.name.replace(":","_").replace("-", "") | compileSdk = 36 | defaultConfig { | minSdk = 24 @@ -65,12 +65,13 @@ class CompositeBuildPluginAndroidLib { |} |""".trimMargin() - fun provideKotlinProcessor(versions: Versions, di: DependencyInjection) = if (versions.kotlin.kotlinProcessor.processor == Processor.KAPT) - """apply("kotlin-kapt")""" - else if( di == DependencyInjection.HILT) - """apply("com.google.devtools.ksp")""" - else - "" + fun provideKotlinProcessor(versions: Versions, di: DependencyInjection): String { + if (versions.kotlin.kotlinProcessor.processor == Processor.KAPT) { + return """apply("kotlin-kapt")""" + } + val shouldApplyKsp = di == DependencyInjection.HILT || versions.android.roomDatabase + return if (shouldApplyKsp) """apply("com.google.devtools.ksp")""" else "" + } fun provideKgpBasedOnAgp(versions: Versions) = if (!versions.android.agp.isAgp9()) """apply("org.jetbrains.kotlin.android")""" diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt index 6b3a8ac7..4064540e 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt @@ -18,7 +18,8 @@ import java.io.File import java.util.concurrent.CopyOnWriteArrayList class ResourceGenerator( - private val di: DependencyInjection + private val di: DependencyInjection, + private val roomDatabase: Boolean = false ) : ResourceGeneratorA { override fun generate( @@ -49,7 +50,7 @@ class ResourceGenerator( ) { val (layoutDir, valuesDir, manifestDir) = createResources(lang, node) Manifest().createManifest(manifestDir, node.layer, NameMappings.moduleName(node.id), TypeProject.ANDROID_APP, dictionary) - AndroidApplication().createApplicationClass(node, lang, di, dictionary) + AndroidApplication().createApplicationClass(node, lang, di, dictionary, roomDatabase) val moduleDir = NameMappings.moduleName(node.id) createLayoutFiles(layoutDir, moduleDir) createValueFiles(valuesDir, moduleDir, typeOfStringResources) diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt index 40e6ade9..3781f108 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt @@ -43,6 +43,9 @@ class TestGeneratorAndroid : TestGenerator generateRepositoryTest(moduleDefinition, classDefinition, classesDictionary) ClassTypeAndroid.API -> generateApiTest(className) + ClassTypeAndroid.ENTITY -> generateEntityTest(className) + ClassTypeAndroid.DAO -> generateDaoTest(className) + ClassTypeAndroid.DATABASE -> generateDatabaseTest(className) // todo generate correct tests for viewModel // ClassTypeAndroid.VIEWMODEL -> generateViewModelTest( // className, @@ -55,8 +58,9 @@ class TestGeneratorAndroid : TestGenerator generateFragmentTest(className) ClassTypeAndroid.SERVICE -> generateServiceTest(className) ClassTypeAndroid.STATE -> generateStateTest(className) + ClassTypeAndroid.SCREEN -> generateScreenTest(className) ClassTypeAndroid.MODEL -> generateModelTest(className) - ClassTypeAndroid.USECASE -> generateUseCaseTest(moduleDefinition, className) + ClassTypeAndroid.USECASE -> generateUseCaseTest(moduleDefinition, className, classesDictionary) else -> "" } @@ -77,6 +81,16 @@ class TestGeneratorAndroid : TestGenerator { + appendLine("import androidx.room.Room") + appendLine("import androidx.test.core.app.ApplicationProvider") + appendLine("import android.content.Context") + appendLine("import kotlinx.coroutines.flow.first") + appendLine("import org.junit.After") + appendLine("import org.robolectric.RobolectricTestRunner") + appendLine("import org.robolectric.annotation.Config") + } + ClassTypeAndroid.WORKER -> { appendLine("import androidx.work.testing.TestWorkerBuilder") appendLine("import androidx.work.Worker") @@ -87,10 +101,6 @@ class TestGeneratorAndroid : TestGenerator { - appendLine("import kotlinx.coroutines.flow.first") - } - // ClassTypeAndroid.VIEWMODEL -> { // todo generate correct tests for viewModel // classDefinition.dependencies.forEach { dep -> @@ -123,13 +133,19 @@ class TestGeneratorAndroid : TestGenerator> ): String { - val xa = mutableListOf() - - if (classDefinition.dependencies.isNotEmpty()) { - classDefinition.dependencies.mapIndexed { index, dep -> - val s = a.filter { it.key == dep.sourceModuleId } - if (s.isNotEmpty()) { - val x = s.values.flatten().first { it.type == ClassTypeAndroid.API } - if (x != null) { - val apiClassName = x.className - xa.add("$apiClassName()") - } - } - } - } else "" + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val databaseClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.DATABASE }?.className + ?: "Database${moduleDefinition.moduleNumber}_1" + val daoClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.DAO }?.className + ?: "Dao${moduleDefinition.moduleNumber}_1" + val entityClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.ENTITY }?.className + ?: "Entity${moduleDefinition.moduleNumber}_1" + val repositoryClass = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" - val constructorParams = xa.joinToString(",\n ") val testContent = """ + | private lateinit var db: $databaseClass + | private lateinit var dao: $daoClass + | private lateinit var repository: $repositoryClass + | | @Before | fun setup() { + | val context = ApplicationProvider.getApplicationContext() + | db = Room.inMemoryDatabaseBuilder(context, $databaseClass::class.java) + | .allowMainThreadQueries() + | .build() + | dao = db.dao() + | repository = $repositoryClass(dao) + | } + | + | @After + | fun tearDown() { + | db.close() | } | | @Test - | fun `test getData returns data`() = runTest { - | val result = ${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}($constructorParams).getData() - | assertNotNull(result) + | fun `observeItems emits items`() = runTest { + | dao.upsertAll(listOf($entityClass(id = 1, title = "Hello", updatedAt = 1L))) + | val items = repository.observeItems().first() + | assertTrue(items.isNotEmpty()) | } """.trimMargin() @@ -187,6 +212,35 @@ class TestGeneratorAndroid : TestGenerator, @@ -316,50 +370,62 @@ class TestGeneratorAndroid : TestGenerator> + ): String { + val moduleId = NameMappings.moduleName(moduleDefinition.moduleId) + val databaseClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.DATABASE }?.className + ?: "Database${moduleDefinition.moduleNumber}_1" + val daoClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.DAO }?.className + ?: "Dao${moduleDefinition.moduleNumber}_1" + val entityClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.ENTITY }?.className + ?: "Entity${moduleDefinition.moduleNumber}_1" + val repositoryClass = a[moduleId]?.firstOrNull { it.type == ClassTypeAndroid.REPOSITORY }?.className + ?: "Repository${moduleDefinition.moduleNumber}_1" return """ + | private lateinit var db: $databaseClass + | private lateinit var dao: $daoClass + | private lateinit var repository: $repositoryClass | private lateinit var useCase: $className | | @Before | fun setup() { - | useCase = $className() + | val context = ApplicationProvider.getApplicationContext() + | db = Room.inMemoryDatabaseBuilder(context, $databaseClass::class.java) + | .allowMainThreadQueries() + | .build() + | dao = db.dao() + | repository = $repositoryClass(dao) + | useCase = $className(repository) + | } + | + | @After + | fun tearDown() { + | db.close() | } | | @Test - | fun `test invoke returns data`() = runTest { - | val result = useCase().first() - | assertEquals("Data from $className UseCase", result) + | fun `invoke returns items`() = runTest { + | dao.upsertAll(listOf($entityClass(id = 1, title = "Hi", updatedAt = 1L))) + | val items = useCase().first() + | assertTrue(items.isNotEmpty()) | } """.trimMargin() } - - } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroidLegacy.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroidLegacy.kt new file mode 100644 index 00000000..5782c029 --- /dev/null +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroidLegacy.kt @@ -0,0 +1,365 @@ +package io.github.cdsap.projectgenerator.generator.test + +import io.github.cdsap.projectgenerator.generator.classes.GenerateDictionaryAndroid +import io.github.cdsap.projectgenerator.writer.TestGenerator +import io.github.cdsap.projectgenerator.model.ClassDefinitionAndroid +import io.github.cdsap.projectgenerator.model.ClassDependencyAndroid +import io.github.cdsap.projectgenerator.model.ClassTypeAndroid +import io.github.cdsap.projectgenerator.model.ModuleClassDefinitionAndroid +import io.github.cdsap.projectgenerator.NameMappings +import java.io.File +import java.util.concurrent.CopyOnWriteArrayList + + +class TestGeneratorAndroidLegacy : TestGenerator { + + + override fun generate( + moduleDefinition: ModuleClassDefinitionAndroid, + projectName: String, + classesDictionary: MutableMap> + ) { + val layerDir = NameMappings.layerName(moduleDefinition.layer) + val moduleDir = NameMappings.moduleName(moduleDefinition.moduleId) + val packageDir = NameMappings.modulePackageName(moduleDefinition.moduleId) + val testDir = + File("$projectName/$layerDir/$moduleDir/src/test/kotlin/com/awesomeapp/$packageDir/") + testDir.mkdirs() + + moduleDefinition.classes.forEach { classDefinition -> + val testFileName = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}Test.kt" + val testContent = generateTestContent(moduleDefinition, classDefinition, classesDictionary) + File(testDir, testFileName).writeText(testContent) + } + } + + private fun generateTestContent( + moduleDefinition: ModuleClassDefinitionAndroid, + classDefinition: ClassDefinitionAndroid, + classesDictionary: MutableMap> + ): String { + + val className = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}" + val testContent = when (classDefinition.type) { + ClassTypeAndroid.REPOSITORY -> generateRepositoryTest(moduleDefinition, classDefinition, classesDictionary) + ClassTypeAndroid.API -> generateApiTest(className) + // todo generate correct tests for viewModel +// ClassTypeAndroid.VIEWMODEL -> generateViewModelTest( +// className, +// classDefinition.dependencies, +// classesDictionary +// ) + + ClassTypeAndroid.WORKER -> generateWorkerTest(className) + ClassTypeAndroid.ACTIVITY -> generateActivityTest(className) + ClassTypeAndroid.FRAGMENT -> generateFragmentTest(className) + ClassTypeAndroid.SERVICE -> generateServiceTest(className) + ClassTypeAndroid.STATE -> generateStateTest(className) + ClassTypeAndroid.MODEL -> generateModelTest(className) + ClassTypeAndroid.USECASE -> generateUseCaseTest(moduleDefinition, className) + else -> "" + } + + val imports = buildString { + appendLine("import org.junit.Test") + appendLine("import org.junit.Before") + appendLine("import org.junit.runner.RunWith") + appendLine("import org.junit.runners.JUnit4") + appendLine("import org.junit.Rule") + appendLine("import kotlinx.coroutines.test.runTest") + appendLine("import kotlinx.coroutines.ExperimentalCoroutinesApi") + appendLine("import org.junit.Assert.*") + appendLine("import kotlin.test.assertTrue") + appendLine("import kotlin.test.assertNotNull") + appendLine("import kotlin.test.assertEquals") + appendLine("import kotlin.test.assertFalse") + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)}.*") + + // Add specific imports based on class type + when (classDefinition.type) { + ClassTypeAndroid.WORKER -> { + appendLine("import androidx.work.testing.TestWorkerBuilder") + appendLine("import androidx.work.Worker") + appendLine("import androidx.work.WorkerParameters") + appendLine("import androidx.test.core.app.ApplicationProvider") + appendLine("import android.content.Context") + appendLine("import androidx.work.testing.TestListenableWorkerBuilder") + appendLine("import androidx.work.CoroutineWorker") + } + + ClassTypeAndroid.USECASE -> { + appendLine("import kotlinx.coroutines.flow.first") + } + + // ClassTypeAndroid.VIEWMODEL -> { + // todo generate correct tests for viewModel +// classDefinition.dependencies.forEach { dep -> +// val s = classesDictionary.filter { it.key == dep.sourceModuleId } +// if (s.isNotEmpty()) { +// val x = s.values.flatten().first { it.type == ClassTypeAndroid.REPOSITORY } +// if (x != null) { +// val repository = x.className +// appendLine("import com.awesomeapp.${NameMappings.modulePackageName(dep.sourceModuleId)}.$repository") +// } +// } +// } + // } + + ClassTypeAndroid.REPOSITORY -> { + classDefinition.dependencies.forEach { dep -> + val s = classesDictionary.filter { it.key == dep.sourceModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.API } + if (x != null) { + val repository = x.className + appendLine("import com.awesomeapp.${NameMappings.modulePackageName(dep.sourceModuleId)}.$repository") + } + } + } + } + + else -> {} + } + } + + val classAnnotations = "@OptIn(ExperimentalCoroutinesApi::class)" + + return """ + |package com.awesomeapp.${NameMappings.modulePackageName(moduleDefinition.moduleId)} + | + |$imports + | + |$classAnnotations + |class ${className}Test { + | $testContent + |} + """.trimMargin() + } + + private fun generateRepositoryTest( + moduleDefinition: ModuleClassDefinitionAndroid, + classDefinition: ClassDefinitionAndroid, + a: MutableMap> + ): String { + val xa = mutableListOf() + + if (classDefinition.dependencies.isNotEmpty()) { + classDefinition.dependencies.mapIndexed { index, dep -> + val s = a.filter { it.key == dep.sourceModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.API } + if (x != null) { + val apiClassName = x.className + xa.add("$apiClassName()") + } + } + } + } else "" + + val constructorParams = xa.joinToString(",\n ") + val testContent = """ + | @Before + | fun setup() { + | } + | + | @Test + | fun `test getData returns data`() = runTest { + | val result = ${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}($constructorParams).getData() + | assertNotNull(result) + | } + """.trimMargin() + + return testContent + } + + private fun generateApiTest(className: String): String = """ + | private lateinit var api: $className + | + | @Before + | fun setup() { + | api = $className() + | } + | + | @Test + | fun `test fetchData returns data`() = runTest { + | val result = api.fetchData() + | assertNotNull(result) + | } + """.trimMargin() + + private fun generateViewModelTest( + className: String, + dependencies: List, + a: MutableMap> + ): String { + val xa = mutableListOf() + val xc = mutableListOf() + dependencies.mapIndexed { index, dep -> + val s = a.filter { it.key == dep.sourceModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.REPOSITORY } + if (x != null) { + val repository = x.className + xa.add("repository$index: $repository") + } + } + } + + val constructorParams = xa.joinToString(",\n ") + + val xb = mutableListOf() + dependencies.mapIndexed { index, dep -> + val s = a.filter { it.key == dep.sourceModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.REPOSITORY } + if (x != null) { + val repository = x.className + xb.add("private lateinit var repository$index: $repository") + } + } + } + val mockRepositories = xb.joinToString("\n ") + + dependencies.mapIndexed { index, dep -> + + val s = a.filter { it.key == dep.sourceModuleId } + if (s.isNotEmpty()) { + val x = s.values.flatten().first { it.type == ClassTypeAndroid.REPOSITORY } + if (x != null) { + val repository = x.className + xc.add( + "repository$index = $repository(${ + x.dependencies.mapIndexed { depIndex, innerDep -> + val innerS = a.filter { it.key == innerDep.sourceModuleId } + if (innerS.isNotEmpty()) { + val innerX = innerS.values.flatten().first { it.type == ClassTypeAndroid.API } + if (innerX != null) { + "com.awesomeapp.${NameMappings.modulePackageName(innerDep.sourceModuleId)}.${innerX.className}()" + } else "" + } else "" + }.joinToString(", ") + })" + ) + + } + } + } + + val testContent = if (constructorParams.isNotEmpty()) { + """ + | $mockRepositories + | + | private lateinit var viewModel: $className + | + | @Before + | fun setup() { + | ${xc.joinToString("\n ")} + | viewModel = $className( + | ${dependencies.mapIndexed { index, _ -> "repository$index" }.joinToString(",\n ")} + | ) + | } + | + | @Test + | fun `test state updates with data`() = runTest { + | assertNotNull(viewModel.state.value) + | } + """.trimMargin() + } else { + """ + | private lateinit var viewModel: $className + | + | @Before + | fun setup() { + | viewModel = $className() + | } + | + | @Test + | fun `test state updates with data`() = runTest { + | assertNotNull(viewModel.state.value) + | } + """.trimMargin() + } + return testContent + } + + private fun generateWorkerTest(className: String): String = """ + | @Test + | fun `placeholder - worker should be tested in androidTest`() { + | // Workers depend on Android Context and should be tested with Instrumented tests (androidTest) + | assertTrue(true) + | } + """.trimMargin() + + private fun generateActivityTest(className: String): String = """ + | @Test + | fun `placeholder - activity should be tested in androidTest`() { + | // Activities should be tested with Instrumented tests (androidTest) + | assertTrue(true) + | } + """.trimMargin() + + private fun generateFragmentTest(className: String): String = """ + | @Test + | fun `placeholder - fragment should be tested in androidTest`() { + | // Fragments should be tested with Instrumented tests (androidTest) + | assertTrue(true) + | } + """.trimMargin() + + private fun generateServiceTest(className: String): String = """ + | @Test + | fun `placeholder - service should be tested in androidTest`() { + | // Services should be tested with Instrumented tests (androidTest) + | assertTrue(true) + | } + """.trimMargin() + + private fun generateStateTest(className: String): String = """ + | @Test + | fun `test loading state`() { + | val state = $className.Loading + | assertNotNull(state) + | } + | + | @Test + | fun `test success state`() { + | val state = $className.Success("test data") + | assertNotNull(state) + | assertEquals("test data", (state as $className.Success).data) + | } + | + | @Test + | fun `test error state`() { + | val state = $className.Error("test error") + | assertNotNull(state) + | assertEquals("test error", (state as $className.Error).message) + | } + """.trimMargin() + + private fun generateModelTest(className: String): String = """ + | @Test + | fun `test model creation`() { + | val model = $className() + | assertNotNull(model) + | } + """.trimMargin() + + private fun generateUseCaseTest(moduleDefinition: ModuleClassDefinitionAndroid, className: String): String { + return """ + | private lateinit var useCase: $className + | + | @Before + | fun setup() { + | useCase = $className() + | } + | + | @Test + | fun `test invoke returns data`() = runTest { + | val result = useCase().first() + | assertEquals("Data from $className UseCase", result) + | } + """.trimMargin() + } + + +} diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt index e9fe83cc..08f13fc7 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/toml/AndroidToml.kt @@ -21,6 +21,7 @@ class AndroidToml { activity = "${version.android.activity}" constraintlayout = "${version.android.constraintlayout}" work = "${version.android.work}" + room = "${version.android.room}" ${hiltVersions(version, version.di)} ${metroVersions(version, version.di)} compose-bom = "${version.android.composeBom}" @@ -47,6 +48,10 @@ class AndroidToml { activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } + room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } + room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } + room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } + room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } ${hiltLibraries(version.di)} ${metroLibraries(version.di)} kotlin-jvm-metadata = { group = "org.jetbrains.kotlin", name = "kotlin-metadata-jvm", version.ref = "kotlin"} @@ -94,7 +99,7 @@ class AndroidToml { kotlin-ksp = { id ="com.google.devtools.ksp", version.ref = "ksp" } """.trimIndent() - fun tomlImplementations(version: Versions, di: DependencyInjection) = """ + fun tomlImplementations(version: Versions, di: DependencyInjection, roomDatabase: Boolean = false) = """ |implementation(libs.androidx.core.ktx) |implementation(libs.appcompat) |implementation(libs.material) @@ -107,6 +112,7 @@ class AndroidToml { |implementation(libs.activity.ktx) |implementation(libs.constraintlayout) |implementation(libs.work.runtime.ktx) + |${roomDependencies(version, roomDatabase)} |${hiltDependencies(version, di)} |${metroDependencies(di)} @@ -136,6 +142,19 @@ class AndroidToml { |androidTestImplementation(libs.espresso.core) """.trimMargin() + private fun roomDependencies(version: Versions, roomDatabase: Boolean): String { + return if (roomDatabase) { + """ + implementation(libs.room.runtime) + implementation(libs.room.ktx) + add("${kotlinProcessor(version)}", libs.room.compiler) + testImplementation(libs.room.testing) + """.trimIndent() + } else { + "" + } + } + fun kotlinProcessor(version: Versions): String { if (version.kotlin.kotlinProcessor.processor == Processor.KAPT) { return "kapt" @@ -229,11 +248,11 @@ class AndroidToml { implementation(libs.hilt.work) implementation(libs.hilt.android) - ${kotlinProcessor(version)}(libs.hilt.compiler.androidx) - ${kotlinProcessor(version)}(libs.hilt.compiler) - ${kotlinProcessor(version)}(libs.kotlin.jvm.metadata) - ${kotlinProcessor(version)}Test(libs.hilt.compiler) - ${kotlinProcessor(version)}AndroidTest(libs.hilt.compiler) + add("${kotlinProcessor(version)}", libs.hilt.compiler.androidx) + add("${kotlinProcessor(version)}", libs.hilt.compiler) + add("${kotlinProcessor(version)}", libs.kotlin.jvm.metadata) + add("${kotlinProcessor(version)}Test", libs.hilt.compiler) + add("${kotlinProcessor(version)}AndroidTest", libs.hilt.compiler) testImplementation(libs.hilt.android.testing) """.trimIndent() } else { diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/ClassTypeAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/ClassTypeAndroid.kt index 11ace52d..494382dc 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/ClassTypeAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/ClassTypeAndroid.kt @@ -7,11 +7,15 @@ enum class ClassTypeAndroid { VIEWMODEL, REPOSITORY, API, + ENTITY, + DAO, + DATABASE, WORKER, ACTIVITY, FRAGMENT, SERVICE, STATE, + SCREEN, MODEL, USECASE; // Semicolon might be needed if methods are present, adding for safety @@ -24,6 +28,8 @@ enum class ClassTypeAndroid { REPOSITORY -> true SERVICE -> true USECASE -> true + DAO -> true + SCREEN -> true else -> false } } @@ -35,6 +41,8 @@ enum class ClassTypeAndroid { REPOSITORY -> API SERVICE -> REPOSITORY USECASE -> REPOSITORY + DAO -> DATABASE + SCREEN -> VIEWMODEL else -> null } } diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt index 4e194692..812b4dd4 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/model/Versions.kt @@ -74,6 +74,8 @@ data class Android( val activity: String = "1.12.4", val constraintlayout: String = "2.2.1", val work: String = "2.11.1", + val room: String = "2.8.4", + val roomDatabase: Boolean = false, val hilt: String = "2.59.1", val hiltAandroidx: String = "1.3.0", val metro: String = "0.10.3", diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/AndroidModulesWriter.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/AndroidModulesWriter.kt index d6f7e718..ee3463db 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/AndroidModulesWriter.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/AndroidModulesWriter.kt @@ -2,10 +2,13 @@ package io.github.cdsap.projectgenerator.writer import io.github.cdsap.projectgenerator.generator.buildfiles.BuildFilesGeneratorAndroid import io.github.cdsap.projectgenerator.generator.classes.ClassGeneratorAndroid +import io.github.cdsap.projectgenerator.generator.classes.ClassGeneratorAndroidLegacy import io.github.cdsap.projectgenerator.generator.classes.GenerateDictionaryAndroid import io.github.cdsap.projectgenerator.generator.planner.ModuleClassPlannerAndroid +import io.github.cdsap.projectgenerator.generator.planner.ModuleClassPlannerAndroidLegacy import io.github.cdsap.projectgenerator.generator.resources.ResourceGenerator import io.github.cdsap.projectgenerator.generator.test.TestGeneratorAndroid +import io.github.cdsap.projectgenerator.generator.test.TestGeneratorAndroidLegacy import io.github.cdsap.projectgenerator.model.* class AndroidModulesWriter( @@ -16,10 +19,10 @@ class AndroidModulesWriter( versions: Versions, di: DependencyInjection ) : ModulesWrite( - classGenerator = ClassGeneratorAndroid(di), - classPlanner = ModuleClassPlannerAndroid(), - testGenerator = TestGeneratorAndroid(), - resourceGeneratorA = ResourceGenerator(di), + classGenerator = if (versions.android.roomDatabase) ClassGeneratorAndroid(di) else ClassGeneratorAndroidLegacy(di), + classPlanner = if (versions.android.roomDatabase) ModuleClassPlannerAndroid() else ModuleClassPlannerAndroidLegacy(), + testGenerator = if (versions.android.roomDatabase) TestGeneratorAndroid() else TestGeneratorAndroidLegacy(), + resourceGeneratorA = ResourceGenerator(di, versions.android.roomDatabase), generateUnitTest = generateUnitTest, buildFilesGenerator = BuildFilesGeneratorAndroid(versions, di), resources = typeOfStringResources, diff --git a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt index c5695b3a..3bba6a94 100644 --- a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt +++ b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/writer/ProjectWriterTest.kt @@ -3,6 +3,7 @@ package io.github.cdsap.projectgenerator import io.github.cdsap.projectgenerator.model.* import io.github.cdsap.projectgenerator.writer.GradleWrapper import io.github.cdsap.projectgenerator.writer.ProjectWriter +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -63,4 +64,39 @@ class ProjectWriterTest { "File does not exist or is not a regular file: $file" } } + + @Test + fun `generates manual room wiring for DI none`() { + val node = ProjectGraph("module_1_1", 1, emptyList(), TypeProject.ANDROID_APP, 12) + val language = LanguageAttributes("gradle.kts", "${tempDir}/project_kts") + val versions = Versions( + di = DependencyInjection.NONE, + android = Android(roomDatabase = true) + ) + + val projectWriter = ProjectWriter( + listOf(node), + listOf(language), + versions, + TypeProjectRequested.ANDROID, + TypeOfStringResources.NORMAL, + false, + GradleWrapper(Gradle.GRADLE_9_3_0), + false, + "manual_room_none" + ) + + projectWriter.write() + + val activityFile = File( + "${language.projectName}/${NameMappings.layerName(1)}/${NameMappings.moduleName("module_1_1")}" + + "/src/main/kotlin/com/awesomeapp/${NameMappings.modulePackageName("module_1_1")}/Activity1_10.kt" + ) + + assertTrue(activityFile.exists(), "Expected generated Activity file to exist") + val content = activityFile.readText() + assertTrue(content.contains("ViewModelProvider.Factory")) + assertTrue(content.contains("Room.databaseBuilder")) + assertTrue(content.contains("by viewModels { viewModelFactory }")) + } }