diff --git a/sample-compose/app/build.gradle.kts b/sample-compose/app/build.gradle.kts index 1ddb5fd9e..df51ea040 100644 --- a/sample-compose/app/build.gradle.kts +++ b/sample-compose/app/build.gradle.kts @@ -145,6 +145,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_VERSION}") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.KOTLINX_COROUTINES_VERSION}") + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:${Versions.KOTLIN_COLLECTIONS_IMMUTABLE_VERSION}") kapt("com.google.dagger:hilt-compiler:${Versions.HILT_VERSION}") diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeScreen.kt index b63436bcc..53bdd088a 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeScreen.kt @@ -21,20 +21,22 @@ import co.nimblehq.sample.compose.ui.models.UiModel import co.nimblehq.sample.compose.ui.showToast import co.nimblehq.sample.compose.ui.theme.ComposeTheme import com.google.accompanist.permissions.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* @Composable fun HomeScreen( + isResultOk: Boolean = false, viewModel: HomeViewModel = hiltViewModel(), navigator: (destination: BaseDestination) -> Unit, - isResultOk: Boolean = false, ) { val context = LocalContext.current viewModel.error.collectAsEffect { e -> e.showToast(context) } viewModel.navigator.collectAsEffect { destination -> navigator(destination) } val isLoading: IsLoading by viewModel.isLoading.collectAsStateWithLifecycle() - val uiModels: List by viewModel.uiModels.collectAsStateWithLifecycle() + val uiModels: ImmutableList by viewModel.uiModels.collectAsStateWithLifecycle() val isFirstTimeLaunch: Boolean by viewModel.isFirstTimeLaunch.collectAsStateWithLifecycle() LaunchedEffect(isFirstTimeLaunch) { @@ -85,7 +87,7 @@ private fun CameraPermission() { @Composable private fun HomeScreenContent( - uiModels: List, + uiModels: ImmutableList, isLoading: IsLoading, onItemClick: (UiModel) -> Unit, onItemLongClick: (UiModel) -> Unit, @@ -116,7 +118,7 @@ private fun HomeScreenContent( private fun HomeScreenPreview() { ComposeTheme { HomeScreenContent( - uiModels = listOf(UiModel("1", "name1"), UiModel("2", "name2"), UiModel("3", "name3")), + uiModels = persistentListOf(UiModel("1", "name1"), UiModel("2", "name2"), UiModel("3", "name3")), isLoading = false, onItemClick = {}, onItemLongClick = {} diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeViewModel.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeViewModel.kt index 593191106..00ec8dca3 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeViewModel.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeViewModel.kt @@ -10,6 +10,9 @@ import co.nimblehq.sample.compose.ui.models.toUiModel import co.nimblehq.sample.compose.ui.screens.main.MainDestination import co.nimblehq.sample.compose.util.DispatchersProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -26,7 +29,7 @@ class HomeViewModel @Inject constructor( private val dispatchersProvider: DispatchersProvider, ) : BaseViewModel() { - private val _uiModels = MutableStateFlow>(emptyList()) + private val _uiModels = MutableStateFlow>(persistentListOf()) val uiModels = _uiModels.asStateFlow() private val _isFirstTimeLaunch = MutableStateFlow(false) @@ -37,7 +40,7 @@ class HomeViewModel @Inject constructor( .injectLoading() .onEach { result -> val uiModels = result.map { it.toUiModel() } - _uiModels.emit(uiModels) + _uiModels.emit(uiModels.toImmutableList()) } .flowOn(dispatchersProvider.io) .catch { e -> _error.emit(e) } diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/ItemList.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/ItemList.kt index 54ead9ea1..37e009413 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/ItemList.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/home/ItemList.kt @@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import co.nimblehq.sample.compose.ui.models.UiModel import co.nimblehq.sample.compose.ui.theme.ComposeTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable fun ItemList( - uiModels: List, + uiModels: ImmutableList, onItemClick: (UiModel) -> Unit, onItemLongClick: (UiModel) -> Unit, modifier: Modifier = Modifier, @@ -33,7 +35,7 @@ fun ItemList( private fun ItemListPreview() { ComposeTheme { ItemList( - uiModels = listOf(UiModel("1", "name1"), UiModel("2", "name2"), UiModel("3", "name3")), + uiModels = persistentListOf(UiModel("1", "name1"), UiModel("2", "name2"), UiModel("3", "name3")), onItemClick = {}, onItemLongClick = {} ) diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/second/SecondScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/second/SecondScreen.kt index 605d093db..9598469f8 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/second/SecondScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/second/SecondScreen.kt @@ -22,9 +22,9 @@ import co.nimblehq.sample.compose.ui.theme.ComposeTheme @Composable fun SecondScreen( + id: String, viewModel: SecondViewModel = hiltViewModel(), navigator: (destination: BaseDestination) -> Unit, - id: String, ) { SecondScreenContent( id = id, @@ -37,11 +37,15 @@ fun SecondScreen( @Composable private fun SecondScreenContent( id: String, + modifier: Modifier = Modifier, onUpdateClick: () -> Unit, ) { - Scaffold(topBar = { - AppBar(R.string.second_title_bar) - }) { paddingValues -> + Scaffold( + topBar = { + AppBar(R.string.second_title_bar) + }, + modifier = modifier, + ) { paddingValues -> Box( modifier = Modifier .fillMaxSize() diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/third/ThirdScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/third/ThirdScreen.kt index 05d20cfa6..73e41b2f9 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/third/ThirdScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/main/third/ThirdScreen.kt @@ -20,19 +20,23 @@ import co.nimblehq.sample.compose.ui.theme.ComposeTheme @Composable fun ThirdScreen( + model: UiModel?, viewModel: ThirdViewModel = hiltViewModel(), navigator: (destination: BaseDestination) -> Unit, - model: UiModel?, ) { ThirdScreenContent(data = model) } @Composable -fun ThirdScreenContent(data: UiModel?) { +fun ThirdScreenContent( + data: UiModel?, + modifier: Modifier = Modifier, +) { Scaffold( topBar = { AppBar(title = R.string.third_title_bar) - } + }, + modifier = modifier, ) { paddingValues -> Box( modifier = Modifier @@ -50,7 +54,7 @@ fun ThirdScreenContent(data: UiModel?) { @Preview @Composable -fun ThirdScreenPreview() { +private fun ThirdScreenPreview() { ComposeTheme { ThirdScreenContent(data = UiModel("1", "name1")) } diff --git a/sample-compose/build.gradle.kts b/sample-compose/build.gradle.kts index 948b8878c..60eb17869 100644 --- a/sample-compose/build.gradle.kts +++ b/sample-compose/build.gradle.kts @@ -17,6 +17,10 @@ plugins { id("org.jetbrains.kotlinx.kover").version(Versions.KOVER_VERSION) } +dependencies { + detektPlugins("io.nlopez.compose.rules:detekt:0.3.3") +} + allprojects { repositories { google() diff --git a/sample-compose/buildSrc/src/main/java/Versions.kt b/sample-compose/buildSrc/src/main/java/Versions.kt index cf87e968b..0c43cc39a 100644 --- a/sample-compose/buildSrc/src/main/java/Versions.kt +++ b/sample-compose/buildSrc/src/main/java/Versions.kt @@ -28,6 +28,7 @@ object Versions { const val KOTLIN_VERSION = "1.8.21" const val KOTLINX_COROUTINES_VERSION = "1.7.1" + const val KOTLIN_COLLECTIONS_IMMUTABLE_VERSION = "0.3.6" const val KOVER_VERSION = "0.7.3" const val MOSHI_VERSION = "1.12.0" diff --git a/sample-compose/detekt-config.yml b/sample-compose/detekt-config.yml index a15e6a621..4b2ad59f1 100644 --- a/sample-compose/detekt-config.yml +++ b/sample-compose/detekt-config.yml @@ -286,6 +286,7 @@ style: MagicNumber: active: true ignoreNumbers: [ '-1', '0', '1', '2' ] + ignoreAnnotated: [ 'Preview' ] ignoreHashCodeFunction: false ignorePropertyDeclaration: true ignoreConstantDeclaration: true @@ -347,3 +348,48 @@ style: active: false WildcardImport: active: false + +Compose: + CompositionLocalAllowlist: + active: true + allowedCompositionLocals: LocalAppColors,LocalAppDimensions,LocalAppShapes,LocalAppStyles,LocalAppTypography + ContentEmitterReturningValues: + active: true + DefaultsVisibility: + active: true + ModifierClickableOrder: + active: true + ModifierComposable: + active: true + ModifierMissing: + active: true + ModifierNaming: + active: true + ModifierNotUsedAtRoot: + active: true + ModifierReused: + active: true + ModifierWithoutDefault: + active: true + MultipleEmitters: + active: true + MutableParams: + active: true + ComposableNaming: + active: true + ComposableParamOrder: + active: true + PreviewAnnotationNaming: + active: true + PreviewPublic: + active: true + RememberMissing: + active: true + RememberContentMissing: + active: true + UnstableCollections: + active: true + ViewModelForwarding: + active: true + ViewModelInjection: + active: true diff --git a/template-compose/app/build.gradle.kts b/template-compose/app/build.gradle.kts index ed899e49b..abaf64ab5 100644 --- a/template-compose/app/build.gradle.kts +++ b/template-compose/app/build.gradle.kts @@ -156,6 +156,10 @@ dependencies { kapt(COMPILER) } + with(Dependencies.Kotlin) { + implementation(COLLECTIONS_IMMUTABLE) + } + with(Dependencies.Log) { implementation(TIMBER) diff --git a/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeScreen.kt b/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeScreen.kt index dda7bd4d4..3debe2577 100644 --- a/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeScreen.kt +++ b/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeScreen.kt @@ -22,6 +22,7 @@ import co.nimblehq.template.compose.ui.models.UiModel import co.nimblehq.template.compose.ui.showToast import co.nimblehq.template.compose.ui.theme.AppTheme.dimensions import co.nimblehq.template.compose.ui.theme.ComposeTheme +import kotlinx.collections.immutable.* import timber.log.Timber @Composable @@ -33,7 +34,7 @@ fun HomeScreen( viewModel.error.collectAsEffect { e -> e.showToast(context) } viewModel.navigator.collectAsEffect { destination -> navigator(destination) } - val uiModels: List by viewModel.uiModels.collectAsStateWithLifecycle() + val uiModels: ImmutableList by viewModel.uiModels.collectAsStateWithLifecycle() HomeScreenContent( title = stringResource(id = R.string.app_name), @@ -44,7 +45,7 @@ fun HomeScreen( @Composable private fun HomeScreenContent( title: String, - uiModels: List + uiModels: ImmutableList ) { Column( modifier = Modifier.fillMaxSize(), @@ -67,7 +68,7 @@ private fun HomeScreenPreview() { ComposeTheme { HomeScreenContent( title = stringResource(id = R.string.app_name), - uiModels = listOf(UiModel(1), UiModel(2), UiModel(3)) + uiModels = persistentListOf(UiModel(1), UiModel(2), UiModel(3)) ) } } diff --git a/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModel.kt b/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModel.kt index 0f2c7a82b..9139d9f67 100644 --- a/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModel.kt +++ b/template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModel.kt @@ -7,6 +7,7 @@ import co.nimblehq.template.compose.ui.models.UiModel import co.nimblehq.template.compose.ui.models.toUiModel import co.nimblehq.template.compose.util.DispatchersProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.* import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -16,7 +17,7 @@ class HomeViewModel @Inject constructor( useCase: UseCase, ) : BaseViewModel() { - private val _uiModels = MutableStateFlow>(emptyList()) + private val _uiModels = MutableStateFlow>(persistentListOf()) val uiModels = _uiModels.asStateFlow() init { @@ -24,7 +25,7 @@ class HomeViewModel @Inject constructor( .injectLoading() .onEach { result -> val uiModels = result.map { it.toUiModel() } - _uiModels.emit(uiModels) + _uiModels.emit(uiModels.toImmutableList()) } .flowOn(dispatchersProvider.io) .catch { e -> _error.emit(e) } diff --git a/template-compose/build.gradle.kts b/template-compose/build.gradle.kts index 9319cd7ef..c6bd6004c 100644 --- a/template-compose/build.gradle.kts +++ b/template-compose/build.gradle.kts @@ -9,6 +9,10 @@ plugins { id(Plugins.KOVER) version Versions.KOVER } +dependencies { + detektPlugins(Plugins.DETEKT_RULES) +} + tasks.register("clean", Delete::class) { delete(rootProject.buildDir) } diff --git a/template-compose/buildSrc/src/main/java/Dependencies.kt b/template-compose/buildSrc/src/main/java/Dependencies.kt index 1bbba810a..aeee4a74d 100644 --- a/template-compose/buildSrc/src/main/java/Dependencies.kt +++ b/template-compose/buildSrc/src/main/java/Dependencies.kt @@ -29,6 +29,8 @@ object Dependencies { object Kotlin { const val COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.KOTLIN_COROUTINES}" + const val COLLECTIONS_IMMUTABLE = + "org.jetbrains.kotlinx:kotlinx-collections-immutable:${Versions.KOTLIN_COLLECTIONS_IMMUTABLE}" } object Log { diff --git a/template-compose/buildSrc/src/main/java/Plugins.kt b/template-compose/buildSrc/src/main/java/Plugins.kt index d22a2cf30..94f770d2f 100644 --- a/template-compose/buildSrc/src/main/java/Plugins.kt +++ b/template-compose/buildSrc/src/main/java/Plugins.kt @@ -4,6 +4,7 @@ object Plugins { const val JAVA_LIBRARY = "java-library" const val DETEKT = "io.gitlab.arturbosch.detekt" + const val DETEKT_RULES = "io.nlopez.compose.rules:detekt:0.3.3" const val HILT_ANDROID = "com.google.dagger.hilt.android" diff --git a/template-compose/buildSrc/src/main/java/Versions.kt b/template-compose/buildSrc/src/main/java/Versions.kt index 04e5458ab..6e86f0643 100644 --- a/template-compose/buildSrc/src/main/java/Versions.kt +++ b/template-compose/buildSrc/src/main/java/Versions.kt @@ -29,6 +29,7 @@ object Versions { const val KOTEST = "5.6.2" const val KOTLIN = "1.9.10" + const val KOTLIN_COLLECTIONS_IMMUTABLE = "0.3.6" const val KOTLIN_COROUTINES = "1.7.1" const val KOVER = "0.7.3" diff --git a/template-compose/detekt-config.yml b/template-compose/detekt-config.yml index a15e6a621..4b2ad59f1 100644 --- a/template-compose/detekt-config.yml +++ b/template-compose/detekt-config.yml @@ -286,6 +286,7 @@ style: MagicNumber: active: true ignoreNumbers: [ '-1', '0', '1', '2' ] + ignoreAnnotated: [ 'Preview' ] ignoreHashCodeFunction: false ignorePropertyDeclaration: true ignoreConstantDeclaration: true @@ -347,3 +348,48 @@ style: active: false WildcardImport: active: false + +Compose: + CompositionLocalAllowlist: + active: true + allowedCompositionLocals: LocalAppColors,LocalAppDimensions,LocalAppShapes,LocalAppStyles,LocalAppTypography + ContentEmitterReturningValues: + active: true + DefaultsVisibility: + active: true + ModifierClickableOrder: + active: true + ModifierComposable: + active: true + ModifierMissing: + active: true + ModifierNaming: + active: true + ModifierNotUsedAtRoot: + active: true + ModifierReused: + active: true + ModifierWithoutDefault: + active: true + MultipleEmitters: + active: true + MutableParams: + active: true + ComposableNaming: + active: true + ComposableParamOrder: + active: true + PreviewAnnotationNaming: + active: true + PreviewPublic: + active: true + RememberMissing: + active: true + RememberContentMissing: + active: true + UnstableCollections: + active: true + ViewModelForwarding: + active: true + ViewModelInjection: + active: true