From f3d2ecd7db046b473949472fd66eb4f1f8b70920 Mon Sep 17 00:00:00 2001 From: stslex Date: Tue, 28 Nov 2023 08:20:13 +0300 Subject: [PATCH] add profile scree, refactor navigation --- composeApp/build.gradle.kts | 1 + composeApp/src/commonMain/kotlin/App.kt | 4 +- .../src/commonMain/kotlin/InitialApp.kt | 5 +- .../kotlin/main_screen/MainScreen.kt | 3 + .../main_screen/bottom_nav_bar/ProfileTab.kt | 11 +-- .../kotlin/com/stslex/core/core/Logger.kt | 8 +- .../network/clients/film/client/FilmClient.kt | 3 +- .../clients/film/client/FilmClientImpl.kt | 15 +--- .../clients/film/client/MockFilmClientImpl.kt | 8 +- .../profile/client/MockProfileClientImpl.kt | 20 +++++ .../clients/profile/client/ProfileClient.kt | 8 ++ .../profile/client/ProfileClientImpl.kt | 20 +++++ .../clients/profile/model/ProfileResponse.kt | 22 ++++++ .../stslex/core/network/di/NetworkModule.kt | 3 + .../stslex/core/network/utils/KtorLogger.kt | 2 +- .../kotlin/com/stslex/core/ui/mvi/StoreExt.kt | 10 +-- .../com/stslex/feature/feed/ui/FeedScreen.kt | 4 +- .../data/repository/FilmRepositoryImpl.kt | 13 ++-- .../com/stslex/feature/film/ui/FilmScreen.kt | 7 +- feature/profile/build.gradle.kts | 50 ++++++++++++ .../profile/data/model/ProfileDataMapper.kt | 13 ++++ .../profile/data/model/ProfileDataModel.kt | 11 +++ .../data/repository/ProfileRepository.kt | 9 +++ .../data/repository/ProfileRepositoryImpl.kt | 17 ++++ .../feature/profile/di/ProfileModule.kt | 23 ++++++ .../domain/interactor/ProfileInteractor.kt | 9 +++ .../interactor/ProfileInteractorImpl.kt | 16 ++++ .../domain/model/ProfileDomainMapper.kt | 13 ++++ .../domain/model/ProfileDomainModel.kt | 11 +++ .../profile/navigation/ProfileRouter.kt | 6 ++ .../profile/navigation/ProfileRouterImpl.kt | 15 ++++ .../feature/profile/ui/ProfileScreen.kt | 77 +++++++++++++++++++ .../feature/profile/ui/model/ProfileModel.kt | 14 ++++ .../profile/ui/model/ProfileUiMapper.kt | 13 ++++ .../profile/ui/store/ProfileScreenState.kt | 17 ++++ .../feature/profile/ui/store/ProfileStore.kt | 39 ++++++++++ .../profile/ui/store/ProfileStoreComponent.kt | 32 ++++++++ settings.gradle.kts | 3 +- 38 files changed, 497 insertions(+), 58 deletions(-) create mode 100644 core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt create mode 100644 core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt create mode 100644 core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt create mode 100644 core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/ProfileResponse.kt create mode 100644 feature/profile/build.gradle.kts create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataMapper.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataModel.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepository.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepositoryImpl.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractor.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractorImpl.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainMapper.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainModel.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileModel.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileUiMapper.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileScreenState.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt create mode 100644 feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 789fbb93..561f48de 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -41,6 +41,7 @@ kotlin { implementation(project(":core:ui")) implementation(project(":feature:feed")) implementation(project(":feature:film")) + implementation(project(":feature:profile")) } } } diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt index de58aeee..7322e18e 100644 --- a/composeApp/src/commonMain/kotlin/App.kt +++ b/composeApp/src/commonMain/kotlin/App.kt @@ -4,6 +4,7 @@ import com.stslex.core.network.di.networkModule import com.stslex.core.ui.theme.AppTheme import com.stslex.feature.feed.di.feedModule import com.stslex.feature.film.di.filmModule +import com.stslex.feature.profile.di.profileModule import di.appModule import org.koin.compose.KoinApplication import org.koin.dsl.KoinAppDeclaration @@ -34,7 +35,8 @@ private fun setupModules(): KoinAppDeclaration = { coreModule, networkModule, feedModule, - filmModule + filmModule, + profileModule, ) ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/InitialApp.kt b/composeApp/src/commonMain/kotlin/InitialApp.kt index 5483ce59..685d3430 100644 --- a/composeApp/src/commonMain/kotlin/InitialApp.kt +++ b/composeApp/src/commonMain/kotlin/InitialApp.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.SlideTransition import main_screen.MainScreen @Composable @@ -22,7 +23,9 @@ fun InitialApp( modifier = Modifier.fillMaxSize() .padding(paddingValues) ) { - Navigator(MainScreen) + Navigator(MainScreen) { + SlideTransition(it) + } } } } diff --git a/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt b/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt index 98661b76..a9a52d6f 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.tab.CurrentTab import cafe.adriel.voyager.navigator.tab.TabNavigator +import com.stslex.core.ui.mvi.setupNavigator import main_screen.bottom_nav_bar.BottomNavigationBar import main_screen.bottom_nav_bar.FeedTab @@ -16,6 +17,8 @@ object MainScreen : Screen { @Composable override fun Content() { + setupNavigator() + TabNavigator( tab = FeedTab, ) { tabNavigator -> diff --git a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/ProfileTab.kt b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/ProfileTab.kt index 69fb2203..330a1483 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/ProfileTab.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/bottom_nav_bar/ProfileTab.kt @@ -2,14 +2,13 @@ package main_screen.bottom_nav_bar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.rememberVectorPainter -import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.stslex.feature.profile.ui.ProfileScreen object ProfileTab : Tab { @@ -33,11 +32,3 @@ object ProfileTab : Tab { Navigator(ProfileScreen) } } - -object ProfileScreen : Screen { - - @Composable - override fun Content() { - Text("Profile") - } -} \ No newline at end of file diff --git a/core/core/src/commonMain/kotlin/com/stslex/core/core/Logger.kt b/core/core/src/commonMain/kotlin/com/stslex/core/core/Logger.kt index ccbdca88..2c7aab5b 100644 --- a/core/core/src/commonMain/kotlin/com/stslex/core/core/Logger.kt +++ b/core/core/src/commonMain/kotlin/com/stslex/core/core/Logger.kt @@ -4,7 +4,7 @@ import co.touchlab.kermit.Logger as Log object Logger { - const val DEFAULT_TAG = "WIZARD" + private const val DEFAULT_TAG = "WIZARD" fun exception( throwable: Throwable, @@ -12,9 +12,8 @@ object Logger { message: String? = null ) { // TODO check build config if (BuildConfig.DEBUG.not()) return - val currentTag = "$DEFAULT_TAG:${tag.orEmpty()}" Log.e( - tag = currentTag, + tag = tag ?: DEFAULT_TAG, throwable = throwable, messageString = message ?: throwable.message.orEmpty(), ) @@ -25,9 +24,8 @@ object Logger { tag: String? = null, ) { // TODO check build config if (BuildConfig.DEBUG.not()) return - val currentTag = "$DEFAULT_TAG:${tag.orEmpty()}" Log.d( - tag = currentTag, + tag = tag ?: DEFAULT_TAG, messageString = message ) } diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt index 96e4bb18..a9f74c8e 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClient.kt @@ -2,11 +2,10 @@ package com.stslex.core.network.clients.film.client import com.stslex.core.network.clients.film.model.FilmFeedResponse import com.stslex.core.network.clients.film.model.FilmResponse -import kotlinx.coroutines.flow.Flow interface FilmClient { suspend fun getFeedFilms(page: Int, pageSize: Int): FilmFeedResponse - fun getFilm(id: String): Flow + suspend fun getFilm(id: String): FilmResponse } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt index e90682b5..f8ed1335 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/FilmClientImpl.kt @@ -6,8 +6,6 @@ import com.stslex.core.network.clients.film.model.FilmResponse import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow class FilmClientImpl( private val client: NetworkClient @@ -22,14 +20,9 @@ class FilmClientImpl( }.body() } - override fun getFilm( - id: String - ): Flow = flow { - val film: FilmResponse = client.request { - get("feed") { - parameter("id", id) - }.body() - } - emit(film) + override suspend fun getFilm(id: String): FilmResponse = client.request { + get("feed") { + parameter("id", id) + }.body() } } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt index 562b09b0..6a8061fc 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/film/client/MockFilmClientImpl.kt @@ -4,8 +4,6 @@ import com.stslex.core.core.Logger import com.stslex.core.network.clients.film.model.FilmFeedResponse import com.stslex.core.network.clients.film.model.FilmResponse import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow class MockFilmClientImpl : FilmClient { @@ -24,11 +22,7 @@ class MockFilmClientImpl : FilmClient { ) } - override fun getFilm( - id: String - ): Flow = flow { - emit(getFilmById(id.toInt())) - } + override suspend fun getFilm(id: String): FilmResponse = getFilmById(id.toInt()) private fun getFilmById(id: Int) = filmsList[id % filmsList.size].copy( id = id.toString() diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt new file mode 100644 index 00000000..aabce4fe --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/MockProfileClientImpl.kt @@ -0,0 +1,20 @@ +package com.stslex.core.network.clients.profile.client + +import com.stslex.core.network.clients.profile.model.ProfileResponse +import kotlinx.coroutines.delay + +class MockProfileClientImpl : ProfileClient { + + override suspend fun getProfile(uuid: String): ProfileResponse { + delay(2000) + return ProfileResponse( + uuid = "uuid", + username = "John Doe", + avatarUrl = "https://avatars.githubusercontent.com/u/139426?s=460&u=8f6b6e2e4e9e4b0e9b5b5e4e9b5b5e4e9b5b5e4e&v=4", + bio = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec auctor, nisl eu ultricies tincidunt, nisl nisl aliquam nisl,", + followers = 100, + following = 93, + favouriteCount = 873 + ) + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt new file mode 100644 index 00000000..516511c2 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClient.kt @@ -0,0 +1,8 @@ +package com.stslex.core.network.clients.profile.client + +import com.stslex.core.network.clients.profile.model.ProfileResponse + +interface ProfileClient { + + suspend fun getProfile(uuid: String): ProfileResponse +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt new file mode 100644 index 00000000..53d28cd8 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/client/ProfileClientImpl.kt @@ -0,0 +1,20 @@ +package com.stslex.core.network.clients.profile.client + +import com.stslex.core.network.client.NetworkClient +import com.stslex.core.network.clients.profile.model.ProfileResponse +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter + +class ProfileClientImpl( + private val client: NetworkClient +) : ProfileClient { + + override suspend fun getProfile( + uuid: String + ): ProfileResponse = client.request { + get("profile") { + parameter("uuid", uuid) + }.body() + } +} \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/ProfileResponse.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/ProfileResponse.kt new file mode 100644 index 00000000..94b2d3c1 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/clients/profile/model/ProfileResponse.kt @@ -0,0 +1,22 @@ +package com.stslex.core.network.clients.profile.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ProfileResponse( + @SerialName("uuid") + val uuid: String, + @SerialName("username") + val username: String, + @SerialName("avatar_url") + val avatarUrl: String, + @SerialName("bio") + val bio: String, + @SerialName("followers") + val followers: Int, + @SerialName("following") + val following: Int, + @SerialName("favourite_count") + val favouriteCount: Int +) diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt index c1c8c503..49712885 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/di/NetworkModule.kt @@ -4,6 +4,8 @@ import com.stslex.core.network.client.NetworkClient import com.stslex.core.network.client.NetworkClientImpl import com.stslex.core.network.clients.film.client.FilmClient import com.stslex.core.network.clients.film.client.MockFilmClientImpl +import com.stslex.core.network.clients.profile.client.MockProfileClientImpl +import com.stslex.core.network.clients.profile.client.ProfileClient import org.koin.dsl.module val networkModule = module { @@ -11,4 +13,5 @@ val networkModule = module { NetworkClientImpl(appDispatcher = get()) } single { MockFilmClientImpl() } + single { MockProfileClientImpl() } } \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/KtorLogger.kt b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/KtorLogger.kt index 4c58ed40..65995ca6 100644 --- a/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/KtorLogger.kt +++ b/core/network/src/commonMain/kotlin/com/stslex/core/network/utils/KtorLogger.kt @@ -5,7 +5,7 @@ import com.stslex.core.core.Logger as Log object KtorLogger : Logger { - private const val TAG = Log.DEFAULT_TAG + ":KtorLogger" + private const val TAG = "KTOR_LOGGER" override fun log(message: String) { Log.debug(message, TAG) diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt index 9fde0f91..39b30e41 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/mvi/StoreExt.kt @@ -1,21 +1,17 @@ package com.stslex.core.ui.mvi import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.koin.getScreenModel +import androidx.compose.runtime.LaunchedEffect import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.stslex.core.ui.navigation.AppNavigator import org.koin.compose.getKoin @Composable -inline fun Screen.getScreenStore(): S { +fun setupNavigator() { val navigator = LocalNavigator.currentOrThrow val koin = getKoin() - remember(navigator) { + LaunchedEffect(navigator.hashCode()) { koin.get().setNavigator(navigator) } - return getScreenModel() } \ No newline at end of file diff --git a/feature/feed/src/commonMain/kotlin/com/stslex/feature/feed/ui/FeedScreen.kt b/feature/feed/src/commonMain/kotlin/com/stslex/feature/feed/ui/FeedScreen.kt index 7d967668..4e63b700 100644 --- a/feature/feed/src/commonMain/kotlin/com/stslex/feature/feed/ui/FeedScreen.kt +++ b/feature/feed/src/commonMain/kotlin/com/stslex/feature/feed/ui/FeedScreen.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getScreenStore +import cafe.adriel.voyager.koin.getScreenModel import com.stslex.feature.feed.ui.components.FeedScreenContent import com.stslex.feature.feed.ui.components.FeedScreenError import com.stslex.feature.feed.ui.components.FeedScreenLoading @@ -23,7 +23,7 @@ object FeedScreen : Screen { @Composable override fun Content() { - val store = getScreenStore() + val store = getScreenModel() val state by remember { store.state }.collectAsState() LaunchedEffect(Unit) { store.event.collect { event -> diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt index 1ab464d8..0446ddea 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/data/repository/FilmRepositoryImpl.kt @@ -4,17 +4,14 @@ import com.stslex.core.network.clients.film.client.FilmClient import com.stslex.feature.film.data.model.FilmData import com.stslex.feature.film.data.model.toData import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flow class FilmRepositoryImpl( private val client: FilmClient ) : FilmRepository { - override fun getFilm( - id: String - ): Flow = client - .getFilm(id = id) - .map { response -> - response.toData() - } + override fun getFilm(id: String): Flow = flow { + val film = client.getFilm(id = id).toData() + emit(film) + } } \ No newline at end of file diff --git a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt index 415658af..5dd3fca6 100644 --- a/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt +++ b/feature/film/src/commonMain/kotlin/com/stslex/feature/film/ui/FilmScreen.kt @@ -13,7 +13,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen -import com.stslex.core.ui.mvi.getScreenStore +import cafe.adriel.voyager.koin.getScreenModel +import com.stslex.core.ui.mvi.setupNavigator import com.stslex.feature.film.ui.components.FilmContentScreen import com.stslex.feature.film.ui.store.FilmScreenState import com.stslex.feature.film.ui.store.FilmStore @@ -26,7 +27,9 @@ data class FilmScreen( @Composable override fun Content() { - val store = getScreenStore() + setupNavigator() + + val store = getScreenModel() LaunchedEffect(Unit) { store.sendAction(Action.Init(id)) } diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts new file mode 100644 index 00000000..f13c244f --- /dev/null +++ b/feature/profile/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsCompose) +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "film" + isStatic = true + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui")) + implementation(project(":core:network")) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + } +} + +android { + namespace = "com.stslex.feature.film" + compileSdk = libs.versions.android.compileSdk.get().toInt() + defaultConfig { + minSdk = libs.versions.android.minSdk.get().toInt() + } + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].res.srcDirs("src/androidMain/res") + sourceSets["main"].resources.srcDirs("src/commonMain/resources") +} diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataMapper.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataMapper.kt new file mode 100644 index 00000000..afbd1625 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataMapper.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.profile.data.model + +import com.stslex.core.network.clients.profile.model.ProfileResponse + +fun ProfileResponse.toData() = ProfileDataModel( + uuid = uuid, + username = username, + avatarUrl = avatarUrl, + bio = bio, + followers = followers, + following = following, + favouriteCount = favouriteCount, +) \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataModel.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataModel.kt new file mode 100644 index 00000000..54ae6c57 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/model/ProfileDataModel.kt @@ -0,0 +1,11 @@ +package com.stslex.feature.profile.data.model + +data class ProfileDataModel( + val uuid: String, + val username: String, + val avatarUrl: String, + val bio: String, + val followers: Int, + val following: Int, + val favouriteCount: Int, +) diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepository.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepository.kt new file mode 100644 index 00000000..ae4970d8 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepository.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.profile.data.repository + +import com.stslex.feature.profile.data.model.ProfileDataModel +import kotlinx.coroutines.flow.Flow + +interface ProfileRepository { + + fun getProfile(uuid: String): Flow +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepositoryImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepositoryImpl.kt new file mode 100644 index 00000000..30f1be19 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/data/repository/ProfileRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.stslex.feature.profile.data.repository + +import com.stslex.core.network.clients.profile.client.ProfileClient +import com.stslex.feature.profile.data.model.ProfileDataModel +import com.stslex.feature.profile.data.model.toData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class ProfileRepositoryImpl( + private val client: ProfileClient +) : ProfileRepository { + + override fun getProfile(uuid: String): Flow = flow { + val profile = client.getProfile(uuid).toData() + emit(profile) + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt new file mode 100644 index 00000000..b3a7c6a0 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/di/ProfileModule.kt @@ -0,0 +1,23 @@ +package com.stslex.feature.profile.di + +import com.stslex.feature.profile.data.repository.ProfileRepository +import com.stslex.feature.profile.data.repository.ProfileRepositoryImpl +import com.stslex.feature.profile.domain.interactor.ProfileInteractor +import com.stslex.feature.profile.domain.interactor.ProfileInteractorImpl +import com.stslex.feature.profile.navigation.ProfileRouter +import com.stslex.feature.profile.navigation.ProfileRouterImpl +import com.stslex.feature.profile.ui.store.ProfileStore +import org.koin.dsl.module + +val profileModule = module { + factory { + ProfileStore( + interactor = get(), + appDispatcher = get(), + router = get(), + ) + } + factory { ProfileRouterImpl(navigator = get()) } + factory { ProfileRepositoryImpl(client = get()) } + factory { ProfileInteractorImpl(repository = get()) } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractor.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractor.kt new file mode 100644 index 00000000..b213d374 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractor.kt @@ -0,0 +1,9 @@ +package com.stslex.feature.profile.domain.interactor + +import com.stslex.feature.profile.domain.model.ProfileDomainModel +import kotlinx.coroutines.flow.Flow + +interface ProfileInteractor { + + fun getProfile(uuid: String): Flow +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractorImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractorImpl.kt new file mode 100644 index 00000000..316108db --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/interactor/ProfileInteractorImpl.kt @@ -0,0 +1,16 @@ +package com.stslex.feature.profile.domain.interactor + +import com.stslex.feature.profile.data.repository.ProfileRepository +import com.stslex.feature.profile.domain.model.ProfileDomainModel +import com.stslex.feature.profile.domain.model.toDomain +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ProfileInteractorImpl( + private val repository: ProfileRepository +) : ProfileInteractor { + + override fun getProfile( + uuid: String + ): Flow = repository.getProfile(uuid).map { it.toDomain() } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainMapper.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainMapper.kt new file mode 100644 index 00000000..e5766b5c --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainMapper.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.profile.domain.model + +import com.stslex.feature.profile.data.model.ProfileDataModel + +fun ProfileDataModel.toDomain() = ProfileDomainModel( + uuid = uuid, + username = username, + avatarUrl = avatarUrl, + bio = bio, + followers = followers, + following = following, + favouriteCount = favouriteCount, +) \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainModel.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainModel.kt new file mode 100644 index 00000000..3094b710 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/domain/model/ProfileDomainModel.kt @@ -0,0 +1,11 @@ +package com.stslex.feature.profile.domain.model + +data class ProfileDomainModel( + val uuid: String, + val username: String, + val avatarUrl: String, + val bio: String, + val followers: Int, + val following: Int, + val favouriteCount: Int, +) diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt new file mode 100644 index 00000000..54435770 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouter.kt @@ -0,0 +1,6 @@ +package com.stslex.feature.profile.navigation + +import com.stslex.core.ui.navigation.Router +import com.stslex.feature.profile.ui.store.ProfileStoreComponent + +interface ProfileRouter : Router \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt new file mode 100644 index 00000000..9c760dee --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/navigation/ProfileRouterImpl.kt @@ -0,0 +1,15 @@ +package com.stslex.feature.profile.navigation + +import com.stslex.core.ui.navigation.AppNavigator +import com.stslex.feature.profile.ui.store.ProfileStoreComponent + +class ProfileRouterImpl( + private val navigator: AppNavigator +) : ProfileRouter { + + override fun invoke( + event: ProfileStoreComponent.Navigation + ) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt new file mode 100644 index 00000000..db03163f --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/ProfileScreen.kt @@ -0,0 +1,77 @@ +package com.stslex.feature.profile.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.stslex.feature.profile.ui.model.ProfileModel +import com.stslex.feature.profile.ui.store.ProfileScreenState +import com.stslex.feature.profile.ui.store.ProfileStore +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.State + +object ProfileScreen : Screen { + + @Composable + override fun Content() { + val store = getScreenModel() + LaunchedEffect(Unit) { + store.sendAction(Action.LoadProfile("uuid")) + } + val state by remember { store.state }.collectAsState() + ProfileScreenContent(state) + } +} + +@Composable +private fun ProfileScreenContent(state: State) { + when (val screen = state.screen) { + is ProfileScreenState.Content -> ProfileScreenContent(screen.model) + is ProfileScreenState.Error -> ProfileScreenError(screen.error) + ProfileScreenState.Loading -> ProfileScreenLoading() + } +} + +@Composable +internal fun ProfileScreenLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Loading") + CircularProgressIndicator() + } +} + +@Composable +internal fun ProfileScreenError( + error: Throwable? = null, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(text = "Error: ${error?.message}") + } +} + +@Composable +internal fun ProfileScreenContent( + profile: ProfileModel, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = profile.username + ) +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileModel.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileModel.kt new file mode 100644 index 00000000..cbc334e2 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileModel.kt @@ -0,0 +1,14 @@ +package com.stslex.feature.profile.ui.model + +import androidx.compose.runtime.Stable + +@Stable +data class ProfileModel( + val uuid: String, + val username: String, + val avatarUrl: String, + val bio: String, + val followers: Int, + val following: Int, + val favouriteCount: Int, +) diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileUiMapper.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileUiMapper.kt new file mode 100644 index 00000000..db14ba75 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/model/ProfileUiMapper.kt @@ -0,0 +1,13 @@ +package com.stslex.feature.profile.ui.model + +import com.stslex.feature.profile.domain.model.ProfileDomainModel + +fun ProfileDomainModel.toUi() = ProfileModel( + uuid = uuid, + username = username, + avatarUrl = avatarUrl, + bio = bio, + followers = followers, + following = following, + favouriteCount = favouriteCount, +) \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileScreenState.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileScreenState.kt new file mode 100644 index 00000000..4d9b7584 --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileScreenState.kt @@ -0,0 +1,17 @@ +package com.stslex.feature.profile.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.feature.profile.ui.model.ProfileModel + +@Stable +sealed interface ProfileScreenState { + + @Stable + data class Content(val model: ProfileModel) : ProfileScreenState + + @Stable + data object Loading : ProfileScreenState + + @Stable + data class Error(val error: Throwable) : ProfileScreenState +} \ No newline at end of file diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt new file mode 100644 index 00000000..90e8fe2e --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStore.kt @@ -0,0 +1,39 @@ +package com.stslex.feature.profile.ui.store + +import com.stslex.core.core.AppDispatcher +import com.stslex.core.ui.mvi.BaseStore +import com.stslex.feature.profile.domain.interactor.ProfileInteractor +import com.stslex.feature.profile.navigation.ProfileRouter +import com.stslex.feature.profile.ui.model.toUi +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Action +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Event +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.Navigation +import com.stslex.feature.profile.ui.store.ProfileStoreComponent.State + +class ProfileStore( + private val interactor: ProfileInteractor, + router: ProfileRouter, + appDispatcher: AppDispatcher, +) : BaseStore( + router = router, + appDispatcher = appDispatcher, + initialState = State.INITIAL, +) { + + override fun sendAction(action: Action) { + when (action) { + is Action.LoadProfile -> actionLoadProfile(action) + } + } + + private fun actionLoadProfile(action: Action.LoadProfile) { + interactor.getProfile(action.uuid) + .launchFlow { profile -> + updateState { currentState -> + currentState.copy( + screen = ProfileScreenState.Content(profile.toUi()) + ) + } + } + } +} diff --git a/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt new file mode 100644 index 00000000..0244737a --- /dev/null +++ b/feature/profile/src/commonMain/kotlin/com/stslex/feature/profile/ui/store/ProfileStoreComponent.kt @@ -0,0 +1,32 @@ +package com.stslex.feature.profile.ui.store + +import androidx.compose.runtime.Stable +import com.stslex.core.ui.mvi.Store + +interface ProfileStoreComponent : Store { + + @Stable + data class State( + val screen: ProfileScreenState + ) : Store.State { + + companion object { + + val INITIAL = State(screen = ProfileScreenState.Loading) + } + } + + @Stable + sealed interface Action : Store.Action { + + @Stable + data class LoadProfile( + val uuid: String + ) : Action + } + + @Stable + sealed interface Event : Store.Event + + sealed interface Navigation : Store.Navigation +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 619af236..2a84bab4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,4 +23,5 @@ include(":core:core") include(":core:network") include(":core:ui") include(":feature:feed") -include(":feature:film") \ No newline at end of file +include(":feature:film") +include(":feature:profile") \ No newline at end of file