├── demo.gif
├── demo-ios.gif
├── iosApp
├── iosApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Components
│ │ ├── LoadingView.swift
│ │ ├── ViewModelWrapper.swift
│ │ ├── ErrorView.swift
│ │ └── EmptyView.swift
│ ├── Resources
│ │ └── Localizable.strings
│ ├── System
│ │ └── IOSLocalProperties.swift
│ ├── Features
│ │ ├── Home
│ │ │ └── HomeView.swift
│ │ ├── Users
│ │ │ └── List
│ │ │ │ ├── UserListView.swift
│ │ │ │ ├── UserListModel.swift
│ │ │ │ └── UserItemView.swift
│ │ └── Posts
│ │ │ ├── Detail
│ │ │ ├── PostDetailModel.swift
│ │ │ ├── CommentItemView.swift
│ │ │ └── PostDetailView.swift
│ │ │ └── List
│ │ │ ├── PostListModel.swift
│ │ │ ├── PostListView.swift
│ │ │ └── PostItemView.swift
│ ├── iOSApp.swift
│ └── Info.plist
└── iosApp.xcodeproj
│ ├── project.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── shared
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── eduayuso
│ │ │ └── kmcs
│ │ │ ├── presentation
│ │ │ ├── UIEffect.kt
│ │ │ ├── UIEvent.kt
│ │ │ ├── UIState.kt
│ │ │ └── ViewModel.kt
│ │ │ ├── domain
│ │ │ ├── entities
│ │ │ │ ├── IEntity.kt
│ │ │ │ ├── TagEntity.kt
│ │ │ │ ├── UserEntity.kt
│ │ │ │ ├── CommentEntity.kt
│ │ │ │ └── PostEntity.kt
│ │ │ ├── interactors
│ │ │ │ ├── type
│ │ │ │ │ ├── Resource.kt
│ │ │ │ │ ├── UseCase.kt
│ │ │ │ │ ├── UseCaseOut.kt
│ │ │ │ │ ├── UseCaseIn.kt
│ │ │ │ │ └── UseCaseInOut.kt
│ │ │ │ ├── impl
│ │ │ │ │ ├── GetPostListInteractor.kt
│ │ │ │ │ ├── GetUserListInteractor.kt
│ │ │ │ │ ├── GetPostDetailInteractor.kt
│ │ │ │ │ └── GetPostCommentsInteractor.kt
│ │ │ │ └── UseCases.kt
│ │ │ └── repository
│ │ │ │ ├── IUsersRepository.kt
│ │ │ │ └── IPostsRepository.kt
│ │ │ ├── di
│ │ │ ├── Module.kt
│ │ │ ├── KoinViewModels.kt
│ │ │ └── Koin.kt
│ │ │ ├── executor
│ │ │ ├── IExecutorScope.kt
│ │ │ ├── MainDispatcher.kt
│ │ │ └── MainIOExecutor.kt
│ │ │ ├── data
│ │ │ ├── LocalProperties.kt
│ │ │ ├── model
│ │ │ │ ├── LoginDto.kt
│ │ │ │ ├── TagResponse.kt
│ │ │ │ ├── LocationResponse.kt
│ │ │ │ ├── UserResponse.kt
│ │ │ │ ├── CommentResponse.kt
│ │ │ │ └── PostReponse.kt
│ │ │ ├── source
│ │ │ │ └── remote
│ │ │ │ │ ├── UsersRemoteDataSource.kt
│ │ │ │ │ ├── PostsRemoteDataSource.kt
│ │ │ │ │ └── ApiClient.kt
│ │ │ ├── repository
│ │ │ │ ├── UsersRepository.kt
│ │ │ │ └── PostsRepository.kt
│ │ │ └── DataMapper.kt
│ │ │ ├── util
│ │ │ └── Validator.kt
│ │ │ ├── features
│ │ │ ├── users
│ │ │ │ └── list
│ │ │ │ │ ├── UserListContract.kt
│ │ │ │ │ └── UserListViewModel.kt
│ │ │ └── posts
│ │ │ │ ├── list
│ │ │ │ ├── PostListContract.kt
│ │ │ │ └── PostListViewModel.kt
│ │ │ │ └── detail
│ │ │ │ ├── PostDetailContract.kt
│ │ │ │ └── PostDetailViewModel.kt
│ │ │ └── AppConstants.kt
│ ├── androidMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── eduayuso
│ │ │ └── kmcs
│ │ │ ├── executor
│ │ │ └── MainDispatcher.kt
│ │ │ └── di
│ │ │ └── Module.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── eduayuso
│ │ │ └── kmcs
│ │ │ ├── di
│ │ │ └── Module.kt
│ │ │ └── executor
│ │ │ └── MainDispatcher.kt
│ └── commonTest
│ │ └── kotlin
│ │ └── dev
│ │ └── eduayuso
│ │ └── kmcs
│ │ ├── IntegrationTestCase.kt
│ │ ├── data
│ │ ├── repository
│ │ │ ├── UsersRepositoryTest.kt
│ │ │ └── PostsRepositoryTest.kt
│ │ ├── MockedResponses.kt
│ │ └── HttpMock.kt
│ │ ├── di
│ │ └── KoinTest.kt
│ │ ├── UnitTestCase.kt
│ │ └── features
│ │ └── posts
│ │ └── PostListViewModelTest.kt
└── build.gradle.kts
├── androidApp
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── styles.xml
│ │ │ └── strings.xml
│ │ └── drawable
│ │ │ ├── ic_baseline_tag_24.xml
│ │ │ ├── ic_baseline_image_24.xml
│ │ │ ├── ic_baseline_message_24.xml
│ │ │ ├── ic_baseline_thumb_up_24.xml
│ │ │ ├── ic_baseline_group_24.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── dev
│ │ │ └── eduayuso
│ │ │ └── kmcs
│ │ │ └── android
│ │ │ ├── system
│ │ │ └── AndroidLocalProperties.kt
│ │ │ ├── navigation
│ │ │ ├── Routes.kt
│ │ │ ├── NavigationBar.kt
│ │ │ └── NavigationGraph.kt
│ │ │ ├── components
│ │ │ ├── LoadingView.kt
│ │ │ ├── ErrorView.kt
│ │ │ ├── EmptyView.kt
│ │ │ └── TopBarView.kt
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── features
│ │ │ ├── users
│ │ │ └── list
│ │ │ │ ├── UserItemView.kt
│ │ │ │ └── UserListView.kt
│ │ │ ├── home
│ │ │ └── HomeView.kt
│ │ │ └── posts
│ │ │ ├── list
│ │ │ ├── PostItemView.kt
│ │ │ └── PostListView.kt
│ │ │ └── detail
│ │ │ └── PostDetailView.kt
│ │ └── AndroidManifest.xml
└── build.gradle.kts
├── .gitignore
├── gradle.properties
├── settings.gradle.kts
├── README.md
├── gradlew.bat
└── gradlew
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eduayuso/kmm-mvi-compose-swiftui/HEAD/demo.gif
--------------------------------------------------------------------------------
/demo-ios.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eduayuso/kmm-mvi-compose-swiftui/HEAD/demo-ios.gif
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eduayuso/kmm-mvi-compose-swiftui/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/presentation/UIEffect.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.presentation
2 |
3 | interface UIEffect
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/presentation/UIEvent.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.presentation
2 |
3 | interface UIEvent
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/presentation/UIState.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.presentation
2 |
3 | interface UIState
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/entities/IEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.entities
2 |
3 | interface IEntity
--------------------------------------------------------------------------------
/androidApp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | .DS_Store
5 | build
6 | captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | xcuserdata
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/di/Module.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect fun platformModule(): Module
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/executor/IExecutorScope.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.executor
2 |
3 | interface IExecutorScope {
4 |
5 | fun cancel()
6 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/LocalProperties.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data
2 |
3 | interface LocalProperties {
4 |
5 | val dummyApiKey: String
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Components/LoadingView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LoadingView: View {
4 |
5 | var body: some View {
6 |
7 | ProgressView().scaleEffect(4.0, anchor: .center)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Resources/Localizable.strings:
--------------------------------------------------------------------------------
1 | "login" = "Login";
2 | "username" = "Username";
3 | "password" = "Password";
4 | "sign_up_button" = "Sign up";
5 | "forgot_pass" = "Forgot password?";
6 | "login_error" = "Incorrect credentials";
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/executor/MainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.executor
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 |
5 | expect class MainDispatcher() {
6 |
7 | val dispatcher: CoroutineDispatcher
8 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/entities/TagEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.entities
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class TagEntity(
7 |
8 | var id: String? = null
9 |
10 | ): IEntity
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/LoginDto.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LoginDto(
7 |
8 | val username: String,
9 | val password: String
10 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 19 11:12:54 CET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/type/Resource.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.type
2 |
3 | sealed class Resource {
4 |
5 | class Success(val data: T) : Resource()
6 | class Error(val exception: Exception) : Resource()
7 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
3 |
4 | #Kotlin
5 | kotlin.code.style=official
6 |
7 | #Android
8 | android.useAndroidX=true
9 | android.nonTransitiveRClass=true
10 |
11 | #MPP
12 | kotlin.mpp.enableCInteropCommonization=true
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/dev/eduayuso/kmcs/executor/MainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.executor
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 |
6 | actual class MainDispatcher {
7 |
8 | actual val dispatcher: CoroutineDispatcher = Dispatchers.Main
9 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/dev/eduayuso/kmcs/di/Module.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import dev.eduayuso.kmcs.executor.MainDispatcher
4 | import org.koin.core.module.Module
5 | import org.koin.dsl.module
6 |
7 | actual fun platformModule(): Module = module {
8 |
9 | single { MainDispatcher() }
10 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/dev/eduayuso/kmcs/di/Module.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import dev.eduayuso.kmcs.executor.MainDispatcher
4 | import org.koin.core.module.Module
5 | import org.koin.dsl.module
6 |
7 | actual fun platformModule(): Module = module {
8 |
9 | single { MainDispatcher() }
10 | }
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/TagResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class TagListResponse(
7 |
8 | val data: List? = null,
9 | val total: Int? = null,
10 | val page: Int? = null
11 | )
--------------------------------------------------------------------------------
/iosApp/iosApp/System/IOSLocalProperties.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Shared
3 |
4 | class IOSLocalProperties: LocalProperties {
5 |
6 | let dummyApiKey: String
7 |
8 | init() {
9 |
10 | self.dummyApiKey = Bundle.main.object(forInfoDictionaryKey: "DummyApiKey") as! String
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/repository/IUsersRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.repository
2 |
3 | import dev.eduayuso.kmcs.domain.entities.UserEntity
4 |
5 | interface IUsersRepository {
6 |
7 | suspend fun getAll(): List
8 |
9 | suspend fun getById(id: Int): UserEntity?
10 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/util/Validator.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.util
2 |
3 | private const val EMAIL_REGEX = "^[A-Za-z](.*)([@])(.+)(\\.)(.+)"
4 | private const val PASSWORD_MIN_LENGTH = 6
5 |
6 | fun String.isValidEmail() = EMAIL_REGEX.toRegex().matches(this)
7 |
8 | fun String.isValidPassword() = this.length >= PASSWORD_MIN_LENGTH
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/system/AndroidLocalProperties.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.system
2 |
3 | import dev.eduayuso.kmcs.android.BuildConfig
4 | import dev.eduayuso.kmcs.data.LocalProperties
5 |
6 | class AndroidLocalProperties: LocalProperties {
7 |
8 | override val dummyApiKey: String
9 | get() = BuildConfig.DUMMY_API_KEY
10 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/entities/UserEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.entities
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class UserEntity(
7 |
8 | var id: String,
9 | var firstName: String? = null,
10 | var lastName: String? = null,
11 | var picture: String? = null
12 |
13 | ): IEntity
--------------------------------------------------------------------------------
/androidApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Login
3 | Username
4 | Password
5 | Sign up
6 | Forgot password?
7 | Incorrect credentials
8 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/entities/CommentEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.entities
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | class CommentEntity(
7 |
8 | val id: String? = null,
9 | val owner: UserEntity? = null,
10 | val message: String? = null,
11 | val publishDate: String? = null
12 |
13 | ): IEntity
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/LocationResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | class LocationResponse(
7 |
8 | val state: String? = null,
9 | val street: String? = null,
10 | val city: String? = null,
11 | val timezone: String? = null,
12 | val country: String? = null
13 | )
--------------------------------------------------------------------------------
/iosApp/iosApp/Components/ViewModelWrapper.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | class ViewModelWrapper: ObservableObject {
5 |
6 | var viewModel: ViewModelType
7 |
8 | init(viewModel: ViewModelType) {
9 |
10 | self.viewModel = viewModel
11 | }
12 |
13 | func set(event: Any?) {
14 |
15 | viewModel.setEvent(event: event)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/repository/IPostsRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.repository
2 |
3 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
4 | import dev.eduayuso.kmcs.domain.entities.PostEntity
5 |
6 | interface IPostsRepository {
7 |
8 | suspend fun getAll(): List
9 |
10 | suspend fun getById(id: String): PostEntity?
11 |
12 | suspend fun getComments(id: String): List
13 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/entities/PostEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.entities
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | class PostEntity(
7 |
8 | var id: String,
9 | var text: String? = null,
10 | var image: String? = null,
11 | var likes: Int? = null,
12 | var publishDate: String? = null,
13 | var tags: List? = null,
14 | var owner: UserEntity? = null
15 |
16 | ): IEntity
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Home/HomeView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 |
4 | struct HomeView: View {
5 |
6 | var body: some View {
7 |
8 | TabView {
9 | PostListView()
10 | .tabItem {
11 | Label("Feed", systemImage: "photo")
12 | }
13 | UserListView()
14 | .tabItem {
15 | Label("Friends", systemImage: "person.2.fill")
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_baseline_tag_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/UserResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class UserResponse(
7 |
8 | var id: String,
9 | var firstName: String? = null,
10 | var lastName: String? = null,
11 | var picture: String? = null
12 | )
13 |
14 | @Serializable
15 | data class UserListResponse(
16 |
17 | val data: List? = null,
18 | val total: Int? = null,
19 | val page: Int? = null
20 | )
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/navigation/Routes.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.navigation
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 |
5 | sealed class Routes(val route: String) {
6 |
7 | object Home: Routes(AppConstants.RouteIds.home)
8 | object PostList: Routes(AppConstants.RouteIds.postList)
9 | object PostDetail: Routes(AppConstants.RouteIds.postDetail)
10 | object UserList: Routes(AppConstants.RouteIds.userList)
11 | object UserDetail: Routes(AppConstants.RouteIds.userDetail)
12 | }
13 |
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_baseline_image_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | maven(url = "https://androidx.dev/storage/compose-compiler/repository/")
7 | }
8 | }
9 |
10 | dependencyResolutionManagement {
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven(url = "https://androidx.dev/storage/compose-compiler/repository/")
15 | }
16 | }
17 |
18 | rootProject.name = "KMCS"
19 | include(":androidApp")
20 | include(":shared")
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/CommentResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | class CommentResponse(
7 |
8 | val id: String? = null,
9 | val owner: UserResponse? = null,
10 | val message: String? = null,
11 | val publishDate: String? = null
12 | )
13 |
14 | @Serializable
15 | data class CommentListResponse(
16 |
17 | val data: List? = null,
18 | val total: Int? = null,
19 | val page: Int? = null
20 | )
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_baseline_message_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/impl/GetPostListInteractor.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.impl
2 |
3 | import dev.eduayuso.kmcs.domain.entities.PostEntity
4 | import dev.eduayuso.kmcs.domain.interactors.GetPostListUseCase
5 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
6 |
7 | class GetPostListInteractor(
8 |
9 | private val repository: IPostsRepository
10 |
11 | ): GetPostListUseCase() {
12 |
13 | override val block: suspend () -> List
14 | get() = {
15 | repository.getAll()
16 | }
17 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/impl/GetUserListInteractor.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.impl
2 |
3 | import dev.eduayuso.kmcs.domain.entities.UserEntity
4 | import dev.eduayuso.kmcs.domain.interactors.GetUserListUseCase
5 | import dev.eduayuso.kmcs.domain.repository.IUsersRepository
6 |
7 | class GetUserListInteractor(
8 |
9 | private val repository: IUsersRepository
10 |
11 | ): GetUserListUseCase() {
12 |
13 | override val block: suspend () -> List
14 | get() = {
15 | repository.getAll()
16 | }
17 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/di/KoinViewModels.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import dev.eduayuso.kmcs.features.posts.detail.PostDetailViewModel
4 | import dev.eduayuso.kmcs.features.posts.list.PostListViewModel
5 | import dev.eduayuso.kmcs.features.users.list.UserListViewModel
6 | import org.koin.core.component.KoinComponent
7 | import org.koin.core.component.inject
8 |
9 | class KoinViewModels: KoinComponent {
10 |
11 | val postList: PostListViewModel by inject()
12 | val userList: UserListViewModel by inject()
13 | val postDetail: PostDetailViewModel by inject()
14 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Users/List/UserListView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct UserListView: View {
5 |
6 | @ObservedObject var viewModel = UserListModel()
7 |
8 | var body: some View {
9 |
10 | VStack {
11 |
12 | List(viewModel.uiState.userList ?? [], id: \.self) { user in
13 | UserItemView(user: user)
14 | }
15 | }
16 | }
17 | }
18 |
19 | struct UserListView_Previews: PreviewProvider {
20 |
21 | static var previews: some View {
22 |
23 | UserListView()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/impl/GetPostDetailInteractor.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.impl
2 |
3 | import dev.eduayuso.kmcs.domain.entities.PostEntity
4 | import dev.eduayuso.kmcs.domain.interactors.GetPostDetailUseCase
5 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
6 |
7 | class GetPostDetailInteractor(
8 |
9 | private val repository: IPostsRepository
10 |
11 | ): GetPostDetailUseCase() {
12 |
13 | override val block: suspend (param: String) -> PostEntity
14 | get() = {
15 | repository.getById(it)!!
16 | }
17 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/source/remote/UsersRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.source.remote
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import dev.eduayuso.kmcs.data.model.UserListResponse
5 | import dev.eduayuso.kmcs.data.model.UserResponse
6 |
7 | class UsersRemoteDataSource(
8 |
9 | private val apiClient: ApiClient,
10 | private val resourceName: String = AppConstants.Apis.DummyApi.users
11 | ) {
12 |
13 | suspend fun getAll() = apiClient.get(resourceName)
14 |
15 | suspend fun getById(id: Int) = apiClient.get("$resourceName/$id")
16 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/model/PostReponse.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | class PostResponse(
7 |
8 | var id: String,
9 | var text: String? = null,
10 | var image: String? = null,
11 | var likes: Int? = null,
12 | var publishDate: String? = null,
13 | var tags: List? = null,
14 | var owner: UserResponse? = null
15 | )
16 |
17 | @Serializable
18 | data class PostListResponse(
19 |
20 | val data: List? = null,
21 | val total: Int? = null,
22 | val page: Int? = null
23 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/impl/GetPostCommentsInteractor.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.impl
2 |
3 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
4 | import dev.eduayuso.kmcs.domain.interactors.GetPostCommentsUseCase
5 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
6 |
7 | class GetPostCommentsInteractor(
8 |
9 | private val repository: IPostsRepository
10 |
11 | ): GetPostCommentsUseCase() {
12 |
13 | override val block: suspend (param: String) -> List
14 | get() = {
15 | repository.getComments(it)
16 | }
17 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/components/LoadingView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material.CircularProgressIndicator
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | fun LoadingView() {
12 |
13 | Box(
14 | contentAlignment = Alignment.Center,
15 | modifier = Modifier.fillMaxSize()
16 | ) {
17 | CircularProgressIndicator()
18 | }
19 | }
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_baseline_thumb_up_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/dev/eduayuso/kmcs/executor/MainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.executor
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Runnable
5 | import platform.Foundation.NSRunLoop
6 | import platform.Foundation.performBlock
7 | import kotlin.coroutines.CoroutineContext
8 |
9 | actual class MainDispatcher {
10 |
11 | actual val dispatcher: CoroutineDispatcher = MainLoopDispatcher
12 | }
13 |
14 | object MainLoopDispatcher: CoroutineDispatcher() {
15 |
16 | override fun dispatch(context: CoroutineContext, block: Runnable) {
17 |
18 | NSRunLoop.mainRunLoop().performBlock { block.run() }
19 | }
20 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/repository/UsersRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.repository
2 |
3 | import dev.eduayuso.kmcs.data.map
4 | import dev.eduayuso.kmcs.data.source.remote.UsersRemoteDataSource
5 | import dev.eduayuso.kmcs.domain.entities.UserEntity
6 | import dev.eduayuso.kmcs.domain.repository.IUsersRepository
7 |
8 | class UsersRepository(
9 |
10 | private val data: UsersRemoteDataSource
11 |
12 | ): IUsersRepository {
13 |
14 | override suspend fun getAll(): List =
15 |
16 | data.getAll()?.map() ?: emptyList()
17 |
18 | override suspend fun getById(id: Int) =
19 |
20 | data.getById(id)?.map()
21 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/type/UseCase.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.type
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.coroutines.flow.flowOf
6 | import kotlinx.coroutines.flow.map
7 |
8 | abstract class UseCase {
9 |
10 | operator fun invoke(): Flow> = flow {
11 | emit(
12 | try {
13 | Resource.Success(block())
14 | } catch (ex: Exception) {
15 | Resource.Error(exception = ex)
16 | }
17 | )
18 | }
19 |
20 | protected abstract val block: suspend () -> Unit
21 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | @main
5 | struct iOSApp: App {
6 |
7 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
8 |
9 | var body: some Scene {
10 | WindowGroup {
11 | HomeView()
12 | }
13 | }
14 | }
15 |
16 | class AppDelegate: NSObject, UIApplicationDelegate {
17 |
18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
19 |
20 | KoinKt.doInitKoinFromIOS(
21 | localProperties: IOSLocalProperties()
22 | )
23 | return true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/type/UseCaseOut.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.type
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.coroutines.flow.flowOf
6 | import kotlinx.coroutines.flow.map
7 |
8 | abstract class UseCaseOut {
9 |
10 | operator fun invoke(): Flow> = flow {
11 | emit(
12 | try {
13 | Resource.Success(block())
14 | } catch (ex: Exception) {
15 | Resource.Error(exception = ex)
16 | }
17 | )
18 | }
19 |
20 | protected abstract val block: suspend () -> OUT
21 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/Detail/PostDetailModel.swift:
--------------------------------------------------------------------------------
1 | import Shared
2 |
3 | class PostDetailModel: ViewModelWrapper {
4 |
5 | @Published var uiState: PostDetailContractState =
6 | .init(post: nil,
7 | isLoadingDetail: false,
8 | isLoadingComments: false,
9 | comments: nil,
10 | isError: false)
11 |
12 | init() {
13 |
14 | super.init(viewModel: KoinViewModels().postDetail)
15 |
16 | viewModel.collect(flow: viewModel.state, collect: { state in
17 |
18 | self.uiState = state as! PostDetailContractState
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/type/UseCaseIn.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.type
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.coroutines.flow.flowOf
6 | import kotlinx.coroutines.flow.map
7 |
8 | abstract class UseCaseIn {
9 |
10 | operator fun invoke(param: IN): Flow> = flow {
11 | emit(
12 | try {
13 | Resource.Success(block(param))
14 | } catch (ex: Exception) {
15 | Resource.Error(exception = ex)
16 | }
17 | )
18 | }
19 |
20 | protected abstract val block: suspend (param: IN) -> Unit
21 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/type/UseCaseInOut.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors.type
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 | import kotlinx.coroutines.flow.flowOf
6 | import kotlinx.coroutines.flow.map
7 |
8 | abstract class UseCaseInOut {
9 |
10 | operator fun invoke(param: IN): Flow> = flow {
11 | emit(
12 | try {
13 | Resource.Success(block(param))
14 | } catch (ex: Exception) {
15 | Resource.Error(exception = ex)
16 | }
17 | )
18 | }
19 |
20 | protected abstract val block: suspend (param: IN) -> OUT
21 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/navigation/NavigationBar.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.navigation
2 |
3 | import dev.eduayuso.kmcs.android.R
4 |
5 | data class NavigationItem(
6 |
7 | val name: String,
8 | val route: String,
9 | val image: Int,
10 | )
11 |
12 | object NavigationBar {
13 |
14 | val items = listOf(
15 | NavigationItem(
16 | name = "Feed",
17 | route = Routes.PostList.route,
18 | image = R.drawable.ic_baseline_image_24
19 | ),
20 | NavigationItem(
21 | name = "Friends",
22 | route = Routes.UserList.route,
23 | image = R.drawable.ic_baseline_group_24
24 | )
25 | )
26 | }
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_baseline_group_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Components/ErrorView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ErrorView: View {
4 |
5 | private var message: String
6 |
7 | init(message: String) {
8 |
9 | self.message = message
10 | }
11 |
12 | var body: some View {
13 |
14 | VStack {
15 | Text(message)
16 | /*Button(action: onClick) {
17 | Text("Try Again")
18 | .font(.title3)
19 | .foregroundColor(.white)
20 | .padding()
21 | .background(Color.blue)
22 | .cornerRadius(10)
23 | .shadow(radius: 10)
24 | }*/
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Components/EmptyView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct EmptyView: View {
4 |
5 | private var message: String
6 |
7 | init(message: String) {
8 |
9 | self.message = message
10 | }
11 |
12 | var body: some View {
13 |
14 | VStack {
15 | Text(message)
16 | /* Button(action: onClick) {
17 | Text("Check Again")
18 | .font(.title3)
19 | .foregroundColor(.white)
20 | .padding()
21 | .background(Color.blue)
22 | .cornerRadius(10)
23 | .shadow(radius: 10)
24 | }*/
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/domain/interactors/UseCases.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.domain.interactors
2 |
3 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
4 | import dev.eduayuso.kmcs.domain.entities.PostEntity
5 | import dev.eduayuso.kmcs.domain.entities.UserEntity
6 | import dev.eduayuso.kmcs.domain.interactors.type.UseCaseInOut
7 | import dev.eduayuso.kmcs.domain.interactors.type.UseCaseOut
8 |
9 | abstract class GetPostListUseCase: UseCaseOut>()
10 |
11 | abstract class GetPostDetailUseCase: UseCaseInOut()
12 |
13 | abstract class GetPostCommentsUseCase: UseCaseInOut>()
14 |
15 | abstract class GetUserListUseCase: UseCaseOut>()
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/List/PostListModel.swift:
--------------------------------------------------------------------------------
1 | import Shared
2 |
3 | class PostListModel: ViewModelWrapper {
4 |
5 | @Published var uiState: PostListContractState = .init(postList: [],
6 | isLoading: false,
7 | isError: false)
8 |
9 | init() {
10 |
11 | super.init(viewModel: KoinViewModels().postList)
12 |
13 | viewModel.collect(flow: viewModel.state, collect: { state in
14 |
15 | self.uiState = state as! PostListContractState
16 | })
17 |
18 | let event = PostListContractEventOnGetPostList()
19 | set(event: event)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Users/List/UserListModel.swift:
--------------------------------------------------------------------------------
1 | import Shared
2 |
3 | class UserListModel: ViewModelWrapper {
4 |
5 | @Published var uiState: UserListContractState = .init(userList: nil,
6 | isLoading: false,
7 | isError: false)
8 |
9 | init() {
10 |
11 | super.init(viewModel: KoinViewModels().userList)
12 |
13 | viewModel.collect(flow: viewModel.state, collect: { state in
14 |
15 | self.uiState = state as! UserListContractState
16 | })
17 |
18 | let event = UserListContractEventOnGetUserList()
19 | set(event: event)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/androidApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/source/remote/PostsRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.source.remote
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import dev.eduayuso.kmcs.data.model.CommentListResponse
5 | import dev.eduayuso.kmcs.data.model.PostListResponse
6 | import dev.eduayuso.kmcs.data.model.PostResponse
7 |
8 | class PostsRemoteDataSource(
9 |
10 | private val apiClient: ApiClient,
11 | private val resourceName: String = AppConstants.Apis.DummyApi.posts
12 | ) {
13 |
14 | suspend fun getAll() = apiClient.get(resourceName)
15 |
16 | suspend fun getById(id: String) = apiClient.get("$resourceName/$id")
17 |
18 | suspend fun getComments(id: String) =
19 |
20 | apiClient.get("$resourceName/$id/comment")
21 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/App.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android
2 |
3 | import android.app.Application
4 | import dev.eduayuso.kmcs.android.system.AndroidLocalProperties
5 | import dev.eduayuso.kmcs.data.LocalProperties
6 | import dev.eduayuso.kmcs.di.KoinViewModels
7 | import dev.eduayuso.kmcs.di.initKoin
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.core.component.KoinComponent
10 | import org.koin.dsl.module
11 |
12 | class App: Application(), KoinComponent {
13 |
14 | override fun onCreate() {
15 |
16 | super.onCreate()
17 | initKoin {
18 | modules(
19 | module {
20 | single { KoinViewModels() }
21 | single { AndroidLocalProperties() }
22 | }
23 | )
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/users/list/UserListContract.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.users.list
2 |
3 | import dev.eduayuso.kmcs.domain.entities.UserEntity
4 | import dev.eduayuso.kmcs.presentation.UIEffect
5 | import dev.eduayuso.kmcs.presentation.UIEvent
6 | import dev.eduayuso.kmcs.presentation.UIState
7 |
8 | interface UserListContract {
9 |
10 | data class State(
11 |
12 | val userList: List? = null,
13 | val isLoading: Boolean = false,
14 | val isError: Boolean = false
15 |
16 | ): UIState
17 |
18 | sealed interface Event: UIEvent {
19 |
20 | object OnGetUserList: Event
21 | data class OnSelectUser(val id: String): Event
22 | }
23 |
24 | sealed interface Effect: UIEffect {
25 |
26 | class NavigateToDetail(val id: String): Effect
27 | }
28 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/repository/PostsRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.repository
2 |
3 | import dev.eduayuso.kmcs.data.map
4 | import dev.eduayuso.kmcs.data.source.remote.PostsRemoteDataSource
5 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
6 | import dev.eduayuso.kmcs.domain.entities.PostEntity
7 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
8 |
9 | class PostsRepository(
10 |
11 | private val data: PostsRemoteDataSource
12 |
13 | ): IPostsRepository {
14 |
15 | override suspend fun getAll(): List =
16 |
17 | data.getAll()?.map() ?: emptyList()
18 |
19 | override suspend fun getById(id: String) =
20 |
21 | data.getById(id)?.map()
22 |
23 | override suspend fun getComments(id: String) =
24 |
25 | data.getComments(id)?.map() ?: emptyList()
26 |
27 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/List/PostListView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct PostListView: View {
5 |
6 | @ObservedObject var viewModel = PostListModel()
7 |
8 | var body: some View {
9 |
10 | NavigationView {
11 |
12 | VStack {
13 |
14 | List(viewModel.uiState.postList ?? [], id: \.self) { post in
15 | NavigationLink {
16 | PostDetailView(postId: post.id)
17 | } label: {
18 | PostItemView(post: post)
19 | }
20 | .isDetailLink(true)
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | struct PostListView_Previews: PreviewProvider {
28 |
29 | static var previews: some View {
30 |
31 | PostListView()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/posts/list/PostListContract.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.posts.list
2 |
3 | import dev.eduayuso.kmcs.domain.entities.PostEntity
4 | import dev.eduayuso.kmcs.domain.entities.UserEntity
5 | import dev.eduayuso.kmcs.presentation.UIEffect
6 | import dev.eduayuso.kmcs.presentation.UIEvent
7 | import dev.eduayuso.kmcs.presentation.UIState
8 |
9 | interface PostListContract {
10 |
11 | data class State(
12 |
13 | val postList: List? = null,
14 | val isLoading: Boolean = false,
15 | val isError: Boolean = false
16 |
17 | ): UIState
18 |
19 | sealed interface Event: UIEvent {
20 |
21 | object OnGetPostList: Event
22 | data class OnSelectPost(val id: String): Event
23 | }
24 |
25 | sealed interface Effect: UIEffect {
26 |
27 | class NavigateToDetail(val id: String): Effect
28 | }
29 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/components/ErrorView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 |
11 | @Composable
12 | fun ErrorView(
13 | message: String
14 | ) {
15 | Box(
16 | contentAlignment = Alignment.Center,
17 | modifier = Modifier.fillMaxSize()
18 | ) {
19 | Column(
20 | horizontalAlignment = Alignment.CenterHorizontally
21 | ) {
22 | Text(
23 | text = message,
24 | style = MaterialTheme.typography.h5
25 | )
26 | Spacer(modifier = Modifier.size(10.dp))
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/IntegrationTestCase.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs
2 |
3 | import dev.eduayuso.kmcs.di.initKoinIntegrationTest
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
7 | import kotlinx.coroutines.test.setMain
8 | import org.koin.core.context.stopKoin
9 | import org.koin.core.module.Module
10 | import org.koin.test.KoinTest
11 | import kotlin.test.AfterTest
12 | import kotlin.test.BeforeTest
13 |
14 | @ExperimentalCoroutinesApi
15 | abstract class IntegrationTestCase: KoinTest {
16 |
17 | @BeforeTest
18 | fun beforeTest() {
19 |
20 | Dispatchers.setMain(UnconfinedTestDispatcher())
21 | initKoinIntegrationTest(repositoryModule)
22 | }
23 |
24 | abstract val repositoryModule: Module
25 |
26 | @AfterTest
27 | fun afterTest() {
28 |
29 | stopKoin()
30 | }
31 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/components/EmptyView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.components
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.OutlinedButton
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 |
12 | @Composable
13 | fun EmptyView(
14 | message: String
15 | ) {
16 | Box(
17 | contentAlignment = Alignment.Center,
18 | modifier = Modifier.fillMaxSize()
19 | ) {
20 | Column(
21 | horizontalAlignment = Alignment.CenterHorizontally
22 | ) {
23 | Text(
24 | text = message,
25 | style = MaterialTheme.typography.h5
26 | )
27 | Spacer(modifier = Modifier.size(10.dp))
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/posts/detail/PostDetailContract.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.posts.detail
2 |
3 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
4 | import dev.eduayuso.kmcs.domain.entities.PostEntity
5 | import dev.eduayuso.kmcs.presentation.UIEffect
6 | import dev.eduayuso.kmcs.presentation.UIEvent
7 | import dev.eduayuso.kmcs.presentation.UIState
8 |
9 | interface PostDetailContract {
10 |
11 | data class State(
12 |
13 | val post: PostEntity? = null,
14 | val isLoadingDetail: Boolean = false,
15 | val isLoadingComments: Boolean = false,
16 | val comments: List? = null,
17 | val isError: Boolean = false
18 |
19 | ): UIState
20 |
21 | sealed interface Event: UIEvent {
22 |
23 | data class OnGetPostDetail(val id: String): Event
24 | data class OnGetComments(val id: String): Event
25 | }
26 |
27 | sealed interface Effect: UIEffect {
28 |
29 | }
30 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/DataMapper.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data
2 |
3 | import dev.eduayuso.kmcs.data.model.*
4 | import dev.eduayuso.kmcs.domain.entities.*
5 |
6 | fun PostResponse.map() = PostEntity(
7 | id = this.id,
8 | text = this.text,
9 | image = this.image,
10 | likes = this.likes,
11 | publishDate = this.publishDate,
12 | owner = this.owner?.map()
13 | )
14 |
15 | fun UserResponse.map() = UserEntity(
16 | id = this.id,
17 | firstName = this.firstName,
18 | lastName = this.lastName,
19 | picture = this.picture,
20 | )
21 |
22 | fun PostListResponse.map() =
23 |
24 | this.data?.map { it.map() }
25 |
26 | fun UserListResponse.map() =
27 |
28 | this.data?.map { it.map() }
29 |
30 | fun CommentResponse.map() = CommentEntity(
31 | id = this.id,
32 | owner = this.owner?.map(),
33 | message = this.message,
34 | publishDate = this.publishDate
35 | )
36 |
37 | fun CommentListResponse.map() =
38 |
39 | this.data?.map { it.map() }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/data/repository/UsersRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.repository
2 |
3 | import dev.eduayuso.kmcs.IntegrationTestCase
4 | import dev.eduayuso.kmcs.domain.repository.IUsersRepository
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.runTest
7 | import org.koin.core.component.inject
8 | import org.koin.dsl.module
9 | import kotlin.test.Test
10 | import kotlin.test.assertTrue
11 |
12 | @ExperimentalCoroutinesApi
13 | class UsersRepositoryTest: IntegrationTestCase() {
14 |
15 | private val repository: IUsersRepository by inject()
16 |
17 | override val repositoryModule = module {
18 |
19 | single { UsersRepository(get()) }
20 | }
21 |
22 | @Test
23 | fun `when call repository to get all users expect return a list of users`() = runTest {
24 |
25 | val result = repository.getAll()
26 | assertTrue(result.isNotEmpty())
27 | assertTrue(result.first().firstName == "First")
28 | }
29 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kotlin Multiplatform Mobile with Jetpack Compose and SwiftUI
2 |
3 | This is a starting point for **KMM (Kotlin Multiplatform Mobile)** projects designed with **Clean Arquitecture** and **MVI (Model View Intent)**, intended to be understable, testable and scalable. This project is preconfigured with essentials libraries and tools, as well as modules, interfaces, base classes, unit and integration tests, just ready for you to start your Android and iOS app.
4 |
5 | * Kotlin Multiplatform
6 | * MVI (Model View Intent)
7 | * UI layer: Jetpack Compose (Android) and SwiftUI (iOS)
8 |
9 | ## Setup
10 | * The app gets data from the API https://dummyapi.io/, so you have to indicate the app id from your dummyapi.io account.
11 | * For Android app add a property called "dummyapi.key" in local.properties (in project root) with yout dummyapi.io app id.
12 | * For iOS add a custom property called: "DummyApiKey" in your Info.plist with yout dummyapi.io app id.
13 |
14 | ## Android:
15 | 
16 |
17 | ## iOS:
18 | 
19 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Surface
9 | import androidx.compose.ui.Modifier
10 | import dev.eduayuso.kmcs.android.navigation.NavigationGraph
11 | import dev.eduayuso.kmcs.di.KoinViewModels
12 | import org.koin.android.ext.android.inject
13 |
14 | class MainActivity: ComponentActivity() {
15 |
16 | private val viewModels: KoinViewModels by inject()
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | setContent {
21 | MaterialTheme {
22 | Surface(
23 | modifier = Modifier.fillMaxSize()
24 | ) {
25 | NavigationGraph(viewModels)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/AppConstants.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs
2 |
3 | object AppConstants {
4 |
5 | object RouteIds {
6 |
7 | const val home = "home"
8 | const val postList = "postList"
9 | const val postDetail = "postDetail"
10 | const val tagList = "tagList"
11 | const val tagDetail = "tagDetail"
12 | const val userList = "userList"
13 | const val userDetail = "userDetail"
14 | }
15 |
16 | object Apis {
17 |
18 | object DummyApi {
19 |
20 | const val url = "https://dummyapi.io/data/v1/"
21 | const val users = "user"
22 | const val posts = "post"
23 | const val tags = "tag"
24 | const val comments = "comment"
25 | }
26 | }
27 |
28 | object Http {
29 |
30 | object Headers {
31 |
32 | const val appId = "app-id"
33 | }
34 | }
35 |
36 | object ViewArguments {
37 |
38 | const val userId = "userId"
39 | const val postId = "postId"
40 | const val tagId = "tagId"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/executor/MainIOExecutor.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.executor
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.flowOn
6 | import org.koin.core.component.KoinComponent
7 | import org.koin.core.component.inject
8 | import kotlin.coroutines.CoroutineContext
9 |
10 | abstract class MainIOExecutor : IExecutorScope, CoroutineScope, KoinComponent {
11 |
12 | private val mainDispatcher: MainDispatcher by inject()
13 | private val ioDispatcher: CoroutineDispatcher by inject()
14 |
15 | private val job: CompletableJob = SupervisorJob()
16 |
17 | override val coroutineContext: CoroutineContext
18 | get() = job + mainDispatcher.dispatcher
19 |
20 | override fun cancel() {
21 | job.cancel()
22 | }
23 |
24 | protected fun collect(
25 | flow: Flow, collect: (T) -> Unit
26 | ) {
27 | launch {
28 | flow
29 | .flowOn(ioDispatcher)
30 | .collect {
31 | collect(it)
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/data/MockedResponses.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data
2 |
3 | import dev.eduayuso.kmcs.data.model.PostListResponse
4 | import dev.eduayuso.kmcs.data.model.PostResponse
5 | import dev.eduayuso.kmcs.data.model.UserListResponse
6 | import dev.eduayuso.kmcs.data.model.UserResponse
7 | import dev.eduayuso.kmcs.domain.entities.UserEntity
8 | import kotlinx.serialization.encodeToString
9 | import kotlinx.serialization.json.Json
10 |
11 | object MockedResponses {
12 |
13 | val userResponse = UserResponse(id = "1", firstName = "First")
14 | val userEntity = UserEntity(id = "1", firstName = "First")
15 | val userJsonResponse = Json.encodeToString(userResponse)
16 |
17 | val postResponse = PostResponse(id = "1", text = "Hola")
18 | val postJsonResponse = Json.encodeToString(postResponse)
19 |
20 | val userListJsonResponse = Json.encodeToString(
21 | UserListResponse(
22 | data = listOf(userResponse)
23 | )
24 | )
25 |
26 | val postListJsonResponse = Json.encodeToString(
27 | PostListResponse(
28 | data = listOf(postResponse)
29 | )
30 | )
31 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/components/TopBarView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.components
2 |
3 | import androidx.compose.foundation.layout.widthIn
4 | import androidx.compose.foundation.layout.wrapContentWidth
5 | import androidx.compose.material.Icon
6 | import androidx.compose.material.IconButton
7 | import androidx.compose.material.Text
8 | import androidx.compose.material.TopAppBar
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.ArrowBack
11 | import androidx.compose.runtime.Composable
12 | import androidx.navigation.NavController
13 |
14 | @Composable
15 | fun TopBarView(
16 | navController: NavController,
17 | title: String
18 | ) {
19 |
20 | TopAppBar(
21 | title = {
22 | Text(
23 | text = title,
24 | maxLines = 1
25 | )
26 | },
27 | navigationIcon = {
28 | IconButton(onClick = { navController.popBackStack() }) {
29 | Icon(
30 | imageVector = Icons.Filled.ArrowBack,
31 | contentDescription = "Back"
32 | )
33 | }
34 | }
35 | )
36 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/di/KoinTest.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import dev.eduayuso.kmcs.data.HttpMock
5 | import dev.eduayuso.kmcs.data.source.remote.ApiClient
6 | import io.ktor.client.*
7 | import io.ktor.client.plugins.contentnegotiation.*
8 | import io.ktor.serialization.kotlinx.json.*
9 | import org.koin.core.context.startKoin
10 | import org.koin.core.module.Module
11 | import org.koin.dsl.module
12 |
13 | fun initKoinIntegrationTest(
14 | repositoryModule: Module
15 | ) =
16 |
17 | startKoin {
18 | modules(
19 | mockedApiModule,
20 | dataSourceModule,
21 | repositoryModule,
22 | dispatcherModule,
23 | platformModule()
24 | )
25 | }
26 |
27 | fun initKoinUnitTest() =
28 |
29 | startKoin {
30 | modules(
31 | dispatcherModule,
32 | platformModule()
33 | )
34 | }
35 |
36 | val mockedApiModule = module {
37 |
38 | single {
39 | HttpClient(engine = HttpMock.engine) {
40 | install(ContentNegotiation) {
41 | json()
42 | }
43 | }
44 | }
45 | single { ApiClient(get(), AppConstants.Apis.DummyApi.url) }
46 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/data/repository/PostsRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.repository
2 |
3 | import dev.eduayuso.kmcs.IntegrationTestCase
4 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.runTest
7 | import org.koin.core.component.inject
8 | import org.koin.dsl.module
9 | import kotlin.test.Test
10 | import kotlin.test.assertNotNull
11 | import kotlin.test.assertTrue
12 |
13 | @ExperimentalCoroutinesApi
14 | class PostsRepositoryTest: IntegrationTestCase() {
15 |
16 | private val repository: IPostsRepository by inject()
17 |
18 | override val repositoryModule = module {
19 |
20 | single { PostsRepository(get()) }
21 | }
22 |
23 | @Test
24 | fun `when calling repository to get all posts expect return a list of posts`() = runTest {
25 |
26 | val result = repository.getAll()
27 | assertTrue(result.isNotEmpty())
28 | assertTrue(result.first().text == "Hola")
29 | }
30 |
31 | @Test
32 | fun `when calling repository to get a post detail expect return a post entity`() = runTest {
33 |
34 | val result = repository.getById(id = "1")
35 | assertNotNull(result)
36 | assertTrue(result.text == "Hola")
37 | }
38 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/Detail/CommentItemView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct CommentItemView: View {
5 |
6 | var comment: CommentEntity
7 | let picDimension: CGFloat = 28.0
8 |
9 | var body: some View {
10 |
11 | VStack(alignment: .leading) {
12 | HStack {
13 | AsyncImage(url: URL(string: comment.owner?.picture ?? "")) { image in
14 | image
15 | .resizable()
16 | .cornerRadius(picDimension)
17 | .frame(width: picDimension, height: picDimension, alignment: .center)
18 | } placeholder: {
19 | ProgressView()
20 | .frame(width: picDimension, height: picDimension, alignment: .center)
21 | }
22 | Text("\(comment.owner?.firstName ?? "") \(comment.owner?.lastName ?? "")")
23 | .font(.body)
24 | .fontWeight(.semibold)
25 | .foregroundColor(.gray)
26 | .padding()
27 | Spacer()
28 | }
29 | Text(comment.message ?? "")
30 | .font(.body)
31 | .fontWeight(.light)
32 | .foregroundColor(.gray)
33 | .padding()
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/data/source/remote/ApiClient.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data.source.remote
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import dev.eduayuso.kmcs.data.LocalProperties
5 | import io.ktor.client.*
6 | import io.ktor.client.call.*
7 | import io.ktor.client.request.*
8 | import io.ktor.http.*
9 |
10 | open class ApiClient(
11 |
12 | val httpClient: HttpClient,
13 | val baseUrl: String,
14 | val localProperties: LocalProperties
15 |
16 | ) {
17 |
18 | suspend inline fun get(endpoint: String): T? =
19 |
20 | httpClient.get("$baseUrl/$endpoint") {
21 | headers {
22 | append(
23 | name = AppConstants.Http.Headers.appId,
24 | value = localProperties.dummyApiKey
25 | )
26 | }
27 | }.body()
28 |
29 | suspend inline fun post(endpoint: String, body: IN): OUT? =
30 |
31 | httpClient.post {
32 | url("$baseUrl/$endpoint")
33 | contentType(ContentType.Application.Json)
34 | headers {
35 | append(
36 | name = AppConstants.Http.Headers.appId,
37 | value = localProperties.dummyApiKey
38 | )
39 | }
40 | setBody(body)
41 | }.body()
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Users/List/UserItemView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct UserItemView: View {
5 |
6 | var user: UserEntity
7 | let picDimension: CGFloat = 48.0
8 |
9 | var body: some View {
10 |
11 | HStack {
12 | AsyncImage(url: URL(string: user.picture ?? "")) { image in
13 | image
14 | .resizable()
15 | .cornerRadius(picDimension)
16 | .frame(width: picDimension, height: picDimension, alignment: .center)
17 | } placeholder: {
18 | ProgressView()
19 | .frame(width: picDimension, height: picDimension, alignment: .center)
20 | }
21 | Text("\(user.firstName ?? "") \(user.lastName ?? "")")
22 | .font(.title3)
23 | .fontWeight(.semibold)
24 | .foregroundColor(.gray)
25 | .padding()
26 | }
27 | }
28 | }
29 |
30 | struct UserItemView_Previews: PreviewProvider {
31 |
32 | static var previews: some View {
33 |
34 | let user: UserEntity = .init(id: "1",
35 | firstName: "aa",
36 | lastName: "bb",
37 | picture: "https://randomuser.me/api/portraits/med/men/10.jpg")
38 |
39 | UserItemView(user: user)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/navigation/NavigationGraph.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.navigation.NavType
5 | import androidx.navigation.compose.NavHost
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.compose.rememberNavController
8 | import androidx.navigation.navArgument
9 | import dev.eduayuso.kmcs.android.features.home.HomeView
10 | import dev.eduayuso.kmcs.android.features.posts.detail.PostDetailView
11 | import dev.eduayuso.kmcs.di.KoinViewModels
12 | import dev.eduayuso.kmcs.features.posts.detail.PostDetailContract
13 |
14 | @Composable
15 | fun NavigationGraph(
16 | viewModels: KoinViewModels
17 | ) {
18 |
19 | val navController = rememberNavController()
20 | val startDestination = Routes.Home.route
21 |
22 | NavHost(
23 | navController = navController,
24 | startDestination = startDestination,
25 | ) {
26 | composable(Routes.Home.route) {
27 | HomeView(navController, viewModels)
28 | }
29 | composable(
30 | route = "${Routes.PostDetail.route}/{id}",
31 | arguments = listOf(navArgument("id") { type = NavType.StringType })
32 | ) { navBackStackEntry ->
33 | PostDetailView(navController, viewModels.postDetail)
34 | val postId = navBackStackEntry.arguments?.getString("id")
35 | val event = PostDetailContract.Event.OnGetPostDetail(id = postId ?: "")
36 | viewModels.postDetail.setEvent(event)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/UnitTestCase.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs
2 |
3 | import dev.eduayuso.kmcs.di.initKoinUnitTest
4 | import dev.eduayuso.kmcs.domain.interactors.type.Resource
5 | import io.mockk.MockKAnnotations
6 | import io.mockk.clearAllMocks
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
10 | import kotlinx.coroutines.test.setMain
11 | import org.koin.core.context.stopKoin
12 | import kotlin.reflect.KClass
13 | import kotlin.test.AfterTest
14 | import kotlin.test.BeforeTest
15 | import kotlin.test.assertEquals
16 | import kotlin.test.fail
17 |
18 | @ExperimentalCoroutinesApi
19 | open class UnitTestCase {
20 |
21 | @BeforeTest
22 | open fun beforeTest() {
23 |
24 | Dispatchers.setMain(UnconfinedTestDispatcher())
25 | initKoinUnitTest()
26 | MockKAnnotations.init(this)
27 | }
28 |
29 | @AfterTest
30 | open fun afterTest() {
31 |
32 | stopKoin()
33 | clearAllMocks()
34 | }
35 |
36 | fun assertSuccess(result: Resource<*>) {
37 |
38 | val actual: KClass<*> = result::class
39 | assertEquals(Resource.Success::class, actual, "The result is not Success => ")
40 | }
41 |
42 | fun assertError(result: Resource<*>) {
43 |
44 | val actual: KClass<*> = result::class
45 | assertEquals(Resource.Error::class, actual, "The result is not Failure => ")
46 | }
47 |
48 | fun resourceTypeFails() {
49 |
50 | fail(message = "Resource type is incorrect")
51 | }
52 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/data/HttpMock.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.data
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import io.ktor.client.engine.mock.*
5 | import io.ktor.client.request.*
6 | import io.ktor.http.*
7 | import kotlinx.serialization.encodeToString
8 | import kotlinx.serialization.json.Json
9 |
10 | object HttpMock {
11 |
12 | val engine = MockEngine { request ->
13 |
14 | handleRequest(request)
15 | }
16 |
17 | private fun MockRequestHandleScope.handleRequest(request: HttpRequestData): HttpResponseData {
18 |
19 | val api = AppConstants.Apis.DummyApi
20 |
21 | val responseContent = with(request.url.encodedPath) {
22 | when {
23 | contains("${api.posts}/1") -> MockedResponses.postJsonResponse
24 | contains(api.posts) -> MockedResponses.postListJsonResponse
25 | contains(api.users) -> MockedResponses.userListJsonResponse
26 | else -> null
27 | }.apply {
28 | Json.encodeToString(this)
29 | }
30 | }
31 |
32 | return if (responseContent == null) {
33 | errorResponse()
34 | } else {
35 | respond(
36 | content = responseContent,
37 | status = HttpStatusCode.OK,
38 | headers = headersOf(HttpHeaders.ContentType, "application/json")
39 | )
40 | }
41 | }
42 |
43 | private fun MockRequestHandleScope.errorResponse(): HttpResponseData {
44 |
45 | return respond(
46 | content = "",
47 | status = HttpStatusCode.BadRequest,
48 | headers = headersOf(HttpHeaders.ContentType, "application/json")
49 | )
50 | }
51 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DummyApiKey
6 | {your app id}
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UIRequiredDeviceCapabilities
31 |
32 | armv7
33 |
34 | UISupportedInterfaceOrientations
35 |
36 | UIInterfaceOrientationPortrait
37 | UIInterfaceOrientationLandscapeLeft
38 | UIInterfaceOrientationLandscapeRight
39 |
40 | UISupportedInterfaceOrientations~ipad
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationPortraitUpsideDown
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UILaunchScreen
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/androidApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.konan.properties.Properties
2 |
3 | plugins {
4 | id(Plugins.androidApp)
5 | kotlin(Plugins.kotlinAndroid)
6 | }
7 |
8 | val properties = Properties()
9 | if (rootProject.file("local.properties").exists()) {
10 | properties.load(rootProject.file("local.properties").inputStream())
11 | }
12 |
13 | android {
14 | namespace = "dev.eduayuso.kmcs.android"
15 | compileSdk = Versions.AndroidSdk.compile
16 | defaultConfig {
17 | applicationId = AndroidApp.id
18 | minSdk = Versions.AndroidSdk.min
19 | targetSdk = Versions.AndroidSdk.target
20 | versionCode = AndroidApp.versionCode
21 | versionName = AndroidApp.versionName
22 | }
23 | buildFeatures {
24 | compose = true
25 | buildConfig = true
26 | }
27 | composeOptions {
28 | kotlinCompilerExtensionVersion = "1.5.0"
29 | }
30 | packaging {
31 | resources {
32 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
33 | }
34 | }
35 | buildTypes {
36 | getByName("release") {
37 | isMinifyEnabled = false
38 | buildConfigField("String", "DUMMY_API_KEY", properties.getProperty("dummyapi.key"))
39 | }
40 | getByName("debug") {
41 | buildConfigField("String", "DUMMY_API_KEY", properties.getProperty("dummyapi.key"))
42 | }
43 | }
44 | compileOptions {
45 | sourceCompatibility = JavaVersion.VERSION_17
46 | targetCompatibility = JavaVersion.VERSION_17
47 | }
48 | }
49 |
50 | dependencies {
51 |
52 | implementation(project(":shared"))
53 | Dependencies.androidPlatform.forEach {
54 | implementation(platform(it))
55 | }
56 | Dependencies.android.forEach {
57 | implementation(it)
58 | }
59 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/users/list/UserListViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.users.list
2 |
3 | import dev.eduayuso.kmcs.domain.interactors.GetUserListUseCase
4 | import dev.eduayuso.kmcs.domain.interactors.type.Resource
5 | import dev.eduayuso.kmcs.presentation.IViewModel
6 | import dev.eduayuso.kmcs.presentation.ViewModel
7 |
8 | interface IUserListViewModel:
9 | IViewModel
10 |
11 | open class UserListViewModel(
12 |
13 | private val getUserListUseCase: GetUserListUseCase
14 |
15 | ) : IUserListViewModel, ViewModel() {
16 |
17 | override fun createInitialState() = UserListContract.State()
18 |
19 | override fun handleEvent(event: UserListContract.Event) {
20 |
21 | when(event) {
22 | is UserListContract.Event.OnGetUserList -> getUserList()
23 | is UserListContract.Event.OnSelectUser -> selectUser(event.id)
24 | }
25 | }
26 |
27 | private fun getUserList() {
28 |
29 | setState {
30 | copy(isLoading = true)
31 | }
32 | collect(getUserListUseCase()) { result ->
33 | when (result) {
34 | is Resource.Error -> setState {
35 | copy(isError = true, userList = emptyList())
36 | }
37 | is Resource.Success -> setState {
38 | copy(isError = false, userList = result.data)
39 | }
40 | }
41 | setState {
42 | copy(isLoading = false)
43 | }
44 | }
45 | }
46 |
47 | private fun selectUser(id: String) {
48 |
49 | setEffect {
50 | UserListContract.Effect.NavigateToDetail(id)
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/posts/list/PostListViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.posts.list
2 |
3 | import dev.eduayuso.kmcs.domain.interactors.GetPostListUseCase
4 | import dev.eduayuso.kmcs.domain.interactors.type.Resource
5 | import dev.eduayuso.kmcs.presentation.IViewModel
6 | import dev.eduayuso.kmcs.presentation.ViewModel
7 | import org.koin.core.component.inject
8 |
9 | interface IPostListViewModel:
10 | IViewModel
11 |
12 | open class PostListViewModel(
13 |
14 | private val getPostListUseCase: GetPostListUseCase
15 |
16 | ) : IPostListViewModel, ViewModel() {
17 |
18 | override fun createInitialState() = PostListContract.State()
19 |
20 | override fun handleEvent(event: PostListContract.Event) {
21 |
22 | when(event) {
23 | is PostListContract.Event.OnGetPostList -> getPostList()
24 | is PostListContract.Event.OnSelectPost -> selectPost(event.id)
25 | }
26 | }
27 |
28 | private fun getPostList() {
29 |
30 | setState {
31 | copy(isLoading = true)
32 | }
33 | collect(getPostListUseCase()) { result ->
34 | when (result) {
35 | is Resource.Error -> setState {
36 | copy(isError = true, postList = emptyList())
37 | }
38 | is Resource.Success -> setState {
39 | copy(isError = false, postList = result.data)
40 | }
41 | }
42 | setState {
43 | copy(isLoading = false)
44 | }
45 | }
46 | }
47 |
48 | private fun selectPost(id: String) {
49 |
50 | setEffect {
51 | PostListContract.Effect.NavigateToDetail(id)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/presentation/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.presentation
2 |
3 | import dev.eduayuso.kmcs.executor.MainIOExecutor
4 | import kotlinx.coroutines.channels.Channel
5 | import kotlinx.coroutines.channels.ReceiveChannel
6 | import kotlinx.coroutines.flow.*
7 | import kotlinx.coroutines.launch
8 |
9 | interface IViewModel {
10 |
11 | val state: StateFlow
12 | val event: SharedFlow
13 | val effect: Flow
14 |
15 | fun setEvent(event: Event)
16 | }
17 |
18 | abstract class ViewModel:
19 |
20 | IViewModel,
21 | MainIOExecutor() {
22 |
23 | private val initialState: State by lazy { createInitialState() }
24 | abstract fun createInitialState(): State
25 |
26 | val currentState: State
27 | get() = state.value
28 |
29 | private val _event: MutableSharedFlow = MutableSharedFlow()
30 | override val event = _event.asSharedFlow()
31 |
32 | private val _state: MutableStateFlow = MutableStateFlow(initialState)
33 | override val state = _state.asStateFlow()
34 |
35 | private val _effect: Channel = Channel()
36 | override val effect = _effect.receiveAsFlow()
37 |
38 | init {
39 |
40 | subscribeEvents()
41 | }
42 |
43 | private fun subscribeEvents() {
44 |
45 | launch {
46 | event.collect {
47 | handleEvent(it)
48 | }
49 | }
50 | }
51 |
52 | abstract fun handleEvent(event: Event)
53 |
54 | override fun setEvent(event: Event) {
55 |
56 | val newEvent = event
57 | launch { _event.emit(newEvent) }
58 | }
59 |
60 | protected fun setState(reduce: State.() -> State) {
61 |
62 | val newState = currentState.reduce()
63 | _state.value = newState
64 | }
65 |
66 | protected fun setEffect(builder: () -> Effect) {
67 |
68 | val effectValue = builder()
69 | launch { _effect.send(effectValue) }
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/features/posts/detail/PostDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.posts.detail
2 |
3 | import dev.eduayuso.kmcs.domain.interactors.GetPostCommentsUseCase
4 | import dev.eduayuso.kmcs.domain.interactors.GetPostDetailUseCase
5 | import dev.eduayuso.kmcs.domain.interactors.type.Resource
6 | import dev.eduayuso.kmcs.presentation.IViewModel
7 | import dev.eduayuso.kmcs.presentation.ViewModel
8 |
9 | interface IPostDetailViewModel:
10 | IViewModel
11 |
12 | open class PostDetailViewModel(
13 |
14 | private val getPostDetailUseCase: GetPostDetailUseCase,
15 | private val getPostCommentsUseCase: GetPostCommentsUseCase,
16 |
17 | ) : IPostDetailViewModel, ViewModel() {
18 |
19 | override fun createInitialState() = PostDetailContract.State()
20 |
21 | override fun handleEvent(event: PostDetailContract.Event) {
22 |
23 | when(event) {
24 | is PostDetailContract.Event.OnGetPostDetail -> getPostDetail(event.id)
25 | is PostDetailContract.Event.OnGetComments -> getPostComments(event.id)
26 | }
27 | }
28 |
29 | private fun getPostDetail(id: String) {
30 |
31 | setState {
32 | copy(isLoadingDetail = true)
33 | }
34 | collect(getPostDetailUseCase(id)) { result ->
35 | when (result) {
36 | is Resource.Error -> setState {
37 | copy(isError = true, post = null)
38 | }
39 | is Resource.Success -> setState {
40 | copy(isError = false, post = result.data)
41 | }
42 | }
43 | setState {
44 | copy(isLoadingDetail = false)
45 | }
46 | setEvent(PostDetailContract.Event.OnGetComments(id))
47 | }
48 | }
49 |
50 | private fun getPostComments(id: String) {
51 |
52 | setState {
53 | copy(isLoadingComments = true)
54 | }
55 | collect(getPostCommentsUseCase(id)) { result ->
56 | when (result) {
57 | is Resource.Error -> setState {
58 | copy(isError = true, post = null)
59 | }
60 | is Resource.Success -> setState {
61 | copy(isError = false, comments = result.data)
62 | }
63 | }
64 | setState {
65 | copy(isLoadingComments = false)
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/shared/src/commonTest/kotlin/dev/eduayuso/kmcs/features/posts/PostListViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.features.posts
2 |
3 | import app.cash.turbine.test
4 | import dev.eduayuso.kmcs.AppConstants.RouteIds.userList
5 | import dev.eduayuso.kmcs.UnitTestCase
6 | import dev.eduayuso.kmcs.data.MockedResponses
7 | import dev.eduayuso.kmcs.domain.interactors.GetUserListUseCase
8 | import dev.eduayuso.kmcs.domain.interactors.type.Resource
9 | import dev.eduayuso.kmcs.features.users.list.UserListContract
10 | import dev.eduayuso.kmcs.features.users.list.UserListViewModel
11 | import io.mockk.coEvery
12 | import io.mockk.impl.annotations.InjectMockKs
13 | import io.mockk.impl.annotations.MockK
14 | import kotlinx.coroutines.ExperimentalCoroutinesApi
15 | import kotlinx.coroutines.flow.flow
16 | import kotlinx.coroutines.test.runTest
17 | import kotlin.test.Test
18 | import kotlin.test.assertFalse
19 | import kotlin.test.assertTrue
20 |
21 | @ExperimentalCoroutinesApi
22 | class PostListViewModelTest: UnitTestCase() {
23 |
24 | @MockK
25 | lateinit var useCase: GetUserListUseCase
26 |
27 | @InjectMockKs
28 | lateinit var viewModel: UserListViewModel
29 |
30 | @Test
31 | fun `when get users successfully expect receive a list of users`() = runTest {
32 |
33 | val userList = listOf(MockedResponses.userEntity)
34 |
35 | coEvery { useCase.invoke() } returns flow {
36 | emit(Resource.Success(userList))
37 | }
38 |
39 | viewModel.state.test {
40 |
41 | viewModel.createInitialState()
42 | skipItems(1)
43 |
44 | viewModel.handleEvent(UserListContract.Event.OnGetUserList)
45 |
46 | assertTrue(awaitItem().isLoading)
47 | val resultItem = awaitItem()
48 | assertFalse(resultItem.userList.isNullOrEmpty())
49 | assertFalse(resultItem.isLoading)
50 | }
51 | }
52 |
53 | @Test
54 | fun `when get users not successfully expect not receive an error`() = runTest {
55 |
56 | coEvery { useCase.invoke() } returns flow {
57 | emit(Resource.Error(Exception("Failure")))
58 | }
59 |
60 | viewModel.state.test {
61 |
62 | viewModel.createInitialState()
63 | skipItems(1)
64 |
65 | viewModel.handleEvent(UserListContract.Event.OnGetUserList)
66 |
67 | assertTrue(awaitItem().isLoading)
68 | val resultItem = awaitItem()
69 | assertTrue(resultItem.userList.isNullOrEmpty())
70 | assertTrue(resultItem.isError)
71 | assertFalse(resultItem.isLoading)
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(Plugins.androidLib)
3 | kotlin(Plugins.kotlinMultiplatform)
4 | kotlin(Plugins.serialization)
5 | }
6 |
7 | kotlin {
8 | android()
9 |
10 | ios {
11 | binaries {
12 | framework("Shared") {
13 | }
14 | }
15 | }
16 |
17 | sourceSets {
18 |
19 | all {
20 | languageSettings.apply {
21 | optIn("kotlin.RequiresOptIn")
22 | }
23 | }
24 |
25 | val commonMain by getting {
26 | dependencies {
27 | Dependencies.Shared.commonMain.forEach {
28 | implementation(it)
29 | }
30 | Dependencies.Shared.commonKotlin.forEach {
31 | implementation(kotlin(it))
32 | }
33 | }
34 | }
35 | val commonTest by getting {
36 | dependencies {
37 | Dependencies.Shared.commonTest.forEach {
38 | implementation(it)
39 | }
40 | Dependencies.Shared.commonKotlinTest.forEach {
41 | implementation(kotlin(it))
42 | }
43 | }
44 | }
45 | val androidMain by getting {
46 | dependencies {
47 | Dependencies.Shared.androidMain.forEach {
48 | implementation(it)
49 | }
50 | }
51 | }
52 | val androidUnitTest by getting {
53 | dependencies {
54 | Dependencies.Shared.androidKotlinTest.forEach {
55 | implementation(kotlin(it))
56 | }
57 | Dependencies.Shared.androidUnitTest.forEach {
58 | implementation(it)
59 | }
60 | }
61 | }
62 | val iosMain by getting {
63 | dependencies {
64 | Dependencies.Shared.iosMain.forEach {
65 | implementation(it)
66 | }
67 | }
68 | }
69 | val iosTest by getting
70 | }
71 | }
72 |
73 | android {
74 | namespace = "dev.eduayuso.kmcs"
75 | compileSdk = Versions.AndroidSdk.compile
76 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
77 | defaultConfig {
78 | minSdk = Versions.AndroidSdk.min
79 | targetSdk = Versions.AndroidSdk.target
80 | }
81 | compileOptions {
82 | sourceCompatibility = JavaVersion.VERSION_17
83 | targetCompatibility = JavaVersion.VERSION_17
84 | }
85 | }
86 |
87 | tasks.withType().all {
88 | kotlinOptions {
89 | jvmTarget = "17"
90 | }
91 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/List/PostItemView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct PostItemView: View {
5 |
6 | var post: PostEntity
7 | let picDimension: CGFloat = 48.0
8 |
9 | var body: some View {
10 |
11 | VStack {
12 |
13 | HStack {
14 | AsyncImage(url: URL(string: post.owner?.picture ?? "")) { image in
15 | image
16 | .resizable()
17 | .cornerRadius(picDimension)
18 | .frame(width: picDimension, height: picDimension, alignment: .center)
19 | } placeholder: {
20 | ProgressView()
21 | .frame(width: picDimension, height: picDimension, alignment: .center)
22 | }
23 | VStack(alignment: .leading) {
24 | Text("\(post.owner?.firstName ?? "") \(post.owner?.lastName ?? "")")
25 | .font(.title3)
26 | .fontWeight(.semibold)
27 | .foregroundColor(.gray)
28 | Text(post.publishDate ?? "")
29 | .font(.body)
30 | .foregroundColor(.gray)
31 | }
32 | .padding()
33 | }
34 | AsyncImage(url: URL(string: post.image ?? "")) { image in
35 | image
36 | .resizable()
37 | .cornerRadius(8)
38 | .scaledToFit()
39 | .frame(width: 256, height: 200, alignment: .center)
40 | .clipped()
41 | } placeholder: {
42 | ProgressView()
43 | .frame(width: 256, height: 256, alignment: .center)
44 | }
45 | }
46 | }
47 | }
48 |
49 | struct PostItemView_Previews: PreviewProvider {
50 |
51 | static var previews: some View {
52 |
53 | let owner: UserEntity = .init(id: "1",
54 | firstName: "aa",
55 | lastName: "bb",
56 | picture: "https://randomuser.me/api/portraits/med/men/10.jpg")
57 |
58 | let post: PostEntity = .init(id: "1",
59 | text: "Post example",
60 | image: "https://img.dummyapi.io/photo-1568480541687-16c2f73eea4c.jpg",
61 | likes: 3,
62 | publishDate: "01-02-2022",
63 | tags: [],
64 | owner: owner)
65 |
66 | PostItemView(post: post)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/users/list/UserItemView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.users.list
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.Text
9 | import androidx.compose.material3.Card
10 | import androidx.compose.material3.CardDefaults
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import coil.annotation.ExperimentalCoilApi
22 | import coil.compose.rememberImagePainter
23 | import dev.eduayuso.kmcs.domain.entities.UserEntity
24 |
25 | @ExperimentalCoilApi
26 | @Composable
27 | fun UserItemView(
28 | user: UserEntity,
29 | onClick: () -> Unit
30 | ) {
31 |
32 | Card(
33 | shape = RoundedCornerShape(8.dp),
34 | modifier = Modifier
35 | .padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 2.dp)
36 | .fillMaxWidth()
37 | .clickable(onClick = onClick),
38 | elevation = CardDefaults.cardElevation(
39 | defaultElevation = 2.dp
40 | ),
41 | colors = CardDefaults.cardColors(containerColor = Color.White)
42 | ) {
43 |
44 | Row(
45 | verticalAlignment = Alignment.CenterVertically
46 | ) {
47 | Image(
48 | painter = rememberImagePainter(user.picture),
49 | contentDescription = null,
50 | modifier = Modifier
51 | .padding(all = 16.dp)
52 | .clip(CircleShape)
53 | .width(56.dp)
54 | .height(56.dp)
55 | )
56 | Column {
57 | Text(
58 | text = "${user.firstName ?: ""} ${user.lastName ?: ""}",
59 | color = Color.Gray,
60 | style = TextStyle(
61 | fontSize = 20.sp,
62 | fontWeight = FontWeight.Bold
63 | )
64 | )
65 | }
66 | }
67 | }
68 | }
69 |
70 | @Preview
71 | @Composable
72 | fun UserItemViewPreview() {
73 |
74 | val character = UserEntity(
75 | id = "1",
76 | firstName = "First",
77 | lastName = "Last"
78 | )
79 |
80 | UserItemView(character) { }
81 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/home/HomeView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.home
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material.Scaffold
5 | import androidx.compose.material.Text
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.NavigationBar
8 | import androidx.compose.material3.NavigationBarItem
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.navigation.NavHostController
13 | import androidx.navigation.compose.NavHost
14 | import androidx.navigation.compose.composable
15 | import androidx.navigation.compose.currentBackStackEntryAsState
16 | import androidx.navigation.compose.rememberNavController
17 | import dev.eduayuso.kmcs.android.features.posts.list.PostListView
18 | import dev.eduayuso.kmcs.android.features.users.list.UserListView
19 | import dev.eduayuso.kmcs.android.navigation.NavigationBar
20 | import dev.eduayuso.kmcs.android.navigation.Routes
21 | import dev.eduayuso.kmcs.di.KoinViewModels
22 | import dev.eduayuso.kmcs.features.posts.list.PostListContract
23 | import dev.eduayuso.kmcs.features.users.list.UserListContract
24 |
25 | @Composable
26 | fun HomeView(
27 | rootNavController: NavHostController,
28 | viewModels: KoinViewModels
29 | ) {
30 |
31 | val navController = rememberNavController()
32 | val startDestination = Routes.PostList.route
33 |
34 | val backStackEntry = navController.currentBackStackEntryAsState()
35 |
36 | Scaffold(
37 | bottomBar = {
38 | NavigationBar {
39 |
40 | NavigationBar.items.forEach { item ->
41 |
42 | val selected = item.route == backStackEntry.value?.destination?.route
43 | NavigationBarItem(
44 | selected = selected,
45 | onClick = { navController.navigate(item.route) },
46 | label = { Text(text = item.name) },
47 | icon = {
48 | Icon(
49 | painter = painterResource(id = item.image),
50 | contentDescription = item.name
51 | )
52 | }
53 | )
54 | }
55 | }
56 | }
57 | ) { padding ->
58 |
59 | NavHost(
60 | navController = navController,
61 | startDestination = startDestination,
62 | Modifier.padding(padding)
63 | ) {
64 | composable(Routes.PostList.route) {
65 | val viewModel = viewModels.postList
66 | PostListView(rootNavController, viewModel)
67 | viewModel.setEvent(PostListContract.Event.OnGetPostList)
68 | }
69 | composable(Routes.UserList.route) {
70 | val viewModel = viewModels.userList
71 | UserListView(rootNavController, viewModel)
72 | viewModel.setEvent(UserListContract.Event.OnGetUserList)
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Features/Posts/Detail/PostDetailView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Shared
3 |
4 | struct PostDetailView: View {
5 |
6 | @ObservedObject var viewModel = PostDetailModel()
7 |
8 | let postId: String
9 |
10 | var body: some View {
11 |
12 | ZStack {
13 |
14 | if viewModel.uiState.isLoadingDetail {
15 | LoadingView()
16 | } else if let post = viewModel.uiState.post {
17 | VStack {
18 | PostDetailContent(post: post)
19 | if !viewModel.uiState.isLoadingComments {
20 | CommentsView(comments: viewModel.uiState.comments ?? [])
21 | }
22 | Spacer()
23 | }
24 | } else {
25 | EmptyView(message: "Error fetching data")
26 | }
27 | }
28 | .navigationTitle(Text("\(viewModel.uiState.post?.text ?? "")"))
29 | .onAppear() {
30 | let event = PostDetailContractEventOnGetPostDetail(id: postId)
31 | viewModel.set(event: event)
32 | }
33 | }
34 | }
35 |
36 | struct PostDetailContent: View {
37 |
38 | let post: PostEntity
39 | let picDimension: CGFloat = 48
40 |
41 | var body: some View {
42 |
43 | VStack {
44 |
45 | HStack {
46 | AsyncImage(url: URL(string: post.owner?.picture ?? "")) { image in
47 | image
48 | .resizable()
49 | .cornerRadius(picDimension)
50 | .frame(width: picDimension, height: picDimension, alignment: .center)
51 | .padding()
52 | } placeholder: {
53 | ProgressView()
54 | .frame(width: picDimension, height: picDimension, alignment: .center)
55 | }
56 | VStack(alignment: .leading) {
57 | Text("\(post.owner?.firstName ?? "") \(post.owner?.lastName ?? "")")
58 | .font(.title3)
59 | .fontWeight(.semibold)
60 | .foregroundColor(.gray)
61 | Text(post.publishDate ?? "")
62 | .font(.body)
63 | .foregroundColor(.gray)
64 | }
65 | .padding()
66 | Spacer()
67 | }
68 | AsyncImage(url: URL(string: post.image ?? "")) { image in
69 | image
70 | .resizable()
71 | .cornerRadius(8)
72 | .scaledToFit()
73 | .frame(width: 348, height: 256, alignment: .center)
74 | .clipped()
75 | } placeholder: {
76 | ProgressView()
77 | .frame(width: 348, height: 256, alignment: .center)
78 | }
79 | Text(post.text ?? "")
80 | .font(.title2)
81 | .fontWeight(.light)
82 | .foregroundColor(.gray)
83 | .padding()
84 | }
85 | }
86 | }
87 |
88 | struct CommentsView: View {
89 |
90 | let comments: [CommentEntity]
91 |
92 | var body: some View {
93 |
94 | VStack {
95 |
96 | List(comments, id: \.self) { comment in
97 | CommentItemView(comment: comment)
98 | }
99 | }
100 | }
101 | }
102 |
103 | struct PostDetailView_Previews: PreviewProvider {
104 |
105 | static var previews: some View {
106 |
107 | PostDetailView(postId: "2")
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/posts/list/PostItemView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.posts.list
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material.Text
10 | import androidx.compose.material3.Card
11 | import androidx.compose.material3.CardDefaults
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.layout.ContentScale
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.tooling.preview.Preview
21 | import androidx.compose.ui.unit.dp
22 | import androidx.compose.ui.unit.sp
23 | import androidx.compose.ui.zIndex
24 | import coil.annotation.ExperimentalCoilApi
25 | import coil.compose.rememberImagePainter
26 | import dev.eduayuso.kmcs.domain.entities.PostEntity
27 |
28 | @ExperimentalCoilApi
29 | @Composable
30 | fun PostItemView(
31 | post: PostEntity,
32 | onClick: () -> Unit
33 | ) {
34 |
35 | Card(
36 | shape = RoundedCornerShape(8.dp),
37 | modifier = Modifier
38 | .padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 2.dp)
39 | .fillMaxWidth()
40 | .clickable(onClick = onClick),
41 | elevation = CardDefaults.cardElevation(
42 | defaultElevation = 2.dp
43 | ),
44 | colors = CardDefaults.cardColors(containerColor = Color.White)
45 | ) {
46 |
47 | Column {
48 |
49 | Row(
50 | verticalAlignment = Alignment.CenterVertically
51 | ) {
52 | Image(
53 | painter = rememberImagePainter(post.owner?.picture),
54 | contentDescription = null,
55 | modifier = Modifier
56 | .padding(all = 16.dp)
57 | .clip(CircleShape)
58 | .width(56.dp)
59 | .height(56.dp)
60 | )
61 | Column {
62 | Text(
63 | text = "${post.owner?.firstName ?: ""} ${post.owner?.lastName ?: ""}",
64 | color = Color.Gray,
65 | style = TextStyle(
66 | fontSize = 20.sp,
67 | fontWeight = FontWeight.Bold
68 | )
69 | )
70 | Text(
71 | text = post.publishDate ?: "",
72 | color = Color.Gray
73 | )
74 | }
75 | }
76 |
77 | Image(
78 | painter = rememberImagePainter(post.image),
79 | contentScale = ContentScale.FillWidth,
80 | contentDescription = null,
81 | modifier = Modifier
82 | .fillMaxWidth()
83 | .height(256.dp)
84 | .padding(8.dp)
85 | .clip(RoundedCornerShape(4.dp))
86 | )
87 | }
88 | }
89 | }
90 |
91 | @Preview
92 | @Composable
93 | fun PostItemViewPreview() {
94 |
95 | val character = PostEntity(
96 | id = "1",
97 | text = "Toxic Rick",
98 | publishDate = "01-02-2000",
99 | image = "https://rickandmortyapi.com/api/character/avatar/361.jpeg"
100 | )
101 |
102 | PostItemView(character) { }
103 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/dev/eduayuso/kmcs/di/Koin.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.di
2 |
3 | import dev.eduayuso.kmcs.AppConstants
4 | import dev.eduayuso.kmcs.data.LocalProperties
5 | import dev.eduayuso.kmcs.data.repository.PostsRepository
6 | import dev.eduayuso.kmcs.data.repository.UsersRepository
7 | import dev.eduayuso.kmcs.data.source.remote.ApiClient
8 | import dev.eduayuso.kmcs.data.source.remote.PostsRemoteDataSource
9 | import dev.eduayuso.kmcs.data.source.remote.UsersRemoteDataSource
10 | import dev.eduayuso.kmcs.domain.interactors.GetPostCommentsUseCase
11 | import dev.eduayuso.kmcs.domain.interactors.GetPostDetailUseCase
12 | import dev.eduayuso.kmcs.domain.interactors.GetPostListUseCase
13 | import dev.eduayuso.kmcs.domain.interactors.GetUserListUseCase
14 | import dev.eduayuso.kmcs.domain.interactors.impl.GetPostCommentsInteractor
15 | import dev.eduayuso.kmcs.domain.interactors.impl.GetPostDetailInteractor
16 | import dev.eduayuso.kmcs.domain.interactors.impl.GetPostListInteractor
17 | import dev.eduayuso.kmcs.domain.interactors.impl.GetUserListInteractor
18 | import dev.eduayuso.kmcs.domain.repository.IPostsRepository
19 | import dev.eduayuso.kmcs.domain.repository.IUsersRepository
20 | import dev.eduayuso.kmcs.features.posts.detail.PostDetailViewModel
21 | import dev.eduayuso.kmcs.features.posts.list.PostListViewModel
22 | import dev.eduayuso.kmcs.features.users.list.UserListViewModel
23 | import io.ktor.client.*
24 | import io.ktor.client.plugins.contentnegotiation.*
25 | import io.ktor.client.plugins.logging.*
26 | import io.ktor.serialization.kotlinx.json.*
27 | import kotlinx.coroutines.Dispatchers
28 | import kotlinx.serialization.json.Json
29 | import org.koin.core.context.startKoin
30 | import org.koin.dsl.KoinAppDeclaration
31 | import org.koin.dsl.module
32 |
33 | fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
34 |
35 | startKoin {
36 | appDeclaration()
37 | modules(
38 | apiModule,
39 | dataSourceModule,
40 | repositoryModule,
41 | useCasesModule,
42 | dispatcherModule,
43 | viewModelModule,
44 | platformModule()
45 | )
46 | }
47 |
48 | fun initKoinFromIOS(localProperties: LocalProperties) = initKoin {
49 | modules(
50 | module {
51 | single { localProperties }
52 | }
53 | )
54 | }
55 |
56 | val apiModule = module {
57 |
58 | single {
59 | Json {
60 | encodeDefaults = false
61 | isLenient = true
62 | ignoreUnknownKeys = true
63 | useArrayPolymorphism = true
64 | }
65 | }
66 |
67 | single {
68 | HttpClient {
69 | install(ContentNegotiation) {
70 | json(get())
71 | }
72 | install(Logging) {
73 | logger = object : Logger {
74 | override fun log(message: String) {
75 | co.touchlab.kermit.Logger.v { "Ktor $message" }
76 | }
77 | }
78 | level = LogLevel.ALL
79 | }
80 | }
81 | }
82 | single { ApiClient(get(), AppConstants.Apis.DummyApi.url, get()) }
83 | }
84 |
85 | val dataSourceModule = module {
86 |
87 | single { PostsRemoteDataSource(get()) }
88 | single { UsersRemoteDataSource(get()) }
89 | }
90 |
91 | val repositoryModule = module {
92 |
93 | single { PostsRepository(get()) }
94 | single { UsersRepository(get()) }
95 | }
96 |
97 | val useCasesModule = module {
98 |
99 | single { GetPostListInteractor(get()) }
100 | single { GetUserListInteractor(get()) }
101 | single { GetPostDetailInteractor(get()) }
102 | single { GetPostCommentsInteractor(get()) }
103 | }
104 |
105 | val viewModelModule = module {
106 |
107 | single { PostListViewModel(get()) }
108 | single { UserListViewModel(get()) }
109 | single { PostDetailViewModel(get(), get()) }
110 | }
111 |
112 | val dispatcherModule = module {
113 |
114 | factory { Dispatchers.Default }
115 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/posts/list/PostListView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.posts.list
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.runtime.collectAsState
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.navigation.NavHostController
18 | import androidx.navigation.compose.rememberNavController
19 | import coil.annotation.ExperimentalCoilApi
20 | import dev.eduayuso.kmcs.AppConstants
21 | import dev.eduayuso.kmcs.android.components.EmptyView
22 | import dev.eduayuso.kmcs.android.components.ErrorView
23 | import dev.eduayuso.kmcs.android.components.LoadingView
24 | import dev.eduayuso.kmcs.domain.entities.PostEntity
25 | import dev.eduayuso.kmcs.features.posts.list.IPostListViewModel
26 | import dev.eduayuso.kmcs.features.posts.list.PostListContract
27 | import kotlinx.coroutines.channels.Channel
28 | import kotlinx.coroutines.flow.*
29 |
30 | @OptIn(ExperimentalCoilApi::class)
31 | @Composable
32 | fun PostListView(
33 | navController: NavHostController,
34 | viewModel: IPostListViewModel
35 | ) {
36 |
37 | val state: PostListContract.State by viewModel.state.collectAsState()
38 |
39 | LaunchedEffect(key1 = true) {
40 | viewModel.effect.collectLatest { effect ->
41 | when (effect) {
42 | is PostListContract.Effect.NavigateToDetail -> {
43 | val route = "${AppConstants.RouteIds.postDetail}/${effect.id}"
44 | navController.navigate(route)
45 | }
46 | }
47 | }
48 | }
49 |
50 | Box(
51 | contentAlignment = Alignment.Center,
52 | modifier = Modifier
53 | .fillMaxSize()
54 | .background(MaterialTheme.colors.surface)
55 | ) {
56 | if (state.isLoading) {
57 | LoadingView()
58 | } else if (state.isError) {
59 | ErrorView(message = "Error fetching data")
60 | } else if (state.postList?.isEmpty() == true) {
61 | EmptyView(message = "Empty")
62 | } else if (state.postList != null) {
63 | PostListContent(viewModel, state.postList!!)
64 | }
65 | }
66 | }
67 |
68 | @ExperimentalCoilApi
69 | @Composable
70 | fun PostListContent(
71 | viewModel: IPostListViewModel,
72 | data: List
73 | ) {
74 | LazyColumn(
75 | modifier = Modifier.fillMaxSize(),
76 | verticalArrangement = Arrangement.Top
77 | ) {
78 | items(data) { post ->
79 | PostItemView(
80 | post = post
81 | ) {
82 | val event = PostListContract.Event.OnSelectPost(post.id)
83 | viewModel.setEvent(event)
84 | }
85 |
86 | }
87 | }
88 | }
89 |
90 | @Preview
91 | @Composable
92 | fun PostListPreview() {
93 |
94 | val initialState = PostListContract.State(
95 | isLoading = false,
96 | isError = false,
97 | postList = emptyList()
98 | )
99 |
100 | val viewModel = object: IPostListViewModel {
101 | override val state: StateFlow
102 | get() = MutableStateFlow(initialState).asStateFlow()
103 | override val event: SharedFlow
104 | get() = MutableSharedFlow()
105 | override val effect: Flow
106 | get() = Channel().receiveAsFlow()
107 |
108 | override fun setEvent(event: PostListContract.Event) {
109 | }
110 | }
111 |
112 | PostListView(rememberNavController(), viewModel)
113 | }
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/users/list/UserListView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.users.list
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.collectAsState
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.tooling.preview.Preview
18 | import androidx.navigation.NavHostController
19 | import androidx.navigation.compose.rememberNavController
20 | import coil.annotation.ExperimentalCoilApi
21 | import dev.eduayuso.kmcs.AppConstants
22 | import dev.eduayuso.kmcs.android.components.EmptyView
23 | import dev.eduayuso.kmcs.android.components.ErrorView
24 | import dev.eduayuso.kmcs.android.components.LoadingView
25 | import dev.eduayuso.kmcs.android.features.posts.list.PostItemView
26 | import dev.eduayuso.kmcs.domain.entities.PostEntity
27 | import dev.eduayuso.kmcs.domain.entities.UserEntity
28 | import dev.eduayuso.kmcs.features.posts.list.IPostListViewModel
29 | import dev.eduayuso.kmcs.features.posts.list.PostListContract
30 | import dev.eduayuso.kmcs.features.users.list.IUserListViewModel
31 | import dev.eduayuso.kmcs.features.users.list.UserListContract
32 | import kotlinx.coroutines.channels.Channel
33 | import kotlinx.coroutines.flow.*
34 |
35 | @OptIn(ExperimentalCoilApi::class)
36 | @Composable
37 | fun UserListView(
38 | navController: NavHostController,
39 | viewModel: IUserListViewModel
40 | ) {
41 |
42 | val state: UserListContract.State by viewModel.state.collectAsState()
43 |
44 | LaunchedEffect(key1 = true) {
45 | viewModel.effect.collectLatest { effect ->
46 | when (effect) {
47 | is UserListContract.Effect.NavigateToDetail -> {
48 | // TODO
49 | }
50 | }
51 | }
52 | }
53 |
54 | Box(
55 | contentAlignment = Alignment.Center,
56 | modifier = Modifier
57 | .fillMaxSize()
58 | .background(MaterialTheme.colors.surface)
59 | ) {
60 | if (state.isLoading) {
61 | LoadingView()
62 | } else if (state.isError) {
63 | ErrorView(message = "Error fetching data")
64 | } else if (state.userList?.isEmpty() == true) {
65 | EmptyView(message = "Empty")
66 | } else if (state.userList != null) {
67 | UserListContent(viewModel, state.userList!!)
68 | }
69 | }
70 | }
71 |
72 | @ExperimentalCoilApi
73 | @Composable
74 | fun UserListContent(
75 | viewModel: IUserListViewModel,
76 | data: List
77 | ) {
78 | LazyColumn(
79 | modifier = Modifier.fillMaxSize(),
80 | verticalArrangement = Arrangement.Top
81 | ) {
82 | items(data) { user ->
83 | UserItemView(user) {
84 | val event = UserListContract.Event.OnSelectUser(user.id)
85 | viewModel.setEvent(event)
86 | }
87 |
88 | }
89 | }
90 | }
91 |
92 | @Preview
93 | @Composable
94 | fun UserListPreview() {
95 |
96 | val initialState = UserListContract.State(
97 | isLoading = false,
98 | isError = false,
99 | userList = emptyList()
100 | )
101 |
102 | val viewModel = object: IUserListViewModel {
103 | override val state: StateFlow
104 | get() = MutableStateFlow(initialState).asStateFlow()
105 | override val event: SharedFlow
106 | get() = MutableSharedFlow()
107 | override val effect: Flow
108 | get() = Channel().receiveAsFlow()
109 |
110 | override fun setEvent(event: UserListContract.Event) {
111 | }
112 | }
113 |
114 | UserListView(rememberNavController(), viewModel)
115 | }
--------------------------------------------------------------------------------
/androidApp/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/androidApp/src/main/java/dev/eduayuso/kmcs/android/features/posts/detail/PostDetailView.kt:
--------------------------------------------------------------------------------
1 | package dev.eduayuso.kmcs.android.features.posts.detail
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.items
8 | import androidx.compose.foundation.shape.CircleShape
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.*
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.ThumbUp
13 | import androidx.compose.material3.Card
14 | import androidx.compose.material3.CardDefaults
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.layout.ContentScale
23 | import androidx.compose.ui.text.TextStyle
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.tooling.preview.Preview
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import androidx.navigation.NavHostController
29 | import androidx.navigation.compose.rememberNavController
30 | import coil.annotation.ExperimentalCoilApi
31 | import coil.compose.rememberImagePainter
32 | import dev.eduayuso.kmcs.android.components.EmptyView
33 | import dev.eduayuso.kmcs.android.components.ErrorView
34 | import dev.eduayuso.kmcs.android.components.LoadingView
35 | import dev.eduayuso.kmcs.android.components.TopBarView
36 | import dev.eduayuso.kmcs.domain.entities.CommentEntity
37 | import dev.eduayuso.kmcs.domain.entities.PostEntity
38 | import dev.eduayuso.kmcs.features.posts.detail.IPostDetailViewModel
39 | import dev.eduayuso.kmcs.features.posts.detail.PostDetailContract
40 | import kotlinx.coroutines.channels.Channel
41 | import kotlinx.coroutines.flow.*
42 |
43 | @ExperimentalCoilApi
44 | @Composable
45 | fun PostDetailView(
46 | navController: NavHostController,
47 | viewModel: IPostDetailViewModel
48 | ) {
49 |
50 | val state: PostDetailContract.State by viewModel.state.collectAsState()
51 |
52 | Scaffold(
53 | scaffoldState = rememberScaffoldState(),
54 | topBar = {
55 | TopBarView(navController, title = state.post?.text?.uppercase() ?: "")
56 | },
57 | ) { padding ->
58 |
59 | Box(
60 | contentAlignment = Alignment.Center,
61 | modifier = Modifier.padding(padding)
62 | ) {
63 | if (state.isLoadingDetail) {
64 | LoadingView()
65 | } else if (state.isError) {
66 | ErrorView(message = "Error fetching detail")
67 | } else if (state.post == null) {
68 | EmptyView(message = "Empty")
69 | } else {
70 | PostDetailContentView(state)
71 | }
72 | }
73 | }
74 | }
75 |
76 | @Composable
77 | fun PostDetailContentView(state: PostDetailContract.State) {
78 |
79 | Column(
80 | modifier = Modifier
81 | .padding(all = 16.dp)) {
82 |
83 | Row(
84 | verticalAlignment = Alignment.CenterVertically,
85 | modifier = Modifier
86 | .fillMaxWidth()
87 | ) {
88 | Image(
89 | painter = rememberImagePainter(state.post?.owner?.picture),
90 | contentDescription = null,
91 | modifier = Modifier
92 | .padding(all = 16.dp)
93 | .clip(CircleShape)
94 | .width(56.dp)
95 | .height(56.dp)
96 | )
97 | Column {
98 | Text(
99 | text = "${state.post?.owner?.firstName ?: ""} ${state.post?.owner?.lastName ?: ""}",
100 | color = Color.Gray,
101 | style = TextStyle(
102 | fontSize = 20.sp,
103 | fontWeight = FontWeight.Bold
104 | )
105 | )
106 | Text(
107 | text = state.post?.publishDate ?: "",
108 | color = Color.Gray
109 | )
110 | }
111 | IconButton(
112 | onClick = { },
113 | modifier = Modifier
114 | .padding(all = 16.dp)) {
115 |
116 | Row {
117 | Icon(
118 | imageVector = Icons.Filled.ThumbUp,
119 | contentDescription = "Back"
120 | )
121 | Text(
122 | text = "${state.post?.likes ?: 0}",
123 | color = Color.Gray
124 | )
125 | }
126 | }
127 | }
128 |
129 | Image(
130 | painter = rememberImagePainter(state.post?.image),
131 | contentScale = ContentScale.FillWidth,
132 | contentDescription = null,
133 | modifier = Modifier
134 | .fillMaxWidth()
135 | .height(256.dp)
136 | .padding(8.dp)
137 | .clip(RoundedCornerShape(4.dp))
138 | )
139 |
140 | Text(
141 | text = state.post?.text ?: "",
142 | color = Color.Gray,
143 | modifier = Modifier
144 | .padding(all = 8.dp)
145 | )
146 |
147 | if (!state.isLoadingComments) {
148 | LazyColumn(
149 | modifier = Modifier.fillMaxSize(),
150 | verticalArrangement = Arrangement.Top
151 | ) {
152 | items(state.comments ?: emptyList()) { comment ->
153 |
154 | CommentView(comment)
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
161 | @Composable
162 | fun CommentView(comment: CommentEntity) {
163 |
164 | Card(
165 | shape = RoundedCornerShape(8.dp),
166 | modifier = Modifier
167 | .padding(10.dp)
168 | .fillMaxWidth(),
169 | elevation = CardDefaults.cardElevation(
170 | defaultElevation = 2.dp
171 | ),
172 | colors = CardDefaults.cardColors(containerColor = Color.White)
173 | ) {
174 |
175 | Row(
176 | verticalAlignment = Alignment.CenterVertically
177 | ) {
178 | Image(
179 | painter = rememberImagePainter(comment.owner?.picture),
180 | contentDescription = null,
181 | modifier = Modifier
182 | .padding(all = 14.dp)
183 | .clip(CircleShape)
184 | .width(36.dp)
185 | .height(36.dp)
186 | )
187 | Column {
188 | Text(
189 | text = comment.publishDate ?: "",
190 | color = Color.Gray
191 | )
192 | Text(
193 | text = comment.message ?: "",
194 | color = Color.Gray,
195 | style = TextStyle(
196 | fontSize = 20.sp,
197 | fontWeight = FontWeight.Bold
198 | )
199 | )
200 | }
201 | }
202 | }
203 | }
204 |
205 | @Preview
206 | @Composable
207 | fun PostDetailPreview() {
208 |
209 | val initialState = PostDetailContract.State(
210 | post = PostEntity(id = "1", text = "hola"),
211 | isLoadingComments = false,
212 | isError = false,
213 | comments = null
214 | )
215 |
216 | val viewModel = object: IPostDetailViewModel {
217 | override val state: StateFlow
218 | get() = MutableStateFlow(initialState).asStateFlow()
219 | override val event: SharedFlow
220 | get() = MutableSharedFlow()
221 | override val effect: Flow
222 | get() = Channel().receiveAsFlow()
223 |
224 | override fun setEvent(event: PostDetailContract.Event) {
225 | }
226 | }
227 |
228 | PostDetailView(rememberNavController(), viewModel)
229 | }
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
13 | 8753C8F1297E8A44001EDB75 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8753C8F0297E8A44001EDB75 /* Localizable.strings */; };
14 | 876BDA27297FC4AF006C638A /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876BDA26297FC4AF006C638A /* PostListView.swift */; };
15 | 876BDA29297FC4B6006C638A /* PostListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876BDA28297FC4B6006C638A /* PostListModel.swift */; };
16 | 876BDA2C297FC6C0006C638A /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876BDA2B297FC6C0006C638A /* HomeView.swift */; };
17 | 876BDA2E297FC718006C638A /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876BDA2D297FC718006C638A /* UserListView.swift */; };
18 | 878D63D82991102600A9EFB5 /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63D72991102600A9EFB5 /* PostDetailView.swift */; };
19 | 878D63DD2991107800A9EFB5 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63D92991107800A9EFB5 /* LoadingView.swift */; };
20 | 878D63DE2991107800A9EFB5 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63DA2991107800A9EFB5 /* EmptyView.swift */; };
21 | 878D63DF2991107800A9EFB5 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63DB2991107800A9EFB5 /* ErrorView.swift */; };
22 | 878D63E02991107800A9EFB5 /* ViewModelWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63DC2991107800A9EFB5 /* ViewModelWrapper.swift */; };
23 | 878D63E2299112DF00A9EFB5 /* PostItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63E1299112DF00A9EFB5 /* PostItemView.swift */; };
24 | 878D63E4299116A100A9EFB5 /* PostDetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63E3299116A100A9EFB5 /* PostDetailModel.swift */; };
25 | 878D63E629911CDA00A9EFB5 /* UserListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63E529911CDA00A9EFB5 /* UserListModel.swift */; };
26 | 878D63E829911E7200A9EFB5 /* UserItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63E729911E7200A9EFB5 /* UserItemView.swift */; };
27 | 878D63EA299120CF00A9EFB5 /* CommentItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878D63E9299120CF00A9EFB5 /* CommentItemView.swift */; };
28 | 87E7C64A2A87745400730164 /* IOSLocalProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87E7C6492A87745400730164 /* IOSLocalProperties.swift */; };
29 | /* End PBXBuildFile section */
30 |
31 | /* Begin PBXCopyFilesBuildPhase section */
32 | 7555FFB4242A642300829871 /* Embed Frameworks */ = {
33 | isa = PBXCopyFilesBuildPhase;
34 | buildActionMask = 2147483647;
35 | dstPath = "";
36 | dstSubfolderSpec = 10;
37 | files = (
38 | );
39 | name = "Embed Frameworks";
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXCopyFilesBuildPhase section */
43 |
44 | /* Begin PBXFileReference section */
45 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
46 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
47 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
48 | 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
49 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
50 | 8753C8F0297E8A44001EDB75 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; };
51 | 876BDA26297FC4AF006C638A /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = ""; };
52 | 876BDA28297FC4B6006C638A /* PostListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListModel.swift; sourceTree = ""; };
53 | 876BDA2B297FC6C0006C638A /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
54 | 876BDA2D297FC718006C638A /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; };
55 | 878D63D72991102600A9EFB5 /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = ""; };
56 | 878D63D92991107800A9EFB5 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; };
57 | 878D63DA2991107800A9EFB5 /* EmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = ""; };
58 | 878D63DB2991107800A9EFB5 /* ErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; };
59 | 878D63DC2991107800A9EFB5 /* ViewModelWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelWrapper.swift; sourceTree = ""; };
60 | 878D63E1299112DF00A9EFB5 /* PostItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostItemView.swift; sourceTree = ""; };
61 | 878D63E3299116A100A9EFB5 /* PostDetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailModel.swift; sourceTree = ""; };
62 | 878D63E529911CDA00A9EFB5 /* UserListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListModel.swift; sourceTree = ""; };
63 | 878D63E729911E7200A9EFB5 /* UserItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItemView.swift; sourceTree = ""; };
64 | 878D63E9299120CF00A9EFB5 /* CommentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentItemView.swift; sourceTree = ""; };
65 | 87E7C6492A87745400730164 /* IOSLocalProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSLocalProperties.swift; sourceTree = ""; };
66 | /* End PBXFileReference section */
67 |
68 | /* Begin PBXFrameworksBuildPhase section */
69 | 7555FF78242A565900829871 /* Frameworks */ = {
70 | isa = PBXFrameworksBuildPhase;
71 | buildActionMask = 2147483647;
72 | files = (
73 | );
74 | runOnlyForDeploymentPostprocessing = 0;
75 | };
76 | /* End PBXFrameworksBuildPhase section */
77 |
78 | /* Begin PBXGroup section */
79 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
83 | );
84 | path = "Preview Content";
85 | sourceTree = "";
86 | };
87 | 7555FF72242A565900829871 = {
88 | isa = PBXGroup;
89 | children = (
90 | 7555FF7D242A565900829871 /* iosApp */,
91 | 7555FF7C242A565900829871 /* Products */,
92 | 7555FFB0242A642200829871 /* Frameworks */,
93 | );
94 | sourceTree = "";
95 | };
96 | 7555FF7C242A565900829871 /* Products */ = {
97 | isa = PBXGroup;
98 | children = (
99 | 7555FF7B242A565900829871 /* iosApp.app */,
100 | );
101 | name = Products;
102 | sourceTree = "";
103 | };
104 | 7555FF7D242A565900829871 /* iosApp */ = {
105 | isa = PBXGroup;
106 | children = (
107 | 87E7C6482A87744400730164 /* System */,
108 | 8753C8EC297E77C9001EDB75 /* Components */,
109 | 8753C8E6297E715C001EDB75 /* Features */,
110 | 8753C8EF297E8A10001EDB75 /* Resources */,
111 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
112 | 7555FF8C242A565B00829871 /* Info.plist */,
113 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
114 | 058557D7273AAEEB004C7B11 /* Preview Content */,
115 | );
116 | path = iosApp;
117 | sourceTree = "";
118 | };
119 | 7555FFB0242A642200829871 /* Frameworks */ = {
120 | isa = PBXGroup;
121 | children = (
122 | );
123 | name = Frameworks;
124 | sourceTree = "";
125 | };
126 | 8753C8E6297E715C001EDB75 /* Features */ = {
127 | isa = PBXGroup;
128 | children = (
129 | 876BDA2A297FC6B4006C638A /* Home */,
130 | 876BDA1D297FC2AB006C638A /* Posts */,
131 | 876BDA1F297FC2B5006C638A /* Users */,
132 | );
133 | path = Features;
134 | sourceTree = "";
135 | };
136 | 8753C8EC297E77C9001EDB75 /* Components */ = {
137 | isa = PBXGroup;
138 | children = (
139 | 878D63DA2991107800A9EFB5 /* EmptyView.swift */,
140 | 878D63DB2991107800A9EFB5 /* ErrorView.swift */,
141 | 878D63D92991107800A9EFB5 /* LoadingView.swift */,
142 | 878D63DC2991107800A9EFB5 /* ViewModelWrapper.swift */,
143 | );
144 | path = Components;
145 | sourceTree = "";
146 | };
147 | 8753C8EF297E8A10001EDB75 /* Resources */ = {
148 | isa = PBXGroup;
149 | children = (
150 | 8753C8F0297E8A44001EDB75 /* Localizable.strings */,
151 | );
152 | path = Resources;
153 | sourceTree = "";
154 | };
155 | 876BDA1D297FC2AB006C638A /* Posts */ = {
156 | isa = PBXGroup;
157 | children = (
158 | 876BDA20297FC47B006C638A /* List */,
159 | 876BDA21297FC481006C638A /* Detail */,
160 | );
161 | path = Posts;
162 | sourceTree = "";
163 | };
164 | 876BDA1F297FC2B5006C638A /* Users */ = {
165 | isa = PBXGroup;
166 | children = (
167 | 876BDA22297FC48E006C638A /* List */,
168 | );
169 | path = Users;
170 | sourceTree = "";
171 | };
172 | 876BDA20297FC47B006C638A /* List */ = {
173 | isa = PBXGroup;
174 | children = (
175 | 876BDA26297FC4AF006C638A /* PostListView.swift */,
176 | 876BDA28297FC4B6006C638A /* PostListModel.swift */,
177 | 878D63E1299112DF00A9EFB5 /* PostItemView.swift */,
178 | );
179 | path = List;
180 | sourceTree = "";
181 | };
182 | 876BDA21297FC481006C638A /* Detail */ = {
183 | isa = PBXGroup;
184 | children = (
185 | 878D63E3299116A100A9EFB5 /* PostDetailModel.swift */,
186 | 878D63D72991102600A9EFB5 /* PostDetailView.swift */,
187 | 878D63E9299120CF00A9EFB5 /* CommentItemView.swift */,
188 | );
189 | path = Detail;
190 | sourceTree = "";
191 | };
192 | 876BDA22297FC48E006C638A /* List */ = {
193 | isa = PBXGroup;
194 | children = (
195 | 878D63E529911CDA00A9EFB5 /* UserListModel.swift */,
196 | 876BDA2D297FC718006C638A /* UserListView.swift */,
197 | 878D63E729911E7200A9EFB5 /* UserItemView.swift */,
198 | );
199 | path = List;
200 | sourceTree = "";
201 | };
202 | 876BDA2A297FC6B4006C638A /* Home */ = {
203 | isa = PBXGroup;
204 | children = (
205 | 876BDA2B297FC6C0006C638A /* HomeView.swift */,
206 | );
207 | path = Home;
208 | sourceTree = "";
209 | };
210 | 87E7C6482A87744400730164 /* System */ = {
211 | isa = PBXGroup;
212 | children = (
213 | 87E7C6492A87745400730164 /* IOSLocalProperties.swift */,
214 | );
215 | path = System;
216 | sourceTree = "";
217 | };
218 | /* End PBXGroup section */
219 |
220 | /* Begin PBXNativeTarget section */
221 | 7555FF7A242A565900829871 /* iosApp */ = {
222 | isa = PBXNativeTarget;
223 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
224 | buildPhases = (
225 | 7555FFB5242A651A00829871 /* ShellScript */,
226 | 7555FF77242A565900829871 /* Sources */,
227 | 7555FF78242A565900829871 /* Frameworks */,
228 | 7555FF79242A565900829871 /* Resources */,
229 | 7555FFB4242A642300829871 /* Embed Frameworks */,
230 | );
231 | buildRules = (
232 | );
233 | dependencies = (
234 | );
235 | name = iosApp;
236 | productName = iosApp;
237 | productReference = 7555FF7B242A565900829871 /* iosApp.app */;
238 | productType = "com.apple.product-type.application";
239 | };
240 | /* End PBXNativeTarget section */
241 |
242 | /* Begin PBXProject section */
243 | 7555FF73242A565900829871 /* Project object */ = {
244 | isa = PBXProject;
245 | attributes = {
246 | LastSwiftUpdateCheck = 1130;
247 | LastUpgradeCheck = 1130;
248 | ORGANIZATIONNAME = orgName;
249 | TargetAttributes = {
250 | 7555FF7A242A565900829871 = {
251 | CreatedOnToolsVersion = 11.3.1;
252 | };
253 | };
254 | };
255 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */;
256 | compatibilityVersion = "Xcode 9.3";
257 | developmentRegion = en;
258 | hasScannedForEncodings = 0;
259 | knownRegions = (
260 | en,
261 | Base,
262 | );
263 | mainGroup = 7555FF72242A565900829871;
264 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
265 | projectDirPath = "";
266 | projectRoot = "";
267 | targets = (
268 | 7555FF7A242A565900829871 /* iosApp */,
269 | );
270 | };
271 | /* End PBXProject section */
272 |
273 | /* Begin PBXResourcesBuildPhase section */
274 | 7555FF79242A565900829871 /* Resources */ = {
275 | isa = PBXResourcesBuildPhase;
276 | buildActionMask = 2147483647;
277 | files = (
278 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
279 | 8753C8F1297E8A44001EDB75 /* Localizable.strings in Resources */,
280 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
281 | );
282 | runOnlyForDeploymentPostprocessing = 0;
283 | };
284 | /* End PBXResourcesBuildPhase section */
285 |
286 | /* Begin PBXShellScriptBuildPhase section */
287 | 7555FFB5242A651A00829871 /* ShellScript */ = {
288 | isa = PBXShellScriptBuildPhase;
289 | buildActionMask = 2147483647;
290 | files = (
291 | );
292 | inputFileListPaths = (
293 | );
294 | inputPaths = (
295 | );
296 | outputFileListPaths = (
297 | );
298 | outputPaths = (
299 | );
300 | runOnlyForDeploymentPostprocessing = 0;
301 | shellPath = /bin/sh;
302 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
303 | };
304 | /* End PBXShellScriptBuildPhase section */
305 |
306 | /* Begin PBXSourcesBuildPhase section */
307 | 7555FF77242A565900829871 /* Sources */ = {
308 | isa = PBXSourcesBuildPhase;
309 | buildActionMask = 2147483647;
310 | files = (
311 | 878D63E02991107800A9EFB5 /* ViewModelWrapper.swift in Sources */,
312 | 876BDA2E297FC718006C638A /* UserListView.swift in Sources */,
313 | 878D63EA299120CF00A9EFB5 /* CommentItemView.swift in Sources */,
314 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
315 | 876BDA29297FC4B6006C638A /* PostListModel.swift in Sources */,
316 | 878D63D82991102600A9EFB5 /* PostDetailView.swift in Sources */,
317 | 87E7C64A2A87745400730164 /* IOSLocalProperties.swift in Sources */,
318 | 878D63DE2991107800A9EFB5 /* EmptyView.swift in Sources */,
319 | 878D63E4299116A100A9EFB5 /* PostDetailModel.swift in Sources */,
320 | 876BDA2C297FC6C0006C638A /* HomeView.swift in Sources */,
321 | 878D63DD2991107800A9EFB5 /* LoadingView.swift in Sources */,
322 | 878D63DF2991107800A9EFB5 /* ErrorView.swift in Sources */,
323 | 878D63E829911E7200A9EFB5 /* UserItemView.swift in Sources */,
324 | 878D63E629911CDA00A9EFB5 /* UserListModel.swift in Sources */,
325 | 878D63E2299112DF00A9EFB5 /* PostItemView.swift in Sources */,
326 | 876BDA27297FC4AF006C638A /* PostListView.swift in Sources */,
327 | );
328 | runOnlyForDeploymentPostprocessing = 0;
329 | };
330 | /* End PBXSourcesBuildPhase section */
331 |
332 | /* Begin XCBuildConfiguration section */
333 | 7555FFA3242A565B00829871 /* Debug */ = {
334 | isa = XCBuildConfiguration;
335 | buildSettings = {
336 | ALWAYS_SEARCH_USER_PATHS = NO;
337 | CLANG_ANALYZER_NONNULL = YES;
338 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
339 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
340 | CLANG_CXX_LIBRARY = "libc++";
341 | CLANG_ENABLE_MODULES = YES;
342 | CLANG_ENABLE_OBJC_ARC = YES;
343 | CLANG_ENABLE_OBJC_WEAK = YES;
344 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
345 | CLANG_WARN_BOOL_CONVERSION = YES;
346 | CLANG_WARN_COMMA = YES;
347 | CLANG_WARN_CONSTANT_CONVERSION = YES;
348 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
349 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
350 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
351 | CLANG_WARN_EMPTY_BODY = YES;
352 | CLANG_WARN_ENUM_CONVERSION = YES;
353 | CLANG_WARN_INFINITE_RECURSION = YES;
354 | CLANG_WARN_INT_CONVERSION = YES;
355 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
356 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
357 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
358 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
359 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
360 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
361 | CLANG_WARN_STRICT_PROTOTYPES = YES;
362 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
363 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
364 | CLANG_WARN_UNREACHABLE_CODE = YES;
365 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
366 | COPY_PHASE_STRIP = NO;
367 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
368 | ENABLE_STRICT_OBJC_MSGSEND = YES;
369 | ENABLE_TESTABILITY = YES;
370 | GCC_C_LANGUAGE_STANDARD = gnu11;
371 | GCC_DYNAMIC_NO_PIC = NO;
372 | GCC_NO_COMMON_BLOCKS = YES;
373 | GCC_OPTIMIZATION_LEVEL = 0;
374 | GCC_PREPROCESSOR_DEFINITIONS = (
375 | "DEBUG=1",
376 | "$(inherited)",
377 | );
378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
380 | GCC_WARN_UNDECLARED_SELECTOR = YES;
381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
382 | GCC_WARN_UNUSED_FUNCTION = YES;
383 | GCC_WARN_UNUSED_VARIABLE = YES;
384 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
385 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
386 | MTL_FAST_MATH = YES;
387 | ONLY_ACTIVE_ARCH = YES;
388 | SDKROOT = iphoneos;
389 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
390 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
391 | };
392 | name = Debug;
393 | };
394 | 7555FFA4242A565B00829871 /* Release */ = {
395 | isa = XCBuildConfiguration;
396 | buildSettings = {
397 | ALWAYS_SEARCH_USER_PATHS = NO;
398 | CLANG_ANALYZER_NONNULL = YES;
399 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
400 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
401 | CLANG_CXX_LIBRARY = "libc++";
402 | CLANG_ENABLE_MODULES = YES;
403 | CLANG_ENABLE_OBJC_ARC = YES;
404 | CLANG_ENABLE_OBJC_WEAK = YES;
405 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
406 | CLANG_WARN_BOOL_CONVERSION = YES;
407 | CLANG_WARN_COMMA = YES;
408 | CLANG_WARN_CONSTANT_CONVERSION = YES;
409 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
410 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
411 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
412 | CLANG_WARN_EMPTY_BODY = YES;
413 | CLANG_WARN_ENUM_CONVERSION = YES;
414 | CLANG_WARN_INFINITE_RECURSION = YES;
415 | CLANG_WARN_INT_CONVERSION = YES;
416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
420 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
422 | CLANG_WARN_STRICT_PROTOTYPES = YES;
423 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
424 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
425 | CLANG_WARN_UNREACHABLE_CODE = YES;
426 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
427 | COPY_PHASE_STRIP = NO;
428 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
429 | ENABLE_NS_ASSERTIONS = NO;
430 | ENABLE_STRICT_OBJC_MSGSEND = YES;
431 | GCC_C_LANGUAGE_STANDARD = gnu11;
432 | GCC_NO_COMMON_BLOCKS = YES;
433 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
434 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
435 | GCC_WARN_UNDECLARED_SELECTOR = YES;
436 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
437 | GCC_WARN_UNUSED_FUNCTION = YES;
438 | GCC_WARN_UNUSED_VARIABLE = YES;
439 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
440 | MTL_ENABLE_DEBUG_INFO = NO;
441 | MTL_FAST_MATH = YES;
442 | SDKROOT = iphoneos;
443 | SWIFT_COMPILATION_MODE = wholemodule;
444 | SWIFT_OPTIMIZATION_LEVEL = "-O";
445 | VALIDATE_PRODUCT = YES;
446 | };
447 | name = Release;
448 | };
449 | 7555FFA6242A565B00829871 /* Debug */ = {
450 | isa = XCBuildConfiguration;
451 | buildSettings = {
452 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
453 | CODE_SIGN_STYLE = Automatic;
454 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
455 | ENABLE_PREVIEWS = YES;
456 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
457 | INFOPLIST_FILE = iosApp/Info.plist;
458 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
459 | LD_RUNPATH_SEARCH_PATHS = (
460 | "$(inherited)",
461 | "@executable_path/Frameworks",
462 | );
463 | OTHER_LDFLAGS = (
464 | "$(inherited)",
465 | "-framework",
466 | shared,
467 | );
468 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp;
469 | PRODUCT_NAME = "$(TARGET_NAME)";
470 | SWIFT_VERSION = 5.0;
471 | TARGETED_DEVICE_FAMILY = "1,2";
472 | };
473 | name = Debug;
474 | };
475 | 7555FFA7242A565B00829871 /* Release */ = {
476 | isa = XCBuildConfiguration;
477 | buildSettings = {
478 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
479 | CODE_SIGN_STYLE = Automatic;
480 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
481 | ENABLE_PREVIEWS = YES;
482 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
483 | INFOPLIST_FILE = iosApp/Info.plist;
484 | IPHONEOS_DEPLOYMENT_TARGET = 15.6;
485 | LD_RUNPATH_SEARCH_PATHS = (
486 | "$(inherited)",
487 | "@executable_path/Frameworks",
488 | );
489 | OTHER_LDFLAGS = (
490 | "$(inherited)",
491 | "-framework",
492 | shared,
493 | );
494 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp;
495 | PRODUCT_NAME = "$(TARGET_NAME)";
496 | SWIFT_VERSION = 5.0;
497 | TARGETED_DEVICE_FAMILY = "1,2";
498 | };
499 | name = Release;
500 | };
501 | /* End XCBuildConfiguration section */
502 |
503 | /* Begin XCConfigurationList section */
504 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = {
505 | isa = XCConfigurationList;
506 | buildConfigurations = (
507 | 7555FFA3242A565B00829871 /* Debug */,
508 | 7555FFA4242A565B00829871 /* Release */,
509 | );
510 | defaultConfigurationIsVisible = 0;
511 | defaultConfigurationName = Release;
512 | };
513 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = {
514 | isa = XCConfigurationList;
515 | buildConfigurations = (
516 | 7555FFA6242A565B00829871 /* Debug */,
517 | 7555FFA7242A565B00829871 /* Release */,
518 | );
519 | defaultConfigurationIsVisible = 0;
520 | defaultConfigurationName = Release;
521 | };
522 | /* End XCConfigurationList section */
523 | };
524 | rootObject = 7555FF73242A565900829871 /* Project object */;
525 | }
526 |
--------------------------------------------------------------------------------