diff --git a/domain/src/main/java/com/acon/acon/domain/model/spot/SpotListRequest.kt b/domain/src/main/java/com/acon/acon/domain/model/spot/SpotListRequest.kt index aa649ee99..84a17f726 100644 --- a/domain/src/main/java/com/acon/acon/domain/model/spot/SpotListRequest.kt +++ b/domain/src/main/java/com/acon/acon/domain/model/spot/SpotListRequest.kt @@ -9,7 +9,7 @@ import com.acon.acon.domain.type.SpotType data class Condition( val spotType: SpotType?, val filterList: List?, - val walkingTime: Int, + val walkingTime: Int?, val priceRange: Int? ) { diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt index 0e481c707..80c6a7f29 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt +++ b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource +import coil3.compose.AsyncImage import coil3.compose.rememberAsyncImagePainter import com.acon.acon.core.designsystem.noRippleClickable import com.acon.acon.feature.profile.R @@ -52,12 +53,13 @@ fun ProfilePhotoBox( ) } - else -> { - Icon( + photoUri.startsWith("https://") -> { + AsyncImage( + model = photoUri, + contentDescription = "선택한 프로필 사진", modifier = Modifier.fillMaxSize(), - imageVector = ImageVector.vectorResource(R.drawable.img_profile_basic_80), - contentDescription = "Profile Image", - tint = Color.Unspecified, + contentScale = ContentScale.Crop, + alignment = Alignment.Center ) } } diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreen.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreen.kt index 9c00eed97..b12be01e6 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreen.kt @@ -60,6 +60,7 @@ fun ProfileScreen( val snackBarText = stringResource(R.string.snackbar_profile_save_success) val success = stringResource(R.string.success) + LaunchedEffect(profileUpdateResult) { if (profileUpdateResult == success) { snackbarHostState.showSnackbar(snackBarText) @@ -100,7 +101,7 @@ fun ProfileScreen( modifier = Modifier .padding(vertical = 32.dp) ) { - if (state.profileImage.isNotEmpty()) { + if (state.profileImage.isEmpty()) { Image( imageVector = ImageVector.vectorResource(com.acon.acon.core.designsystem.R.drawable.ic_default_profile_40), contentDescription = stringResource(R.string.content_description_default_profile_image), diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModel.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModel.kt index 5569ca7b2..1792d9243 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModel.kt +++ b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModel.kt @@ -100,24 +100,27 @@ class ProfileModViewModel @Inject constructor( val filteredText = text.filter { it.isAllowedChar() } var nicknameCount = 0 + var limitedText = "" for (char in filteredText) { - nicknameCount += if (char.isKorean()) 2 else 1 - if (nicknameCount > 16) break + val weight = if (char.isKorean()) 2 else 1 + if (nicknameCount + weight > 16) break + limitedText += char + nicknameCount += weight } val updatedNicknameStatus = when { - filteredText.isEmpty() -> NicknameStatus.Empty + text.isEmpty() -> NicknameStatus.Empty else -> NicknameStatus.Typing } val updatedFieldStatus = when { - filteredText.isEmpty() -> TextFieldStatus.Empty + text.isEmpty() -> TextFieldStatus.Empty else -> state.nickNameFieldStatus } reduce { state.copy( - nickNameState = filteredText, + nickNameState = limitedText, nicknameCount = nicknameCount, nicknameStatus = updatedNicknameStatus, nickNameFieldStatus = updatedFieldStatus @@ -134,23 +137,23 @@ class ProfileModViewModel @Inject constructor( val errors = mutableListOf() - if (filteredText.any { it in "!@#$%^&*()-=+[]{};:'\",<>/?\\|" }) { + if (text.any { it in "!@#$%^&*()-=+[]{};:'\",<>/?\\|" }) { errors.add(NicknameErrorType.InvalidChar) } val allowedChars = (33..126).map { it.toChar() } + ('가'..'힣') + ('ㄱ'..'ㅎ') + ('ㅏ'..'ㅣ') - if (filteredText.any { it !in allowedChars }) { + if (text.any { it !in allowedChars }) { errors.add(NicknameErrorType.InvalidLang) } - validateNickname(nickname = filteredText, errors = errors) + validateNickname(nickname = limitedText, errors = errors) intent { reduce { state.copy( nicknameStatus = when { - filteredText.isBlank() -> NicknameStatus.Empty + limitedText.isBlank() -> NicknameStatus.Empty errors.isNotEmpty() -> NicknameStatus.Error(errors) else -> NicknameStatus.Valid } diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreen.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreen.kt index f79e4f012..66331b703 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreen.kt +++ b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreen.kt @@ -68,6 +68,7 @@ import com.acon.acon.feature.profile.composable.utils.BirthdayTransformation import com.acon.acon.feature.profile.composable.utils.isAllowedChar import com.acon.acon.feature.profile.composable.utils.isKorean import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ProfileModScreenContainer( @@ -82,42 +83,39 @@ fun ProfileModScreenContainer( val context = LocalContext.current LaunchedEffect(selectedPhotoId) { - selectedPhotoId.let { - viewModel.updateProfileImage(selectedPhotoId) - } + viewModel.updateProfileImage(selectedPhotoId) } - LaunchedEffect(Unit) { - viewModel.container.sideEffectFlow.collect { effect -> - when (effect) { - is ProfileModSideEffect.NavigateToSettings -> { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", effect.packageName, null) - } - context.startActivity(intent) - } - is ProfileModSideEffect.NavigateBack -> { - backToProfile() + viewModel.collectSideEffect { effect -> + when (effect) { + is ProfileModSideEffect.NavigateToSettings -> { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", effect.packageName, null) } + context.startActivity(intent) + } - is ProfileModSideEffect.NavigateToCustomGallery -> { - onNavigateToCustomGallery() - } + is ProfileModSideEffect.NavigateBack -> { + backToProfile() + } - is ProfileModSideEffect.UpdateProfileImage -> { - selectedPhotoId.let { - viewModel.updateProfileImage(selectedPhotoId) - } - } + is ProfileModSideEffect.NavigateToCustomGallery -> { + onNavigateToCustomGallery() + } - is ProfileModSideEffect.NavigateToProfileSuccess -> { - onNavigateToProfile(ProfileUpdateResult.SUCCESS) + is ProfileModSideEffect.UpdateProfileImage -> { + selectedPhotoId.let { + viewModel.updateProfileImage(selectedPhotoId) } + } - is ProfileModSideEffect.NavigateToProfileFailed -> { - onNavigateToProfile(ProfileUpdateResult.FAILURE) - } + is ProfileModSideEffect.NavigateToProfileSuccess -> { + onNavigateToProfile(ProfileUpdateResult.SUCCESS) + } + + is ProfileModSideEffect.NavigateToProfileFailed -> { + onNavigateToProfile(ProfileUpdateResult.FAILURE) } } } @@ -326,22 +324,23 @@ fun ProfileModScreen( onTextChanged = { fieldValue -> val inputText = fieldValue.text var count = 0 + var textLimit = false - val validText = buildString { + val displayText = buildString { for (char in inputText) { - if (!char.isAllowedChar()) continue val weight = if (char.isKorean()) 2 else 1 - if (count + weight > 16) break - append(char) - count += weight + if (count + weight > 16) { + textLimit = true + break + } + if (char.isAllowedChar()) { + append(char) + count += weight + } } } - - if (validText.length < inputText.length) { - // 초과된 입력이므로 무시하고 아무것도 하지 않음 (16자 이상은 입력 무시(불가능)) - } else { - // 정상적인 입력만 가능 - nicknameText = fieldValue + if (!textLimit || displayText.length <= nicknameText.text.length) { + nicknameText = fieldValue.copy(text = displayText) onNicknameChanged(inputText) } }, @@ -486,9 +485,11 @@ fun ProfileModScreen( enabledTextColor = AconTheme.color.White, onClick = onSaveClicked, isEnabled = (state.nicknameStatus == NicknameStatus.Valid) && - (state.birthdayStatus != BirthdayStatus.Invalid("정확한 생년월일을 입력해주세요")) && - ((state.nickNameState != state.originalNickname || state.birthdayState != state.originalBirthday || state.selectedPhotoUri != state.originalPhotoUri) - && state.birthdayState.isNotEmpty()) + ( + (state.nickNameState != state.originalNickname) || + (state.birthdayState != state.originalBirthday && state.birthdayStatus == BirthdayStatus.Valid) || + (state.selectedPhotoUri != state.originalPhotoUri) + ) ) } } diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt index ff05c5660..04c0e4f62 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt @@ -50,7 +50,7 @@ class SpotListViewModel @Inject constructor( fun googleLogin(socialRepository: SocialRepository, location: Location) = intent { socialRepository.googleLogin() .onSuccess { - if(it.hasVerifiedArea) { + if (it.hasVerifiedArea) { fetchSpots(location) } else { postSideEffect(SpotListSideEffect.NavigateToAreaVerification) @@ -72,7 +72,8 @@ class SpotListViewModel @Inject constructor( spotRepository.fetchSpotList( latitude = location.latitude, longitude = location.longitude, - condition = Condition.Default, + condition = (state as? SpotListUiState.Success)?.currentCondition?.toCondition() + ?: Condition.Default, ) } @@ -92,10 +93,18 @@ class SpotListViewModel @Inject constructor( spotListResult.reduceResult( syntax = this, onSuccess = { - SpotListUiState.Success( + (state as? SpotListUiState.Success)?.copy( spotList = it, + isRefreshing = false, + userType = userType.value, legalAddressName = legalArea.area, + isFilteredResultFetching = it.isEmpty() + ) ?: SpotListUiState.Success( + spotList = it, + isRefreshing = false, userType = userType.value, + legalAddressName = legalArea.area, + isFilteredResultFetching = it.isEmpty() ) }, onFailure = { when (it) { diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt index 2bfc50c1d..5e62fef43 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -48,7 +47,6 @@ import com.acon.acon.core.designsystem.component.bottomsheet.LoginBottomSheet import com.acon.acon.core.designsystem.component.loading.SkeletonItem import com.acon.acon.core.designsystem.theme.AconTheme import com.acon.acon.core.utils.feature.action.BackOnPressed -import com.acon.acon.core.utils.feature.amplitude.AconAmplitude import com.acon.acon.domain.type.SpotType import com.acon.acon.domain.type.UserType import com.acon.acon.feature.spot.R @@ -66,8 +64,8 @@ import com.acon.acon.feature.spot.amplitudeFilterWalkSlideCafe import com.acon.acon.feature.spot.amplitudeFilterWalkSlideRestaurant import com.acon.acon.feature.spot.screen.spotlist.SpotListUiState import com.acon.acon.feature.spot.screen.spotlist.amplitude.amplitudeSpotListSignIn -import com.acon.acon.feature.spot.screen.spotlist.composable.bottomsheet.SpotFilterBottomSheet import com.acon.acon.feature.spot.screen.spotlist.amplitude.spotListSpotNumberAmplitude +import com.acon.acon.feature.spot.screen.spotlist.composable.bottomsheet.SpotFilterBottomSheet import com.acon.acon.feature.spot.state.ConditionState import com.acon.acon.feature.spot.type.AvailableWalkingTimeType import com.acon.acon.feature.spot.type.CafePriceRangeType @@ -156,12 +154,16 @@ internal fun SpotListScreen( amplitudeFilterRestaurant() if (it.restaurantFeatureOptionType.isNotEmpty()) { - val restaurantCategories = it.restaurantFeatureOptionType.map { option -> option.name }.toSet() + val restaurantCategories = + it.restaurantFeatureOptionType.map { option -> option.name } + .toSet() amplitudeFilterVisitRestaurant(restaurantCategories) } if (it.companionTypeOptionType.isNotEmpty()) { - val companions = it.companionTypeOptionType.map { option -> option.name }.toSet() + val companions = + it.companionTypeOptionType.map { option -> option.name } + .toSet() amplitudeFilterPassengerRestaurant(companions) } @@ -172,8 +174,12 @@ internal fun SpotListScreen( AvailableWalkingTimeType.UNDER_20_MINUTES -> "20분" AvailableWalkingTimeType.OVER_20_MINUTES -> "25분 이상" } - val isWalkingTimeDefault = it.restaurantWalkingTime == AvailableWalkingTimeType.UNDER_15_MINUTES - amplitudeFilterWalkSlideRestaurant(walkingTime, isWalkingTimeDefault) + val isWalkingTimeDefault = + it.restaurantWalkingTime == AvailableWalkingTimeType.UNDER_15_MINUTES + amplitudeFilterWalkSlideRestaurant( + walkingTime, + isWalkingTimeDefault + ) val priceRange = when (it.restaurantPriceRange) { @@ -183,21 +189,27 @@ internal fun SpotListScreen( RestaurantPriceRangeType.UNDER_50000 -> "5만원" RestaurantPriceRangeType.OVER_50000 -> "5만원 이상" } - val isPriceDefault = it.restaurantPriceRange == RestaurantPriceRangeType.UNDER_10000 + val isPriceDefault = + it.restaurantPriceRange == RestaurantPriceRangeType.UNDER_10000 amplitudeFilterPriceSlideRestaurant(priceRange, isPriceDefault) - val isCompleteFilter = !(isWalkingTimeDefault && isPriceDefault && it.restaurantFeatureOptionType.isEmpty() && it.companionTypeOptionType.isEmpty()) + val isCompleteFilter = + !(isWalkingTimeDefault && isPriceDefault && it.restaurantFeatureOptionType.isEmpty() && it.companionTypeOptionType.isEmpty()) amplitudeFilterCompleteRestaurant(isCompleteFilter) } else { amplitudeFilterCafe() if (it.cafeFeatureOptionType.isNotEmpty()) { - val cafeCategories = it.cafeFeatureOptionType.map { option -> option.name }.toSet() + val cafeCategories = + it.cafeFeatureOptionType.map { option -> option.name } + .toSet() amplitudeFilterVisitCafe(cafeCategories) } if (it.visitPurposeOptionType.isNotEmpty()) { - val purposes = it.visitPurposeOptionType.map { option -> option.name }.toSet() + val purposes = + it.visitPurposeOptionType.map { option -> option.name } + .toSet() amplitudeFilterPurposeCafe(purposes) } @@ -208,7 +220,8 @@ internal fun SpotListScreen( AvailableWalkingTimeType.UNDER_20_MINUTES -> "20분" AvailableWalkingTimeType.OVER_20_MINUTES -> "25분 이상" } - val isWalkingTimeDefault = it.cafeWalkingTime == AvailableWalkingTimeType.UNDER_15_MINUTES + val isWalkingTimeDefault = + it.cafeWalkingTime == AvailableWalkingTimeType.UNDER_15_MINUTES amplitudeFilterWalkSlideCafe(walkingTime, isWalkingTimeDefault) val priceRange = when (it.cafePriceRange) { @@ -216,10 +229,12 @@ internal fun SpotListScreen( CafePriceRangeType.UNDER_5000 -> "5천원 이하" CafePriceRangeType.OVER_10000 -> "1만원 이상" } - val isPriceDefault = it.cafePriceRange == CafePriceRangeType.UNDER_5000 + val isPriceDefault = + it.cafePriceRange == CafePriceRangeType.UNDER_5000 amplitudeFilterPriceSlideCafe(priceRange, isPriceDefault) - val isCompleteFilter = !(isWalkingTimeDefault && isPriceDefault && it.visitPurposeOptionType.isEmpty() && it.cafeFeatureOptionType.isEmpty()) + val isCompleteFilter = + !(isWalkingTimeDefault && isPriceDefault && it.visitPurposeOptionType.isEmpty() && it.cafeFeatureOptionType.isEmpty()) amplitudeFilterCompleteCafe(isCompleteFilter) } }, @@ -239,11 +254,6 @@ internal fun SpotListScreen( ) } - val isResultEmpty by remember { - derivedStateOf { - state.spotList.isEmpty() - } - } Box( modifier = Modifier.fillMaxSize() ) { @@ -283,7 +293,7 @@ internal fun SpotListScreen( scrollableScreenHeightPx = size.height } ) { - if (isResultEmpty || state.isFilteredListEmpty) { + if (state.isFilteredListEmpty) { Spacer(Modifier.height(100.dp)) EmptySpotListView(modifier = Modifier.fillMaxSize()) } else { @@ -322,7 +332,10 @@ internal fun SpotListScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - modifier = Modifier.padding(top = 38.dp, bottom = 50.dp), + modifier = Modifier.padding( + top = 38.dp, + bottom = 50.dp + ), text = stringResource(R.string.alert_max_spot_count), style = AconTheme.typography.body2_14_reg, color = AconTheme.color.Gray5 diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/state/ConditionState.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/state/ConditionState.kt index ec97401a5..c16d569d7 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/state/ConditionState.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/state/ConditionState.kt @@ -32,21 +32,21 @@ data class ConditionState( ) } - private fun getPriceRange() : Int { + private fun getPriceRange(): Int? { return when (spotType) { SpotType.RESTAURANT -> restaurantPriceRange.value SpotType.CAFE -> cafePriceRange.value } } - private fun getWalkingTime() : Int { + private fun getWalkingTime(): Int? { return when (spotType) { SpotType.RESTAURANT -> restaurantWalkingTime.value SpotType.CAFE -> cafeWalkingTime.value } } - private fun getFilterList() : List { + private fun getFilterList(): List { return when (spotType) { SpotType.RESTAURANT -> { listOf( diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/type/AvailableWalkingTimeType.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/type/AvailableWalkingTimeType.kt index 3fc922aa4..07578aa77 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/type/AvailableWalkingTimeType.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/type/AvailableWalkingTimeType.kt @@ -5,11 +5,11 @@ import com.acon.acon.feature.spot.R enum class AvailableWalkingTimeType( @StringRes val titleResId: Int, - val value: Int + val value: Int? ) { UNDER_5_MINUTES(titleResId = R.string.under_5_minutes, 5), UNDER_10_MINUTES(titleResId = R.string.under_10_minutes, 10), UNDER_15_MINUTES(titleResId = R.string.under_15_minutes, 15), UNDER_20_MINUTES(titleResId = R.string.under_20_minutes, 20), - OVER_20_MINUTES(titleResId = R.string.over_20_minutes, -1), + OVER_20_MINUTES(titleResId = R.string.over_20_minutes, null), } \ No newline at end of file diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/type/RestaurantPriceRangeType.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/type/RestaurantPriceRangeType.kt index 5f0d4bd5e..f0c60e1e1 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/type/RestaurantPriceRangeType.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/type/RestaurantPriceRangeType.kt @@ -5,11 +5,11 @@ import com.acon.acon.feature.spot.R enum class RestaurantPriceRangeType( @StringRes val titleResId: Int, - val value: Int, + val value: Int?, ) { UNDER_5000(titleResId = R.string.under_5000, 5000), UNDER_10000(titleResId = R.string.under_10000, 10000), UNDER_30000(titleResId = R.string.under_30000, 30000), UNDER_50000(titleResId = R.string.under_50000, 50000), - OVER_50000(titleResId = R.string.over_50000, -1), + OVER_50000(titleResId = R.string.over_50000, null), } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ca13bcda9..ca672e0ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,14 @@ [versions] +############### Project ############### +projectApplicationId = "com.acon.acon" +projectCompileSdk = "35" +projectTargetSdk = "35" +projectMinSdk = "28" +projectVersionCode = "10000" +projectVersionName = "1.0.0" +####################################### + +composeCompilerVersion = "1.5.1" accompanistPermissions = "0.37.0" agp = "8.6.0" androidTools = "31.9.1" @@ -45,16 +55,6 @@ lottie = "6.6.2" kotlinxImmutable = "0.3.7" -# Project -projectApplicationId = "com.acon.acon" -projectCompileSdk = "35" -projectTargetSdk = "35" -projectMinSdk = "28" -projectVersionCode = "5" -projectVersionName = "1.0" - -composeCompilerVersion = "1.5.1" - [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }