diff --git a/README.md b/README.md index 2b9d6569..474d8af2 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ The app client with Image library using Unsplash api ### Tools - JetpackCompose -- MVVM -- Coin (DI) +- MVVM + MVI +- Dagger (DI) - Ktor - Coroutines / Flow - Jetpack paging library - Jetpack compose navigation -- Coil (in process for migration from Glide) +- Coil - Material design 3 (Material YOU) ### Installation diff --git a/build-logic/dependencies/src/main/kotlin/AppVersions.kt b/build-logic/dependencies/src/main/kotlin/AppVersions.kt index 2532d551..4f1588b9 100644 --- a/build-logic/dependencies/src/main/kotlin/AppVersions.kt +++ b/build-logic/dependencies/src/main/kotlin/AppVersions.kt @@ -1,4 +1,4 @@ object AppVersions { - const val versionName = "1.62" - const val versionCode = 8 + const val versionName = "1.63" + const val versionCode = 9 } \ No newline at end of file diff --git a/build-logic/dependencies/src/main/kotlin/st.slex.csplashscreen/KotlinAndroid.kt b/build-logic/dependencies/src/main/kotlin/st.slex.csplashscreen/KotlinAndroid.kt index c2bf0dcd..fc1e60bf 100644 --- a/build-logic/dependencies/src/main/kotlin/st.slex.csplashscreen/KotlinAndroid.kt +++ b/build-logic/dependencies/src/main/kotlin/st.slex.csplashscreen/KotlinAndroid.kt @@ -63,6 +63,9 @@ internal fun Project.configureKotlinAndroid( val daggerCompiler = libs.findLibrary("dagger-compiler").get() add("ksp", daggerCompiler) + + val coroutines = libs.findLibrary("coroutines").get() + add("implementation", coroutines) } } diff --git a/core/core/src/main/java/st/slex/csplashscreen/core/core/CoroutineExt.kt b/core/core/src/main/java/st/slex/csplashscreen/core/core/CoroutineExt.kt index c9ed63ad..393d6f80 100644 --- a/core/core/src/main/java/st/slex/csplashscreen/core/core/CoroutineExt.kt +++ b/core/core/src/main/java/st/slex/csplashscreen/core/core/CoroutineExt.kt @@ -1,4 +1,26 @@ package st.slex.csplashscreen.core.core +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine as combinePrimary +import kotlinx.coroutines.flow.map as mapPrimary + object CoroutineExt { -} \ No newline at end of file + + fun StateFlow.mapState( + transform: (a: T) -> R, + ): StateFlow = TransformableStateFlow( + getValue = { transform(this.value) }, + flow = mapPrimary(transform = transform), + ) + + fun StateFlow.combineState( + flow: StateFlow, + transform: (a: T1, b: T2) -> R, + ): StateFlow = TransformableStateFlow( + getValue = { transform(this.value, flow.value) }, + flow = combinePrimary( + flow = flow, + transform = transform, + ), + ) +} diff --git a/core/core/src/main/java/st/slex/csplashscreen/core/core/TransformableStateFlow.kt b/core/core/src/main/java/st/slex/csplashscreen/core/core/TransformableStateFlow.kt new file mode 100644 index 00000000..2adf7e69 --- /dev/null +++ b/core/core/src/main/java/st/slex/csplashscreen/core/core/TransformableStateFlow.kt @@ -0,0 +1,28 @@ +package st.slex.csplashscreen.core.core + +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +class TransformableStateFlow( + val getValue: () -> T, + val flow: Flow, +) : StateFlow { + + override val replayCache: List + get() = listOf(value) + + override val value: T + get() = getValue() + + override suspend fun collect(collector: FlowCollector): Nothing { + coroutineScope { + flow.distinctUntilChanged() + .stateIn(this) + .collect(collector) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/base/BaseViewModel.kt b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/base/BaseViewModel.kt index 43a0f92b..17c295cc 100644 --- a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/base/BaseViewModel.kt +++ b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/base/BaseViewModel.kt @@ -23,4 +23,9 @@ open class BaseViewModel( fun sendAction(action: A) { store.processAction(action) } + + override fun onCleared() { + super.onCleared() + store.destroy() + } } \ No newline at end of file diff --git a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/BaseStore.kt b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/BaseStore.kt index 5deee00c..dd89f13a 100644 --- a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/BaseStore.kt +++ b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/BaseStore.kt @@ -1,13 +1,22 @@ package st.slex.csplashscreen.core.ui.mvi -import st.slex.csplashscreen.core.ui.mvi.Store.Action -import st.slex.csplashscreen.core.ui.mvi.Store.Event -import st.slex.csplashscreen.core.ui.mvi.Store.State +import androidx.paging.PagingData +import androidx.paging.cachedIn import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import st.slex.csplashscreen.core.ui.mvi.Store.Action +import st.slex.csplashscreen.core.ui.mvi.Store.Event +import st.slex.csplashscreen.core.ui.mvi.Store.State abstract class BaseStoreImpl : Store, @@ -34,4 +43,18 @@ abstract class BaseStoreImpl : override fun init(scope: CoroutineScope) { _scope = scope } + + override fun destroy() { + _scope?.cancel() + _scope = null + } + + fun Flow>.state(): StateFlow> = this + .flowOn(Dispatchers.IO) + .cachedIn(scope) + .stateIn( + initialValue = PagingData.empty(), + scope = scope, + started = SharingStarted.Lazily + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/Store.kt b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/Store.kt index dd0a6e28..7f7612ae 100644 --- a/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/Store.kt +++ b/core/ui/src/main/java/st/slex/csplashscreen/core/ui/mvi/Store.kt @@ -1,11 +1,11 @@ package st.slex.csplashscreen.core.ui.mvi -import st.slex.csplashscreen.core.ui.mvi.Store.Action -import st.slex.csplashscreen.core.ui.mvi.Store.Event -import st.slex.csplashscreen.core.ui.mvi.Store.State import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import st.slex.csplashscreen.core.ui.mvi.Store.Action +import st.slex.csplashscreen.core.ui.mvi.Store.Event +import st.slex.csplashscreen.core.ui.mvi.Store.State interface Store { @@ -16,8 +16,9 @@ interface Store { fun init(scope: CoroutineScope) + fun destroy() + interface State interface Event interface Action } - diff --git a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/domain/SingleCollectionInteractorImpl.kt b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/domain/SingleCollectionInteractorImpl.kt index 07415580..1401a2f3 100644 --- a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/domain/SingleCollectionInteractorImpl.kt +++ b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/domain/SingleCollectionInteractorImpl.kt @@ -13,9 +13,11 @@ class SingleCollectionInteractorImpl @Inject constructor( uuid: String, page: Int, pageSize: Int - ): List = repository.getPhotos( - uuid = uuid, - page = page, - pageSize = pageSize - ).toDomain() + ): List = repository + .getPhotos( + uuid = uuid, + page = page, + pageSize = pageSize + ) + .toDomain() } \ No newline at end of file diff --git a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStore.kt b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStore.kt index 8ed8dc0c..7dd424bf 100644 --- a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStore.kt +++ b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStore.kt @@ -13,7 +13,8 @@ interface SingleCollectionStore : Store { @Stable data class State( - val photos: () -> StateFlow> + val photos: () -> StateFlow>, + val collectionId: String, ) : Store.State @Stable diff --git a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStoreImpl.kt b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStoreImpl.kt index 6d1ca523..eb74db26 100644 --- a/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStoreImpl.kt +++ b/feature/collection/src/main/java/st/slex/csplashscreen/feature/collection/ui/store/SingleCollectionStoreImpl.kt @@ -3,14 +3,15 @@ package st.slex.csplashscreen.feature.collection.ui.store import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.cachedIn import androidx.paging.map -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import st.slex.csplashscreen.core.core.CoroutineExt.mapState +import st.slex.csplashscreen.core.photos.ui.model.PhotoModel import st.slex.csplashscreen.core.photos.ui.model.toPresentation import st.slex.csplashscreen.core.ui.mvi.BaseStoreImpl import st.slex.csplashscreen.core.ui.paging.PagingSource @@ -24,10 +25,10 @@ class SingleCollectionStoreImpl @Inject constructor( private val interactor: SingleCollectionInteractor ) : SingleCollectionStore, BaseStoreImpl() { - override val initialState: State get() = State( - photos = { MutableStateFlow(PagingData.empty()) } + photos = ::allPhotos, + collectionId = "" ) override val state: MutableStateFlow = MutableStateFlow(initialState) @@ -43,31 +44,31 @@ class SingleCollectionStoreImpl @Inject constructor( private fun actionInit(action: Action.Init) { updateState { currentState -> currentState.copy( - photos = { - getPhotos(action.collectionId) - } + collectionId = action.collectionId ) } } - private fun getPhotos(collectionId: String) = Pager(pagingConfig) { - PagingSource { page, pageSize -> - interactor.getPhotos( - uuid = collectionId, - page = page, - pageSize = pageSize - ) - } - } - .flow - .map { pagingData -> pagingData.map { it.toPresentation() } } - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn( - initialValue = PagingData.empty(), - scope = scope, - started = SharingStarted.Lazily - ) + @OptIn(ExperimentalCoroutinesApi::class) + private val allPhotos: StateFlow> + get() = state + .mapState { currentState -> + currentState.collectionId + } + .filter { it.isNotBlank() } + .flatMapLatest { collectionId -> + Pager(pagingConfig) { + PagingSource { page, pageSize -> + interactor.getPhotos( + uuid = collectionId, + page = page, + pageSize = pageSize + ) + } + }.flow + } + .map { pagingData -> pagingData.map { it.toPresentation() } } + .state() private fun actionProfileClick(action: Action.OnProfileClick) { sendEvent(Event.Navigation.Profile(action.username)) diff --git a/feature/favourite/src/main/java/st/slex/csplashscreen/feature/favourite/ui/store/FavouriteStoreImpl.kt b/feature/favourite/src/main/java/st/slex/csplashscreen/feature/favourite/ui/store/FavouriteStoreImpl.kt index bc93034c..728d1f9e 100644 --- a/feature/favourite/src/main/java/st/slex/csplashscreen/feature/favourite/ui/store/FavouriteStoreImpl.kt +++ b/feature/favourite/src/main/java/st/slex/csplashscreen/feature/favourite/ui/store/FavouriteStoreImpl.kt @@ -1,14 +1,9 @@ package st.slex.csplashscreen.feature.favourite.ui.store import androidx.paging.PagingData -import androidx.paging.cachedIn -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import st.slex.csplashscreen.core.photos.ui.model.PhotoModel import st.slex.csplashscreen.core.ui.mvi.BaseStoreImpl import st.slex.csplashscreen.feature.favourite.domain.FavouriteInteractor @@ -32,13 +27,7 @@ class FavouriteStoreImpl @Inject constructor( .map { pagingData -> pagingData } - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn( - scope = scope, - started = SharingStarted.Lazily, - initialValue = PagingData.empty() - ) + .state() override fun processAction(action: Action) { when (action) { diff --git a/feature/home/src/main/java/st/slex/csplashscreen/feature/home/ui/store/HomeStoreImpl.kt b/feature/home/src/main/java/st/slex/csplashscreen/feature/home/ui/store/HomeStoreImpl.kt index b3b96504..10aab328 100644 --- a/feature/home/src/main/java/st/slex/csplashscreen/feature/home/ui/store/HomeStoreImpl.kt +++ b/feature/home/src/main/java/st/slex/csplashscreen/feature/home/ui/store/HomeStoreImpl.kt @@ -41,13 +41,7 @@ class HomeStoreImpl @Inject constructor( } .flow .map { pagingData -> pagingData.map { it.toPresentation() } } - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn( - scope = scope, - started = SharingStarted.Lazily, - initialValue = PagingData.empty() - ) + .state() private val photos: StateFlow> get() = Pager(config = config) { diff --git a/feature/search/src/main/java/st/slex/csplashscreen/feature/search/ui/store/SearchStoreImpl.kt b/feature/search/src/main/java/st/slex/csplashscreen/feature/search/ui/store/SearchStoreImpl.kt index dabaa8d3..94eaa670 100644 --- a/feature/search/src/main/java/st/slex/csplashscreen/feature/search/ui/store/SearchStoreImpl.kt +++ b/feature/search/src/main/java/st/slex/csplashscreen/feature/search/ui/store/SearchStoreImpl.kt @@ -3,18 +3,14 @@ package st.slex.csplashscreen.feature.search.ui.store import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.cachedIn import androidx.paging.map import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import st.slex.csplashscreen.core.core.Logger import st.slex.csplashscreen.core.network.model.ui.ImageModel @@ -42,10 +38,7 @@ class SearchStoreImpl @Inject constructor( override val state = MutableStateFlow(initialState) private val searchHistory: StateFlow> - get() = interactor.searchHistory - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn(scope, SharingStarted.Lazily, PagingData.empty()) + get() = interactor.searchHistory.state() @OptIn(ExperimentalCoroutinesApi::class) private val photosSearch: StateFlow> @@ -54,9 +47,7 @@ class SearchStoreImpl @Inject constructor( .map(::newPagerPhotosSearch) .flatMapLatest { it.flow } .map { pagingData -> pagingData.map { it.toPresentation() } } - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn(scope, SharingStarted.Lazily, PagingData.empty()) + .state() private fun newPagerPhotosSearch( query: String diff --git a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/UserScreen.kt b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/UserScreen.kt index d7966392..922b75d6 100644 --- a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/UserScreen.kt +++ b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/UserScreen.kt @@ -9,9 +9,14 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.swipeable import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged import kotlinx.coroutines.launch import st.slex.csplashscreen.core.ui.base.DimensionSubcomposeLayout import st.slex.csplashscreen.core.ui.utils.UiExt.toPx @@ -36,6 +41,7 @@ fun UserScreen( modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() + DimensionSubcomposeLayout( mainContent = { UserHeader( @@ -44,7 +50,10 @@ fun UserScreen( ) } ) { contentSize -> - val initialHeight = contentSize.height.toPx + val currentHeight = contentSize.height.toPx + var userHeaderHeight by remember { + mutableStateOf(currentHeight) + } Column( modifier = modifier @@ -60,14 +69,23 @@ fun UserScreen( state = userSwipeState.swipeableState, anchors = mapOf( 0f to SwipeState.COLLAPSE, - initialHeight to SwipeState.EXPAND + userHeaderHeight to SwipeState.EXPAND ), orientation = Orientation.Vertical, ) .nestedScroll(userSwipeState.swipeScrollConnection) ) { UserHeader( - modifier = Modifier, + modifier = Modifier + .onSizeChanged { currentSize -> + val floatSize = currentSize.height.toFloat() + if ( + userHeaderHeight != floatSize && + floatSize != 0f + ) { + userHeaderHeight = floatSize + } + }, user = state.user, onTabClick = { tab -> coroutineScope.launch { diff --git a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/components/header/UserBioComponent.kt b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/components/header/UserBioComponent.kt index b0565139..c2eeabe9 100644 --- a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/components/header/UserBioComponent.kt +++ b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/components/header/UserBioComponent.kt @@ -1,7 +1,6 @@ package st.slex.csplashscreen.feature.user.ui.components.header import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -24,10 +23,10 @@ private enum class UserBioState(val value: Int) { } } -@OptIn(ExperimentalAnimationApi::class) @Composable fun BindUserBio( - modifier: Modifier = Modifier, bioText: String + bioText: String, + modifier: Modifier = Modifier, ) { val userBioState = remember { mutableStateOf(UserBioState.COLLAPSE) diff --git a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/store/UserStoreImpl.kt b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/store/UserStoreImpl.kt index 4fae13d3..ab58580c 100644 --- a/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/store/UserStoreImpl.kt +++ b/feature/user/src/main/java/st/slex/csplashscreen/feature/user/ui/store/UserStoreImpl.kt @@ -3,16 +3,13 @@ package st.slex.csplashscreen.feature.user.ui.store import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import androidx.paging.cachedIn import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import st.slex.csplashscreen.core.collection.ui.model.CollectionModel import st.slex.csplashscreen.core.collection.ui.model.toPresentation import st.slex.csplashscreen.core.core.Logger @@ -76,11 +73,7 @@ class UserStoreImpl @Inject constructor( pageSize = pageSize ).map { it.toPresentation() } } - } - .flow - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn(scope, SharingStarted.Lazily, PagingData.empty()) + }.flow.state() private fun getLikes( username: String @@ -92,11 +85,7 @@ class UserStoreImpl @Inject constructor( pageSize = pageSize ).map { it.toPresentation() } } - } - .flow - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn(scope, SharingStarted.Lazily, PagingData.empty()) + }.flow.state() private fun getCollections( username: String @@ -108,11 +97,7 @@ class UserStoreImpl @Inject constructor( pageSize = pageSize ).map { it.toPresentation() } } - } - .flow - .flowOn(Dispatchers.IO) - .cachedIn(scope) - .stateIn(scope, SharingStarted.Lazily, PagingData.empty()) + }.flow.state() private fun actionBackClick() { sendEvent(Event.Navigation.PopBack) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c527a55..c7c0b6dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,9 +8,10 @@ material = "1.9.0" appcompat = "1.6.1" immutableCollection = "0.3.5" lifecycle = "2.6.2" +coroutines = "1.7.3" composeCompiler = "1.5.1" -composeBom = "2023.09.00" +composeBom = "2023.10.00" composeNavigation = "2.7.2" accompanist = "0.30.0" coilCompose = "2.4.0" @@ -44,7 +45,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a material = { group = "com.google.android.material", name = "material", version.ref = "material" } lifecycle-viewModel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } - +coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-activity = "androidx.activity:activity-compose:1.7.2"