├── iosContactsMP
├── iosContactsMP
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── iOSApp.swift
│ ├── ContentView.swift
│ ├── ComposeView.swift
│ └── Info.plist
└── iosContactsMP.xcodeproj
│ └── project.pbxproj
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── androidContactsMP
├── src
│ └── main
│ │ ├── res
│ │ └── values
│ │ │ └── styles.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── plcoding
│ │ └── contactscomposemultiplatform
│ │ └── android
│ │ └── MainActivity.kt
└── build.gradle.kts
├── .gitignore
├── shared
├── src
│ ├── commonMain
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── plcoding
│ │ │ │ └── contactscomposemultiplatform
│ │ │ │ ├── core
│ │ │ │ ├── data
│ │ │ │ │ ├── DatabaseDriverFactory.kt
│ │ │ │ │ └── ImageStorage.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── ImagePickerFactory.kt
│ │ │ │ │ ├── BitmapUtil.kt
│ │ │ │ │ ├── ContactsTheme.kt
│ │ │ │ │ ├── ImagePicker.kt
│ │ │ │ │ └── BottomSheetFromWish.kt
│ │ │ │ ├── di
│ │ │ │ └── AppModule.kt
│ │ │ │ ├── contacts
│ │ │ │ ├── domain
│ │ │ │ │ ├── Contact.kt
│ │ │ │ │ ├── ContactDataSource.kt
│ │ │ │ │ └── ContactValidator.kt
│ │ │ │ ├── data
│ │ │ │ │ ├── ContactMapper.kt
│ │ │ │ │ └── SqlDelightContactDataSource.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── ContactListState.kt
│ │ │ │ │ ├── ContactListEvent.kt
│ │ │ │ │ ├── components
│ │ │ │ │ ├── ContactListItem.kt
│ │ │ │ │ ├── ContactPreviewItem.kt
│ │ │ │ │ ├── RecentlyAddedContacts.kt
│ │ │ │ │ ├── ContactPhoto.kt
│ │ │ │ │ ├── AddContactSheet.kt
│ │ │ │ │ └── ContactDetailSheet.kt
│ │ │ │ │ ├── ContactListScreen.kt
│ │ │ │ │ └── ContactListViewModel.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Color.kt
│ │ │ │ └── App.kt
│ │ └── sqldelight
│ │ │ └── database
│ │ │ └── contact.sq
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── plcoding
│ │ │ └── contactscomposemultiplatform
│ │ │ ├── core
│ │ │ ├── data
│ │ │ │ ├── DatabaseDriverFactory.kt
│ │ │ │ └── ImageStorage.kt
│ │ │ └── presentation
│ │ │ │ ├── ImagePickerFactory.kt
│ │ │ │ ├── rememberBitmapFromBytes.kt
│ │ │ │ ├── ContactsTheme.kt
│ │ │ │ └── ImagePicker.kt
│ │ │ ├── di
│ │ │ └── AppModule.kt
│ │ │ └── MainViewController.kt
│ └── androidMain
│ │ └── kotlin
│ │ └── com
│ │ └── plcoding
│ │ └── contactscomposemultiplatform
│ │ ├── core
│ │ ├── presentation
│ │ │ ├── ImagePickerFactory.kt
│ │ │ ├── rememberBitmapFromBytes.kt
│ │ │ ├── ImagePicker.kt
│ │ │ └── ContactsTheme.kt
│ │ └── data
│ │ │ ├── DatabaseDriverFactory.kt
│ │ │ └── ImageStorage.kt
│ │ └── di
│ │ └── AppModule.kt
└── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
├── gradlew.bat
└── gradlew
/iosContactsMP/iosContactsMP/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philipplackner/ContactsComposeMultiplatform/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/androidContactsMP/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | .DS_Store
5 | build
6 | captures
7 | .externalNativeBuild
8 | .cxx
9 | local.properties
10 | xcuserdata
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/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 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 25 11:57:55 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/DatabaseDriverFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | import com.squareup.sqldelight.db.SqlDriver
4 |
5 | expect class DatabaseDriverFactory {
6 | fun create(): SqlDriver
7 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import shared
3 |
4 | struct ContentView: View {
5 | var body: some View {
6 | ComposeView()
7 | }
8 | }
9 |
10 | struct ContentView_Previews: PreviewProvider {
11 | static var previews: some View {
12 | ContentView()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.di
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactDataSource
4 |
5 | expect class AppModule {
6 | val contactDataSource: ContactDataSource
7 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePickerFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | expect class ImagePickerFactory {
6 |
7 | @Composable
8 | fun createPicker(): ImagePicker
9 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/ImageStorage.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | expect class ImageStorage {
4 | suspend fun saveImage(bytes: ByteArray): String
5 | suspend fun getImage(fileName: String): ByteArray?
6 | suspend fun deleteImage(fileName: String)
7 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/BitmapUtil.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.ImageBitmap
5 |
6 | @Composable
7 | expect fun rememberBitmapFromBytes(bytes: ByteArray?): ImageBitmap?
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/domain/Contact.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.domain
2 |
3 | data class Contact(
4 | val id: Long?,
5 | val firstName: String,
6 | val lastName: String,
7 | val email: String,
8 | val phoneNumber: String,
9 | val photoBytes: ByteArray?
10 | )
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ContactsTheme.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | expect fun ContactsTheme(
7 | darkTheme: Boolean,
8 | dynamicColor: Boolean,
9 | content: @Composable () -> Unit
10 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePicker.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | expect class ImagePicker {
6 |
7 | @Composable
8 | fun registerPicker(onImagePicked: (ByteArray) -> Unit)
9 |
10 | fun pickImage()
11 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/domain/ContactDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.domain
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface ContactDataSource {
6 | fun getContacts(): Flow>
7 | fun getRecentContacts(amount: Int): Flow>
8 | suspend fun insertContact(contact: Contact)
9 | suspend fun deleteContact(id: Long)
10 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/DatabaseDriverFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | import com.plcoding.contactscomposemultiplatform.database.ContactDatabase
4 | import com.squareup.sqldelight.db.SqlDriver
5 | import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
6 |
7 | actual class DatabaseDriverFactory {
8 | actual fun create(): SqlDriver {
9 | return NativeSqliteDriver(ContactDatabase.Schema, "contact.db")
10 | }
11 | }
--------------------------------------------------------------------------------
/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
13 | kotlin.mpp.androidSourceSetLayoutVersion=2
14 |
15 | org.jetbrains.compose.experimental.uikit.enabled=true
16 | kotlin.native.cacheKind=none
17 |
18 | kotlin.version=1.8.20
19 | agp.version=8.0.1
20 | compose.version=1.4.0
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePickerFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import platform.UIKit.UIViewController
6 |
7 | actual class ImagePickerFactory(
8 | private val rootController: UIViewController
9 | ){
10 |
11 | @Composable
12 | actual fun createPicker(): ImagePicker {
13 | return remember {
14 | ImagePicker(rootController)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/ComposeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComposeView.swift
3 | // iosContactsMP
4 | //
5 | // Created by Philipp Lackner on 25.06.23.
6 | // Copyright © 2023 orgName. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import shared
11 | import SwiftUI
12 |
13 | struct ComposeView: UIViewControllerRepresentable {
14 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
15 | }
16 |
17 | func makeUIViewController(context: Context) -> some UIViewController {
18 | MainViewControllerKt.MainViewController()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePickerFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.activity.ComponentActivity
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.platform.LocalContext
7 |
8 | actual class ImagePickerFactory {
9 |
10 | @Composable
11 | actual fun createPicker(): ImagePicker {
12 | val activity = LocalContext.current as ComponentActivity
13 | return remember(activity) {
14 | ImagePicker(activity)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/DatabaseDriverFactory.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | import android.content.Context
4 | import com.plcoding.contactscomposemultiplatform.database.ContactDatabase
5 | import com.squareup.sqldelight.android.AndroidSqliteDriver
6 | import com.squareup.sqldelight.db.SqlDriver
7 |
8 | actual class DatabaseDriverFactory(
9 | private val context: Context
10 | ) {
11 | actual fun create(): SqlDriver {
12 | return AndroidSqliteDriver(
13 | ContactDatabase.Schema,
14 | context,
15 | "contact.db"
16 | )
17 | }
18 | }
--------------------------------------------------------------------------------
/androidContactsMP/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/data/ContactMapper.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.data
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
4 | import com.plcoding.contactscomposemultiplatform.core.data.ImageStorage
5 | import database.ContactEntity
6 |
7 | suspend fun ContactEntity.toContact(imageStorage: ImageStorage): Contact {
8 | return Contact(
9 | id = id,
10 | firstName = firstName,
11 | lastName = lastName,
12 | email = email,
13 | phoneNumber = phoneNumber,
14 | photoBytes = imagePath?.let { imageStorage.getImage(it) }
15 | )
16 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/rememberBitmapFromBytes.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import android.graphics.BitmapFactory
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.remember
6 | import androidx.compose.ui.graphics.ImageBitmap
7 | import androidx.compose.ui.graphics.asImageBitmap
8 |
9 | @Composable
10 | actual fun rememberBitmapFromBytes(bytes: ByteArray?): ImageBitmap? {
11 | return remember(bytes) {
12 | if(bytes != null) {
13 | BitmapFactory.decodeByteArray(bytes, 0, bytes.size).asImageBitmap()
14 | } else {
15 | null
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/ContactListState.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
4 |
5 | data class ContactListState(
6 | val contacts: List = emptyList(),
7 | val recentlyAddedContacts: List = emptyList(),
8 | val selectedContact: Contact? = null,
9 | val isAddContactSheetOpen: Boolean = false,
10 | val isSelectedContactSheetOpen: Boolean = false,
11 | val firstNameError: String? = null,
12 | val lastNameError: String? = null,
13 | val emailError: String? = null,
14 | val phoneNumberError: String? = null,
15 | )
16 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/rememberBitmapFromBytes.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.remember
5 | import androidx.compose.ui.graphics.ImageBitmap
6 | import androidx.compose.ui.graphics.asComposeImageBitmap
7 | import org.jetbrains.skia.Bitmap
8 | import org.jetbrains.skia.Image
9 |
10 | @Composable
11 | actual fun rememberBitmapFromBytes(bytes: ByteArray?): ImageBitmap? {
12 | return remember(bytes) {
13 | if(bytes != null) {
14 | Bitmap.makeFromImage(
15 | Image.makeFromEncoded(bytes)
16 | ).asComposeImageBitmap()
17 | } else {
18 | null
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ContactsTheme.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import com.plcoding.contactscomposemultiplatform.ui.theme.DarkColorScheme
6 | import com.plcoding.contactscomposemultiplatform.ui.theme.LightColorScheme
7 | import com.plcoding.contactscomposemultiplatform.ui.theme.Typography
8 |
9 | @Composable
10 | actual fun ContactsTheme(
11 | darkTheme: Boolean,
12 | dynamicColor: Boolean,
13 | content: @Composable () -> Unit
14 | ) {
15 | MaterialTheme(
16 | colorScheme = if(darkTheme) DarkColorScheme else LightColorScheme,
17 | typography = Typography,
18 | content = content
19 | )
20 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.di
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.data.SqlDelightContactDataSource
4 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactDataSource
5 | import com.plcoding.contactscomposemultiplatform.core.data.DatabaseDriverFactory
6 | import com.plcoding.contactscomposemultiplatform.core.data.ImageStorage
7 | import com.plcoding.contactscomposemultiplatform.database.ContactDatabase
8 |
9 | actual class AppModule {
10 |
11 | actual val contactDataSource: ContactDataSource by lazy {
12 | SqlDelightContactDataSource(
13 | db = ContactDatabase(
14 | driver = DatabaseDriverFactory().create()
15 | ),
16 | imageStorage = ImageStorage()
17 | )
18 | }
19 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/sqldelight/database/contact.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE contactEntity (
2 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
3 | firstName TEXT NOT NULL,
4 | lastName TEXT NOT NULL,
5 | phoneNumber TEXT NOT NULL,
6 | email TEXT NOT NULL,
7 | createdAt INTEGER NOT NULL,
8 | imagePath TEXT
9 | );
10 |
11 | getContacts:
12 | SELECT *
13 | FROM contactEntity
14 | ORDER BY firstName ASC;
15 |
16 | getRecentContacts:
17 | SELECT *
18 | FROM contactEntity
19 | ORDER BY createdAt DESC
20 | LIMIT :amount;
21 |
22 | insertContactEntity:
23 | INSERT OR REPLACE
24 | INTO contactEntity(
25 | id,
26 | firstName,
27 | lastName,
28 | phoneNumber,
29 | email,
30 | createdAt,
31 | imagePath
32 | ) VALUES (?, ?, ?, ?, ? ,?, ?);
33 |
34 | deleteContact:
35 | DELETE FROM contactEntity
36 | WHERE id = :id;
37 |
38 | getContactById:
39 | SELECT *
40 | FROM contactEntity
41 | WHERE id = :id;
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform
2 |
3 | import androidx.compose.ui.interop.LocalUIViewController
4 | import androidx.compose.ui.window.ComposeUIViewController
5 | import com.plcoding.contactscomposemultiplatform.core.presentation.ImagePickerFactory
6 | import com.plcoding.contactscomposemultiplatform.di.AppModule
7 | import platform.UIKit.UIScreen
8 | import platform.UIKit.UIUserInterfaceStyle
9 |
10 | fun MainViewController() = ComposeUIViewController {
11 | val isDarkTheme =
12 | UIScreen.mainScreen.traitCollection.userInterfaceStyle ==
13 | UIUserInterfaceStyle.UIUserInterfaceStyleDark
14 | App(
15 | darkTheme = isDarkTheme,
16 | dynamicColor = false,
17 | appModule = AppModule(),
18 | imagePicker = ImagePickerFactory(LocalUIViewController.current).createPicker()
19 | )
20 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.di
2 |
3 | import android.content.Context
4 | import com.plcoding.contactscomposemultiplatform.contacts.data.SqlDelightContactDataSource
5 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactDataSource
6 | import com.plcoding.contactscomposemultiplatform.core.data.DatabaseDriverFactory
7 | import com.plcoding.contactscomposemultiplatform.core.data.ImageStorage
8 | import com.plcoding.contactscomposemultiplatform.database.ContactDatabase
9 |
10 | actual class AppModule(
11 | private val context: Context
12 | ) {
13 |
14 | actual val contactDataSource: ContactDataSource by lazy {
15 | SqlDelightContactDataSource(
16 | db = ContactDatabase(
17 | driver = DatabaseDriverFactory(context).create(),
18 | ),
19 | imageStorage = ImageStorage(context)
20 | )
21 | }
22 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/ContactListEvent.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
4 |
5 | sealed interface ContactListEvent {
6 | object OnAddNewContactClick: ContactListEvent
7 | object DismissContact: ContactListEvent
8 | data class OnFirstNameChanged(val value: String): ContactListEvent
9 | data class OnLastNameChanged(val value: String): ContactListEvent
10 | data class OnEmailChanged(val value: String): ContactListEvent
11 | data class OnPhoneNumberChanged(val value: String): ContactListEvent
12 | class OnPhotoPicked(val bytes: ByteArray): ContactListEvent
13 | object OnAddPhotoClicked: ContactListEvent
14 | object SaveContact: ContactListEvent
15 | data class SelectContact(val contact: Contact): ContactListEvent
16 | data class EditContact(val contact: Contact): ContactListEvent
17 | object DeleteContact: ContactListEvent
18 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | }
8 |
9 | plugins {
10 | val kotlinVersion = extra["kotlin.version"] as String
11 | val agpVersion = extra["agp.version"] as String
12 | val composeVersion = extra["compose.version"] as String
13 |
14 | kotlin("jvm").version(kotlinVersion)
15 | kotlin("multiplatform").version(kotlinVersion)
16 | kotlin("android").version(kotlinVersion)
17 |
18 | id("com.android.application").version(agpVersion)
19 | id("com.android.library").version(agpVersion)
20 |
21 | id("org.jetbrains.compose").version(composeVersion)
22 | }
23 | }
24 |
25 | dependencyResolutionManagement {
26 | repositories {
27 | google()
28 | mavenCentral()
29 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
30 | }
31 | }
32 |
33 | rootProject.name = "ContactsComposeMultiplatform"
34 | include(":androidContactsMP")
35 | include(":shared")
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePicker.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.activity.ComponentActivity
4 | import androidx.activity.compose.rememberLauncherForActivityResult
5 | import androidx.activity.result.ActivityResultLauncher
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.runtime.Composable
8 |
9 | actual class ImagePicker(
10 | private val activity: ComponentActivity
11 | ) {
12 | private lateinit var getContent: ActivityResultLauncher
13 |
14 | @Composable
15 | actual fun registerPicker(onImagePicked: (ByteArray) -> Unit) {
16 | getContent = rememberLauncherForActivityResult(
17 | ActivityResultContracts.GetContent()
18 | ) { uri ->
19 | uri?.let {
20 | activity.contentResolver.openInputStream(uri)?.use {
21 | onImagePicked(it.readBytes())
22 | }
23 | }
24 | }
25 | }
26 |
27 | actual fun pickImage() {
28 | getContent.launch("image/*")
29 | }
30 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/ImageStorage.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | import android.content.Context
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import java.util.UUID
7 |
8 | actual class ImageStorage(
9 | private val context: Context
10 | ) {
11 | actual suspend fun saveImage(bytes: ByteArray): String {
12 | return withContext(Dispatchers.IO) {
13 | val fileName = UUID.randomUUID().toString() + ".jpg"
14 | context.openFileOutput(fileName, Context.MODE_PRIVATE).use { outputStream ->
15 | outputStream.write(bytes)
16 | }
17 | fileName
18 | }
19 | }
20 |
21 | actual suspend fun getImage(fileName: String): ByteArray? {
22 | return withContext(Dispatchers.IO) {
23 | context.openFileInput(fileName).use { inputStream ->
24 | inputStream.readBytes()
25 | }
26 | }
27 | }
28 |
29 | actual suspend fun deleteImage(fileName: String) {
30 | return withContext(Dispatchers.IO) {
31 | context.deleteFile(fileName)
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/ContactListItem.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
13 |
14 | @Composable
15 | fun ContactListItem(
16 | contact: Contact,
17 | modifier: Modifier = Modifier
18 | ) {
19 | Row(
20 | modifier = modifier,
21 | verticalAlignment = Alignment.CenterVertically
22 | ) {
23 | ContactPhoto(
24 | contact = contact,
25 | modifier = Modifier.size(50.dp)
26 | )
27 |
28 | Spacer(Modifier.width(16.dp))
29 |
30 | Text(
31 | text = "${contact.firstName} ${contact.lastName}",
32 | modifier = Modifier.weight(1f)
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/domain/ContactValidator.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.domain
2 |
3 | object ContactValidator {
4 |
5 | fun validateContact(contact: Contact): ValidationResult {
6 | var result = ValidationResult()
7 |
8 | if(contact.firstName.isBlank()) {
9 | result = result.copy(firstNameError = "The first name can't be empty.")
10 | }
11 |
12 | if(contact.lastName.isBlank()) {
13 | result = result.copy(lastNameError = "The last name can't be empty.")
14 | }
15 |
16 | val emailRegex = Regex("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}\$")
17 | if(!emailRegex.matches(contact.email)) {
18 | result = result.copy(emailError = "This is not a valid email.")
19 | }
20 |
21 | if(contact.phoneNumber.isBlank()) {
22 | result = result.copy(phoneNumberError = "The phone number can't be empty.")
23 | }
24 |
25 | return result
26 | }
27 |
28 | data class ValidationResult(
29 | val firstNameError: String? = null,
30 | val lastNameError: String? = null,
31 | val emailError: String? = null,
32 | val phoneNumberError: String? = null,
33 | )
34 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/ContactPreviewItem.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
14 |
15 | @Composable
16 | fun ContactPreviewItem(
17 | contact: Contact,
18 | onClick: () -> Unit,
19 | modifier: Modifier = Modifier
20 | ) {
21 | Column(
22 | modifier = modifier
23 | .clickable(onClick = onClick),
24 | horizontalAlignment = Alignment.CenterHorizontally
25 | ) {
26 | ContactPhoto(
27 | contact = contact,
28 | modifier = Modifier.size(50.dp)
29 | )
30 | Spacer(Modifier.height(8.dp))
31 | Text(
32 | text = contact.firstName,
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/androidContactsMP/src/main/java/com/plcoding/contactscomposemultiplatform/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.material.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.platform.LocalContext
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import com.plcoding.contactscomposemultiplatform.App
14 | import com.plcoding.contactscomposemultiplatform.core.presentation.ImagePickerFactory
15 | import com.plcoding.contactscomposemultiplatform.di.AppModule
16 |
17 | class MainActivity : ComponentActivity() {
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | setContent {
21 | App(
22 | darkTheme = isSystemInDarkTheme(),
23 | dynamicColor = true,
24 | appModule = AppModule(LocalContext.current.applicationContext),
25 | imagePicker = ImagePickerFactory().createPicker()
26 | )
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/androidContactsMP/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | kotlin("android")
4 | id("org.jetbrains.compose")
5 | }
6 |
7 | android {
8 | namespace = "com.plcoding.contactscomposemultiplatform.android"
9 | compileSdk = 33
10 | defaultConfig {
11 | applicationId = "com.plcoding.contactscomposemultiplatform.android"
12 | minSdk = 24
13 | targetSdk = 33
14 | versionCode = 1
15 | versionName = "1.0"
16 | }
17 | buildFeatures {
18 | compose = true
19 | }
20 | composeOptions {
21 | kotlinCompilerExtensionVersion = "1.4.4"
22 | }
23 | packagingOptions {
24 | resources {
25 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
26 | }
27 | }
28 | buildTypes {
29 | getByName("release") {
30 | isMinifyEnabled = false
31 | }
32 | }
33 | compileOptions {
34 | sourceCompatibility = JavaVersion.VERSION_17
35 | targetCompatibility = JavaVersion.VERSION_17
36 | }
37 | kotlinOptions {
38 | jvmTarget = "17"
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation(project(":shared"))
44 | implementation("androidx.compose.ui:ui:1.4.0")
45 | implementation("androidx.compose.ui:ui-tooling:1.4.0")
46 | implementation("androidx.compose.ui:ui-tooling-preview:1.4.0")
47 | implementation("androidx.compose.foundation:foundation:1.4.0")
48 | implementation("androidx.compose.material:material:1.4.0")
49 | implementation("androidx.activity:activity-compose:1.7.0")
50 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIRequiredDeviceCapabilities
29 |
30 | armv7
31 |
32 | UISupportedInterfaceOrientations
33 |
34 | UIInterfaceOrientationPortrait
35 | UIInterfaceOrientationLandscapeLeft
36 | UIInterfaceOrientationLandscapeRight
37 |
38 | UISupportedInterfaceOrientations~ipad
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationPortraitUpsideDown
42 | UIInterfaceOrientationLandscapeLeft
43 | UIInterfaceOrientationLandscapeRight
44 |
45 | UILaunchScreen
46 |
47 |
48 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/BottomSheetFromWish.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.animation.slideInVertically
6 | import androidx.compose.animation.slideOutVertically
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.ColumnScope
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.rememberScrollState
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.foundation.verticalScroll
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.clip
18 | import androidx.compose.ui.unit.dp
19 |
20 | @Composable
21 | fun BottomSheetFromWish(
22 | visible: Boolean,
23 | modifier: Modifier = Modifier,
24 | content: @Composable ColumnScope.() -> Unit
25 | ) {
26 | AnimatedVisibility(
27 | visible = visible,
28 | enter = slideInVertically(
29 | animationSpec = tween(durationMillis = 300),
30 | initialOffsetY = { it }
31 | ),
32 | exit = slideOutVertically(
33 | animationSpec = tween(durationMillis = 300),
34 | targetOffsetY = { it }
35 | ),
36 | ) {
37 | Column(
38 | modifier = modifier
39 | .clip(
40 | RoundedCornerShape(
41 | topStart = 30.dp,
42 | topEnd = 30.dp
43 | )
44 | )
45 | .background(MaterialTheme.colorScheme.surface)
46 | .padding(16.dp)
47 | .verticalScroll(rememberScrollState())
48 | ) {
49 | content()
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ContactsTheme.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.material3.lightColorScheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.SideEffect
11 | import androidx.compose.ui.graphics.toArgb
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.platform.LocalView
14 | import androidx.core.view.WindowCompat
15 | import com.plcoding.contactscomposemultiplatform.ui.theme.DarkColorScheme
16 | import com.plcoding.contactscomposemultiplatform.ui.theme.LightColorScheme
17 | import com.plcoding.contactscomposemultiplatform.ui.theme.Typography
18 |
19 | @Composable
20 | actual fun ContactsTheme(
21 | darkTheme: Boolean,
22 | dynamicColor: Boolean,
23 | content: @Composable () -> Unit
24 | ) {
25 | val colorScheme = when {
26 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
27 | val context = LocalContext.current
28 | if(darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
29 | }
30 | darkTheme -> DarkColorScheme
31 | else -> LightColorScheme
32 | }
33 |
34 | val view = LocalView.current
35 | if(!view.isInEditMode) {
36 | SideEffect {
37 | val window = (view.context as Activity).window
38 | window.statusBarColor = colorScheme.primary.toArgb()
39 | WindowCompat.getInsetsController(
40 | window,
41 | view
42 | ).isAppearanceLightStatusBars = darkTheme
43 | }
44 | }
45 |
46 | MaterialTheme(
47 | colorScheme = colorScheme,
48 | typography = Typography,
49 | content = content
50 | )
51 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/App.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Surface
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.collectAsState
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.ui.Modifier
10 | import com.plcoding.contactscomposemultiplatform.contacts.data.SqlDelightContactDataSource
11 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.ContactListScreen
12 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.ContactListViewModel
13 | import com.plcoding.contactscomposemultiplatform.core.presentation.ContactsTheme
14 | import com.plcoding.contactscomposemultiplatform.core.presentation.ImagePicker
15 | import com.plcoding.contactscomposemultiplatform.di.AppModule
16 | import dev.icerock.moko.mvvm.compose.getViewModel
17 | import dev.icerock.moko.mvvm.compose.viewModelFactory
18 |
19 | @Composable
20 | fun App(
21 | darkTheme: Boolean,
22 | dynamicColor: Boolean,
23 | appModule: AppModule,
24 | imagePicker: ImagePicker
25 | ) {
26 | ContactsTheme(
27 | darkTheme = darkTheme,
28 | dynamicColor = dynamicColor
29 | ) {
30 | val viewModel = getViewModel(
31 | key = "contact-list-screen",
32 | factory = viewModelFactory {
33 | ContactListViewModel(appModule.contactDataSource)
34 | }
35 | )
36 | val state by viewModel.state.collectAsState()
37 | Surface(
38 | modifier = Modifier.fillMaxSize(),
39 | color = MaterialTheme.colorScheme.background
40 | ) {
41 | ContactListScreen(
42 | state = state,
43 | newContact = viewModel.newContact,
44 | onEvent = viewModel::onEvent,
45 | imagePicker = imagePicker
46 | )
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/RecentlyAddedContacts.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.lazy.LazyRow
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.text.font.FontWeight
16 | import androidx.compose.ui.unit.dp
17 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
18 |
19 | @Composable
20 | fun RecentlyAddedContacts(
21 | contacts: List,
22 | onClick: (Contact) -> Unit,
23 | modifier: Modifier = Modifier
24 | ) {
25 | Column(
26 | modifier = modifier
27 | ) {
28 | if(contacts.isNotEmpty()) {
29 | Text(
30 | text = "Recently Added",
31 | modifier = Modifier
32 | .fillMaxWidth()
33 | .padding(horizontal = 16.dp),
34 | fontWeight = FontWeight.Bold
35 | )
36 | Spacer(Modifier.height(16.dp))
37 | }
38 | LazyRow(
39 | modifier = Modifier.fillMaxWidth(),
40 | contentPadding = PaddingValues(horizontal = 16.dp),
41 | horizontalArrangement = Arrangement.spacedBy(16.dp)
42 | ) {
43 | items(contacts) { contact ->
44 | ContactPreviewItem(
45 | contact = contact,
46 | onClick = { onClick(contact) }
47 | )
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP/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/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/ContactPhoto.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.rounded.Person
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.MaterialTheme
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.layout.ContentScale
17 | import androidx.compose.ui.unit.Dp
18 | import androidx.compose.ui.unit.dp
19 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
20 | import com.plcoding.contactscomposemultiplatform.core.presentation.rememberBitmapFromBytes
21 |
22 | @Composable
23 | fun ContactPhoto(
24 | contact: Contact?,
25 | modifier: Modifier = Modifier,
26 | iconSize: Dp = 25.dp
27 | ) {
28 | val bitmap = rememberBitmapFromBytes(contact?.photoBytes)
29 | val photoModifier = modifier.clip(RoundedCornerShape(35))
30 |
31 | if(bitmap != null) {
32 | Image(
33 | bitmap = bitmap,
34 | contentDescription = contact?.firstName,
35 | modifier = photoModifier,
36 | contentScale = ContentScale.Crop
37 | )
38 | } else {
39 | Box(
40 | modifier = photoModifier
41 | .background(MaterialTheme.colorScheme.secondaryContainer),
42 | contentAlignment = Alignment.Center
43 | ) {
44 | Icon(
45 | imageVector = Icons.Rounded.Person,
46 | contentDescription = contact?.firstName,
47 | modifier = Modifier.size(iconSize),
48 | tint = MaterialTheme.colorScheme.onSecondaryContainer
49 | )
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/presentation/ImagePicker.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.presentation
2 |
3 | import androidx.compose.runtime.Composable
4 | import kotlinx.cinterop.refTo
5 | import platform.UIKit.UIImage
6 | import platform.UIKit.UIImageJPEGRepresentation
7 | import platform.UIKit.UIImagePickerController
8 | import platform.UIKit.UIImagePickerControllerDelegateProtocol
9 | import platform.UIKit.UIImagePickerControllerSourceType
10 | import platform.UIKit.UINavigationControllerDelegateProtocol
11 | import platform.UIKit.UIViewController
12 | import platform.darwin.NSObject
13 | import platform.posix.memcpy
14 |
15 | actual class ImagePicker(
16 | private val rootController: UIViewController
17 | ) {
18 | private val imagePickerController = UIImagePickerController().apply {
19 | sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypePhotoLibrary
20 | }
21 |
22 | private var onImagePicked: (ByteArray) -> Unit = {}
23 |
24 | private val delegate = object : NSObject(), UIImagePickerControllerDelegateProtocol,
25 | UINavigationControllerDelegateProtocol {
26 |
27 | override fun imagePickerController(
28 | picker: UIImagePickerController,
29 | didFinishPickingImage: UIImage,
30 | editingInfo: Map?
31 | ) {
32 | val imageNsData = UIImageJPEGRepresentation(didFinishPickingImage, 1.0)
33 | ?: return
34 | val bytes = ByteArray(imageNsData.length.toInt())
35 | memcpy(bytes.refTo(0), imageNsData.bytes, imageNsData.length)
36 |
37 | onImagePicked(bytes)
38 |
39 | picker.dismissViewControllerAnimated(true, null)
40 | }
41 |
42 | override fun imagePickerControllerDidCancel(picker: UIImagePickerController) {
43 | picker.dismissViewControllerAnimated(true, null)
44 | }
45 | }
46 |
47 | @Composable
48 | actual fun registerPicker(onImagePicked: (ByteArray) -> Unit) {
49 | this.onImagePicked = onImagePicked
50 | }
51 |
52 | actual fun pickImage() {
53 | rootController.presentViewController(imagePickerController, true) {
54 | imagePickerController.delegate = delegate
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.ui.theme
2 |
3 | import androidx.compose.material3.darkColorScheme
4 | import androidx.compose.material3.lightColorScheme
5 |
6 | val DarkColorScheme = darkColorScheme(
7 | primary = GreenPrimaryDark,
8 | secondary = GreenSecondaryDark,
9 | tertiary = GreenTertiaryDark,
10 | onPrimary = OnGreenDark,
11 | primaryContainer = GreenContainerDark,
12 | onPrimaryContainer = OnGreenContainerDark,
13 | onSecondary = OnGreenSecondaryDark,
14 | secondaryContainer = GreenSecondaryContainerDark,
15 | onSecondaryContainer = OnGreenSecondaryContainerDark,
16 | onTertiary = OnGreenTertiaryDark,
17 | onTertiaryContainer = OnGreenTertiaryContainerDark,
18 | tertiaryContainer = GreenTertiaryContainerDark,
19 | background = BackgroundDark,
20 | onBackground = OnBackgroundDark,
21 | surface = SurfaceDark,
22 | onSurface = OnSurfaceDark,
23 | surfaceVariant = SurfaceVariantDark,
24 | onSurfaceVariant = OnSurfaceVariantDark,
25 | error = ErrorDark,
26 | onError = OnErrorDark,
27 | errorContainer = ErrorContainerDark,
28 | onErrorContainer = OnErrorContainerDark,
29 | outline = OutlineDark,
30 | )
31 |
32 | val LightColorScheme = lightColorScheme(
33 | primary = GreenPrimaryLight,
34 | secondary = GreenSecondaryLight,
35 | tertiary = GreenTertiaryLight,
36 | onPrimary = OnGreenLight,
37 | primaryContainer = GreenContainerLight,
38 | onPrimaryContainer = OnGreenContainerLight,
39 | onSecondary = OnGreenSecondaryLight,
40 | secondaryContainer = GreenSecondaryContainerLight,
41 | onSecondaryContainer = OnGreenSecondaryContainerLight,
42 | onTertiary = OnGreenTertiaryLight,
43 | onTertiaryContainer = OnGreenTertiaryContainerLight,
44 | tertiaryContainer = GreenTertiaryContainerLight,
45 | background = BackgroundLight,
46 | onBackground = OnBackgroundLight,
47 | surface = SurfaceLight,
48 | onSurface = OnSurfaceLight,
49 | surfaceVariant = SurfaceVariantLight,
50 | onSurfaceVariant = OnSurfaceVariantLight,
51 | error = ErrorLight,
52 | onError = OnErrorLight,
53 | errorContainer = ErrorContainerLight,
54 | onErrorContainer = OnErrorContainerLight,
55 | outline = OutlineLight,
56 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | // LIGHT
6 | val GreenPrimaryLight = Color(0xff006e26)
7 | val OnGreenLight = Color(0xffffffff)
8 | val GreenContainerLight = Color(0xff6cff82)
9 | val OnGreenContainerLight = Color(0xff002106)
10 |
11 | val GreenSecondaryLight = Color(0xff526350)
12 | val OnGreenSecondaryLight = OnGreenLight
13 | val GreenSecondaryContainerLight = Color(0xffd4e8d0)
14 | val OnGreenSecondaryContainerLight = Color(0xff101f10)
15 |
16 | val GreenTertiaryLight = Color(0xff39656b)
17 | val OnGreenTertiaryLight = OnGreenLight
18 | val GreenTertiaryContainerLight = Color(0xffbcebf2)
19 | val OnGreenTertiaryContainerLight = Color(0xff001f23)
20 |
21 | val ErrorLight = Color(0xffba1a1a)
22 | val OnErrorLight = Color(0xffffffff)
23 | val ErrorContainerLight = Color(0xffffdad6)
24 | val OnErrorContainerLight = Color(0xff410002)
25 |
26 | val BackgroundLight = Color(0xfffcfdf7)
27 | val OnBackgroundLight = Color(0xff1a1c19)
28 | val SurfaceLight = BackgroundLight
29 | val OnSurfaceLight = OnBackgroundLight
30 | val SurfaceVariantLight = Color(0xffdee5d9)
31 | val OnSurfaceVariantLight = Color(0xff424940)
32 |
33 | val OutlineLight = Color(0xff72796f)
34 |
35 | // DARK
36 | val GreenPrimaryDark = Color(0xff00e559)
37 | val OnGreenDark = Color(0xff003910)
38 | val GreenContainerDark = Color(0xff00531b)
39 | val OnGreenContainerDark = Color(0xff6cff82)
40 |
41 | val GreenSecondaryDark = Color(0xffb9ccb5)
42 | val OnGreenSecondaryDark = OnGreenDark
43 | val GreenSecondaryContainerDark = Color(0xff3a4b39)
44 | val OnGreenSecondaryContainerDark = Color(0xffd4e8d0)
45 |
46 | val GreenTertiaryDark = Color(0xffa1ced5)
47 | val OnGreenTertiaryDark = Color(0xff00363c)
48 | val GreenTertiaryContainerDark = Color(0xff1f4d53)
49 | val OnGreenTertiaryContainerDark = Color(0xffbcebf2)
50 |
51 | val ErrorDark = Color(0xffffb4ab)
52 | val OnErrorDark = Color(0xff690005)
53 | val ErrorContainerDark = Color(0xff93000a)
54 | val OnErrorContainerDark = Color(0xffffdad6)
55 |
56 | val BackgroundDark = Color(0xff1a1c19)
57 | val OnBackgroundDark = Color(0xffe2e3dd)
58 | val SurfaceDark = BackgroundDark
59 | val OnSurfaceDark = OnBackgroundDark
60 | val SurfaceVariantDark = Color(0xff424940)
61 | val OnSurfaceVariantDark = Color(0xffc2c9bd)
62 |
63 | val OutlineDark = Color(0xff72796f)
64 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/com/plcoding/contactscomposemultiplatform/core/data/ImageStorage.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.core.data
2 |
3 | import kotlinx.cinterop.addressOf
4 | import kotlinx.cinterop.memScoped
5 | import kotlinx.cinterop.refTo
6 | import kotlinx.cinterop.usePinned
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import platform.Foundation.NSData
10 | import platform.Foundation.NSDocumentDirectory
11 | import platform.Foundation.NSFileManager
12 | import platform.Foundation.NSSearchPathForDirectoriesInDomains
13 | import platform.Foundation.NSString
14 | import platform.Foundation.NSUUID
15 | import platform.Foundation.NSUserDomainMask
16 | import platform.Foundation.create
17 | import platform.Foundation.dataWithContentsOfFile
18 | import platform.Foundation.getBytes
19 | import platform.Foundation.stringByAppendingPathComponent
20 | import platform.Foundation.writeToFile
21 |
22 | actual class ImageStorage {
23 |
24 | private val fileManager = NSFileManager.defaultManager
25 | private val documentDirectory = NSSearchPathForDirectoriesInDomains(
26 | directory = NSDocumentDirectory,
27 | domainMask = NSUserDomainMask,
28 | expandTilde = true
29 | ).first() as NSString
30 |
31 | actual suspend fun saveImage(bytes: ByteArray): String {
32 | return withContext(Dispatchers.Default) {
33 | val fileName = NSUUID.UUID().UUIDString + ".jpg"
34 | val fullPath = documentDirectory.stringByAppendingPathComponent(fileName)
35 |
36 | val data = bytes.usePinned {
37 | NSData.create(
38 | bytes = it.addressOf(0),
39 | length = bytes.size.toULong()
40 | )
41 | }
42 |
43 | data.writeToFile(
44 | path = fullPath,
45 | atomically = true
46 | )
47 | fullPath
48 | }
49 | }
50 |
51 | actual suspend fun getImage(fileName: String): ByteArray? {
52 | return withContext(Dispatchers.Default) {
53 | memScoped {
54 | NSData.dataWithContentsOfFile(fileName)?.let { bytes ->
55 | val array = ByteArray(bytes.length.toInt())
56 | bytes.getBytes(array.refTo(0).getPointer(this), bytes.length)
57 | return@withContext array
58 | }
59 | }
60 | return@withContext null
61 | }
62 | }
63 |
64 | actual suspend fun deleteImage(fileName: String) {
65 | withContext(Dispatchers.Default) {
66 | fileManager.removeItemAtPath(fileName, null)
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/data/SqlDelightContactDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.data
2 |
3 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
4 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactDataSource
5 | import com.plcoding.contactscomposemultiplatform.core.data.ImageStorage
6 | import com.plcoding.contactscomposemultiplatform.database.ContactDatabase
7 | import com.squareup.sqldelight.runtime.coroutines.asFlow
8 | import com.squareup.sqldelight.runtime.coroutines.mapToList
9 | import kotlinx.coroutines.async
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.map
12 | import kotlinx.coroutines.supervisorScope
13 | import kotlinx.datetime.Clock
14 |
15 | class SqlDelightContactDataSource(
16 | db: ContactDatabase,
17 | private val imageStorage: ImageStorage
18 | ) : ContactDataSource {
19 |
20 | private val queries = db.contactQueries
21 |
22 | override fun getContacts(): Flow> {
23 | return queries
24 | .getContacts()
25 | .asFlow()
26 | .mapToList()
27 | .map { contactEntities ->
28 | supervisorScope {
29 | contactEntities
30 | .map {
31 | async { it.toContact(imageStorage) }
32 | }
33 | .map { it.await() }
34 | }
35 | }
36 | }
37 |
38 | override fun getRecentContacts(amount: Int): Flow> {
39 | return queries
40 | .getRecentContacts(amount.toLong())
41 | .asFlow()
42 | .mapToList()
43 | .map { contactEntities ->
44 | supervisorScope {
45 | contactEntities
46 | .map {
47 | async { it.toContact(imageStorage) }
48 | }
49 | .map { it.await() }
50 | }
51 | }
52 | }
53 |
54 | override suspend fun insertContact(contact: Contact) {
55 | val imagePath = contact.photoBytes?.let {
56 | imageStorage.saveImage(it)
57 | }
58 | queries.insertContactEntity(
59 | id = contact.id,
60 | firstName = contact.firstName,
61 | lastName = contact.lastName,
62 | phoneNumber = contact.phoneNumber,
63 | email = contact.email,
64 | createdAt = Clock.System.now().toEpochMilliseconds(),
65 | imagePath = imagePath
66 | )
67 | }
68 |
69 | override suspend fun deleteContact(id: Long) {
70 | val entity = queries.getContactById(id).executeAsOne()
71 | entity.imagePath?.let {
72 | imageStorage.deleteImage(it)
73 | }
74 | queries.deleteContact(id)
75 | }
76 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | id("com.squareup.sqldelight")
6 | }
7 |
8 | kotlin {
9 | android {
10 | compilations.all {
11 | kotlinOptions {
12 | jvmTarget = "17"
13 | }
14 | }
15 | }
16 |
17 | targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).all {
18 | binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java).all {
19 | export("dev.icerock.moko:mvvm-core:0.16.1")
20 | }
21 | }
22 |
23 | listOf(
24 | iosX64(),
25 | iosArm64(),
26 | iosSimulatorArm64()
27 | ).forEach {
28 | it.binaries.framework {
29 | baseName = "shared"
30 | isStatic = true
31 | }
32 | }
33 |
34 | sourceSets {
35 | val commonMain by getting {
36 | dependencies {
37 | implementation(compose.runtime)
38 | implementation(compose.foundation)
39 | implementation(compose.material3)
40 | implementation(compose.materialIconsExtended)
41 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
42 | implementation(compose.components.resources)
43 |
44 | implementation("com.squareup.sqldelight:runtime:1.5.5")
45 | implementation("com.squareup.sqldelight:coroutines-extensions:1.5.5")
46 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
47 | }
48 | }
49 | val commonTest by getting {
50 | dependencies {
51 | implementation(kotlin("test"))
52 | }
53 | }
54 | val androidMain by getting {
55 | dependencies {
56 | implementation("com.squareup.sqldelight:android-driver:1.5.5")
57 | implementation("androidx.appcompat:appcompat:1.6.1")
58 | implementation("androidx.activity:activity-compose:1.7.2")
59 | }
60 | }
61 | val androidUnitTest by getting
62 | val iosX64Main by getting
63 | val iosArm64Main by getting
64 | val iosSimulatorArm64Main by getting
65 | val iosMain by creating {
66 | dependencies {
67 | implementation("com.squareup.sqldelight:native-driver:1.5.5")
68 | }
69 | dependsOn(commonMain)
70 | iosX64Main.dependsOn(this)
71 | iosArm64Main.dependsOn(this)
72 | iosSimulatorArm64Main.dependsOn(this)
73 | }
74 | val iosX64Test by getting
75 | val iosArm64Test by getting
76 | val iosSimulatorArm64Test by getting
77 | val iosTest by creating {
78 | dependsOn(commonTest)
79 | iosX64Test.dependsOn(this)
80 | iosArm64Test.dependsOn(this)
81 | iosSimulatorArm64Test.dependsOn(this)
82 | }
83 | }
84 | }
85 |
86 | android {
87 | namespace = "com.plcoding.contactscomposemultiplatform"
88 | compileSdk = 33
89 | defaultConfig {
90 | minSdk = 24
91 | }
92 | compileOptions {
93 | sourceCompatibility = JavaVersion.VERSION_17
94 | targetCompatibility = JavaVersion.VERSION_17
95 | }
96 | }
97 |
98 | sqldelight {
99 | database("ContactDatabase") {
100 | packageName = "com.plcoding.contactscomposemultiplatform.database"
101 | sourceFolders = listOf("sqldelight")
102 | }
103 | }
104 |
105 | dependencies {
106 | implementation("androidx.core:core:1.10.1")
107 | commonMainApi("dev.icerock.moko:mvvm-core:0.16.1")
108 | commonMainApi("dev.icerock.moko:mvvm-compose:0.16.1")
109 | commonMainApi("dev.icerock.moko:mvvm-flow:0.16.1")
110 | commonMainApi("dev.icerock.moko:mvvm-flow-compose:0.16.1")
111 | }
112 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/ContactListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.PaddingValues
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.items
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.rounded.PersonAdd
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.unit.dp
23 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
24 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.components.AddContactSheet
25 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.components.ContactDetailSheet
26 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.components.ContactListItem
27 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.components.RecentlyAddedContacts
28 | import com.plcoding.contactscomposemultiplatform.core.presentation.ImagePicker
29 |
30 | @OptIn(ExperimentalMaterial3Api::class)
31 | @Composable
32 | fun ContactListScreen(
33 | state: ContactListState,
34 | newContact: Contact?,
35 | onEvent: (ContactListEvent) -> Unit,
36 | imagePicker: ImagePicker
37 | ) {
38 | imagePicker.registerPicker { imageBytes ->
39 | onEvent(ContactListEvent.OnPhotoPicked(imageBytes))
40 | }
41 | Scaffold(
42 | floatingActionButton = {
43 | FloatingActionButton(
44 | onClick = {
45 | onEvent(ContactListEvent.OnAddNewContactClick)
46 | },
47 | shape = RoundedCornerShape(20.dp)
48 | ) {
49 | Icon(
50 | imageVector = Icons.Rounded.PersonAdd,
51 | contentDescription = "Add contact"
52 | )
53 | }
54 | }
55 | ) {
56 | LazyColumn(
57 | modifier = Modifier.fillMaxSize(),
58 | contentPadding = PaddingValues(vertical = 16.dp),
59 | verticalArrangement = Arrangement.spacedBy(16.dp)
60 | ) {
61 | item {
62 | RecentlyAddedContacts(
63 | contacts = state.recentlyAddedContacts,
64 | onClick = {
65 | onEvent(ContactListEvent.SelectContact(it))
66 | }
67 | )
68 | }
69 |
70 | item {
71 | Text(
72 | text = "My contacts (${state.contacts.size})",
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(horizontal = 16.dp),
76 | fontWeight = FontWeight.Bold
77 | )
78 | }
79 |
80 | items(state.contacts) { contact ->
81 | ContactListItem(
82 | contact = contact,
83 | modifier = Modifier
84 | .fillMaxWidth()
85 | .clickable {
86 | onEvent(ContactListEvent.SelectContact(contact))
87 | }
88 | .padding(horizontal = 16.dp)
89 | )
90 | }
91 | }
92 | }
93 |
94 | ContactDetailSheet(
95 | isOpen = state.isSelectedContactSheetOpen,
96 | selectedContact = state.selectedContact,
97 | onEvent = onEvent,
98 | )
99 | AddContactSheet(
100 | state = state,
101 | newContact = newContact,
102 | isOpen = state.isAddContactSheetOpen,
103 | onEvent = { event ->
104 | if(event is ContactListEvent.OnAddPhotoClicked) {
105 | imagePicker.pickImage()
106 | }
107 | onEvent(event)
108 | },
109 | )
110 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/ContactListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
7 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactDataSource
8 | import com.plcoding.contactscomposemultiplatform.contacts.domain.ContactValidator
9 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.combine
14 | import kotlinx.coroutines.flow.stateIn
15 | import kotlinx.coroutines.flow.update
16 | import kotlinx.coroutines.launch
17 |
18 | class ContactListViewModel(
19 | private val contactDataSource: ContactDataSource
20 | ): ViewModel() {
21 |
22 | private val _state = MutableStateFlow(ContactListState())
23 | val state = combine(
24 | _state,
25 | contactDataSource.getContacts(),
26 | contactDataSource.getRecentContacts(20)
27 | ) { state, contacts, recentContacts ->
28 | state.copy(
29 | contacts = contacts,
30 | recentlyAddedContacts = recentContacts
31 | )
32 | }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), ContactListState())
33 |
34 | var newContact: Contact? by mutableStateOf(null)
35 | private set
36 |
37 | fun onEvent(event: ContactListEvent) {
38 | when(event) {
39 | ContactListEvent.DeleteContact -> {
40 | viewModelScope.launch {
41 | _state.value.selectedContact?.id?.let { id ->
42 | _state.update { it.copy(
43 | isSelectedContactSheetOpen = false
44 | ) }
45 | contactDataSource.deleteContact(id)
46 | delay(300L) // Animation delay
47 | _state.update { it.copy(
48 | selectedContact = null
49 | ) }
50 | }
51 | }
52 | }
53 | ContactListEvent.DismissContact -> {
54 | viewModelScope.launch {
55 | _state.update { it.copy(
56 | isSelectedContactSheetOpen = false,
57 | isAddContactSheetOpen = false,
58 | firstNameError = null,
59 | lastNameError = null,
60 | emailError = null,
61 | phoneNumberError = null
62 | ) }
63 | delay(300L) // Animation delay
64 | newContact = null
65 | _state.update { it.copy(
66 | selectedContact = null
67 | ) }
68 | }
69 | }
70 | is ContactListEvent.EditContact -> {
71 | _state.update { it.copy(
72 | selectedContact = null,
73 | isAddContactSheetOpen = true,
74 | isSelectedContactSheetOpen = false
75 | ) }
76 | newContact = event.contact
77 | }
78 | ContactListEvent.OnAddNewContactClick -> {
79 | _state.update { it.copy(
80 | isAddContactSheetOpen = true
81 | ) }
82 | newContact = Contact(
83 | id = null,
84 | firstName = "",
85 | lastName = "",
86 | email = "",
87 | phoneNumber = "",
88 | photoBytes = null
89 | )
90 | }
91 | is ContactListEvent.OnEmailChanged -> {
92 | newContact = newContact?.copy(
93 | email = event.value
94 | )
95 | }
96 | is ContactListEvent.OnFirstNameChanged -> {
97 | newContact = newContact?.copy(
98 | firstName = event.value
99 | )
100 | }
101 | is ContactListEvent.OnLastNameChanged -> {
102 | newContact = newContact?.copy(
103 | lastName = event.value
104 | )
105 | }
106 | is ContactListEvent.OnPhoneNumberChanged -> {
107 | newContact = newContact?.copy(
108 | phoneNumber = event.value
109 | )
110 | }
111 | is ContactListEvent.OnPhotoPicked -> {
112 | newContact = newContact?.copy(
113 | photoBytes = event.bytes
114 | )
115 | }
116 | ContactListEvent.SaveContact -> {
117 | newContact?.let { contact ->
118 | val result = ContactValidator.validateContact(contact)
119 | val errors = listOfNotNull(
120 | result.firstNameError,
121 | result.lastNameError,
122 | result.emailError,
123 | result.phoneNumberError
124 | )
125 |
126 | if(errors.isEmpty()) {
127 | _state.update { it.copy(
128 | isAddContactSheetOpen = false,
129 | firstNameError = null,
130 | lastNameError = null,
131 | emailError = null,
132 | phoneNumberError = null
133 | ) }
134 | viewModelScope.launch {
135 | contactDataSource.insertContact(contact)
136 | delay(300L) // Animation delay
137 | newContact = null
138 | }
139 | } else {
140 | _state.update { it.copy(
141 | firstNameError = result.firstNameError,
142 | lastNameError = result.lastNameError,
143 | emailError = result.emailError,
144 | phoneNumberError = result.phoneNumberError
145 | ) }
146 | }
147 | }
148 | }
149 | is ContactListEvent.SelectContact -> {
150 | _state.update { it.copy(
151 | selectedContact = event.contact,
152 | isSelectedContactSheetOpen = true
153 | ) }
154 | }
155 | else -> Unit
156 | }
157 | }
158 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/AddContactSheet.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.rounded.Add
16 | import androidx.compose.material.icons.rounded.Close
17 | import androidx.compose.material3.Button
18 | import androidx.compose.material3.ExperimentalMaterial3Api
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.IconButton
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.OutlinedTextField
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.draw.clip
28 | import androidx.compose.ui.unit.dp
29 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
30 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.ContactListEvent
31 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.ContactListState
32 | import com.plcoding.contactscomposemultiplatform.core.presentation.BottomSheetFromWish
33 |
34 | @Composable
35 | fun AddContactSheet(
36 | state: ContactListState,
37 | newContact: Contact?,
38 | isOpen: Boolean,
39 | onEvent: (ContactListEvent) -> Unit,
40 | modifier: Modifier = Modifier
41 | ) {
42 | BottomSheetFromWish(
43 | visible = isOpen,
44 | modifier = modifier.fillMaxWidth()
45 | ) {
46 | Box(
47 | modifier = Modifier.fillMaxSize(),
48 | contentAlignment = Alignment.TopStart
49 | ) {
50 | Column(
51 | modifier = Modifier.fillMaxWidth(),
52 | horizontalAlignment = Alignment.CenterHorizontally
53 | ) {
54 | Spacer(Modifier.height(60.dp))
55 | if(newContact?.photoBytes == null) {
56 | Box(
57 | modifier = Modifier
58 | .size(150.dp)
59 | .clip(RoundedCornerShape(40))
60 | .background(MaterialTheme.colorScheme.secondaryContainer)
61 | .clickable {
62 | onEvent(ContactListEvent.OnAddPhotoClicked)
63 | }
64 | .border(
65 | width = 1.dp,
66 | color = MaterialTheme.colorScheme.onSecondaryContainer,
67 | shape = RoundedCornerShape(40)
68 | ),
69 | contentAlignment = Alignment.Center
70 | ) {
71 | Icon(
72 | imageVector = Icons.Rounded.Add,
73 | contentDescription = "Add photo",
74 | tint = MaterialTheme.colorScheme.onSecondaryContainer,
75 | modifier = Modifier.size(40.dp)
76 | )
77 | }
78 | } else {
79 | ContactPhoto(
80 | contact = newContact,
81 | modifier = Modifier
82 | .size(150.dp)
83 | .clickable {
84 | onEvent(ContactListEvent.OnAddPhotoClicked)
85 | }
86 | )
87 | }
88 | Spacer(Modifier.height(16.dp))
89 | ContactTextField(
90 | value = newContact?.firstName ?: "",
91 | placeholder = "First name",
92 | error = state.firstNameError,
93 | onValueChanged = {
94 | onEvent(ContactListEvent.OnFirstNameChanged(it))
95 | },
96 | modifier = Modifier.fillMaxWidth()
97 | )
98 | Spacer(Modifier.height(16.dp))
99 | ContactTextField(
100 | value = newContact?.lastName ?: "",
101 | placeholder = "Last name",
102 | error = state.lastNameError,
103 | onValueChanged = {
104 | onEvent(ContactListEvent.OnLastNameChanged(it))
105 | },
106 | modifier = Modifier.fillMaxWidth()
107 | )
108 | Spacer(Modifier.height(16.dp))
109 | ContactTextField(
110 | value = newContact?.email ?: "",
111 | placeholder = "Email",
112 | error = state.emailError,
113 | onValueChanged = {
114 | onEvent(ContactListEvent.OnEmailChanged(it))
115 | },
116 | modifier = Modifier.fillMaxWidth()
117 | )
118 | Spacer(Modifier.height(16.dp))
119 | ContactTextField(
120 | value = newContact?.phoneNumber ?: "",
121 | placeholder = "Phone number",
122 | error = state.phoneNumberError,
123 | onValueChanged = {
124 | onEvent(ContactListEvent.OnPhoneNumberChanged(it))
125 | },
126 | modifier = Modifier.fillMaxWidth()
127 | )
128 | Spacer(Modifier.height(16.dp))
129 | Button(
130 | onClick = {
131 | onEvent(ContactListEvent.SaveContact)
132 | }
133 | ) {
134 | Text(text = "Save")
135 | }
136 | }
137 | IconButton(
138 | onClick = {
139 | onEvent(ContactListEvent.DismissContact)
140 | }
141 | ) {
142 | Icon(
143 | imageVector = Icons.Rounded.Close,
144 | contentDescription = "Close"
145 | )
146 | }
147 | }
148 | }
149 | }
150 |
151 | @OptIn(ExperimentalMaterial3Api::class)
152 | @Composable
153 | private fun ContactTextField(
154 | value: String,
155 | placeholder: String,
156 | error: String?,
157 | onValueChanged: (String) -> Unit,
158 | modifier: Modifier = Modifier
159 | ) {
160 | Column(modifier) {
161 | OutlinedTextField(
162 | value = value,
163 | placeholder = {
164 | Text(text = placeholder)
165 | },
166 | onValueChange = onValueChanged,
167 | shape = RoundedCornerShape(20.dp),
168 | modifier = Modifier.fillMaxWidth()
169 | )
170 | if(error != null) {
171 | Text(
172 | text = error,
173 | color = MaterialTheme.colorScheme.error
174 | )
175 | }
176 | }
177 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/com/plcoding/contactscomposemultiplatform/contacts/presentation/components/ContactDetailSheet.kt:
--------------------------------------------------------------------------------
1 | package com.plcoding.contactscomposemultiplatform.contacts.presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.width
17 | import androidx.compose.foundation.shape.CircleShape
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material.icons.Icons
20 | import androidx.compose.material.icons.rounded.Add
21 | import androidx.compose.material.icons.rounded.Close
22 | import androidx.compose.material.icons.rounded.Delete
23 | import androidx.compose.material.icons.rounded.Edit
24 | import androidx.compose.material.icons.rounded.Email
25 | import androidx.compose.material.icons.rounded.Phone
26 | import androidx.compose.material3.Button
27 | import androidx.compose.material3.FilledTonalIconButton
28 | import androidx.compose.material3.Icon
29 | import androidx.compose.material3.IconButton
30 | import androidx.compose.material3.IconButtonDefaults
31 | import androidx.compose.material3.MaterialTheme
32 | import androidx.compose.material3.Text
33 | import androidx.compose.runtime.Composable
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.draw.clip
37 | import androidx.compose.ui.graphics.vector.ImageVector
38 | import androidx.compose.ui.text.font.FontWeight
39 | import androidx.compose.ui.text.style.TextAlign
40 | import androidx.compose.ui.unit.dp
41 | import androidx.compose.ui.unit.sp
42 | import com.plcoding.contactscomposemultiplatform.contacts.domain.Contact
43 | import com.plcoding.contactscomposemultiplatform.contacts.presentation.ContactListEvent
44 | import com.plcoding.contactscomposemultiplatform.core.presentation.BottomSheetFromWish
45 |
46 | @Composable
47 | fun ContactDetailSheet(
48 | isOpen: Boolean,
49 | selectedContact: Contact?,
50 | onEvent: (ContactListEvent) -> Unit,
51 | modifier: Modifier = Modifier
52 | ) {
53 | BottomSheetFromWish(
54 | visible = isOpen,
55 | modifier = modifier.fillMaxWidth()
56 | ) {
57 | Box(
58 | modifier = Modifier.fillMaxSize(),
59 | contentAlignment = Alignment.TopStart
60 | ) {
61 | Column(
62 | modifier = Modifier.fillMaxWidth(),
63 | horizontalAlignment = Alignment.CenterHorizontally
64 | ) {
65 | Spacer(Modifier.height(60.dp))
66 | ContactPhoto(
67 | contact = selectedContact,
68 | iconSize = 50.dp,
69 | modifier = Modifier
70 | .size(150.dp)
71 | )
72 | Spacer(Modifier.height(16.dp))
73 | Text(
74 | text = "${selectedContact?.firstName} ${selectedContact?.lastName}",
75 | textAlign = TextAlign.Center,
76 | modifier = Modifier.fillMaxWidth(),
77 | fontWeight = FontWeight.Bold,
78 | fontSize = 30.sp
79 | )
80 | Spacer(Modifier.height(16.dp))
81 | EditRow(
82 | onEditClick = {
83 | selectedContact?.let {
84 | onEvent(ContactListEvent.EditContact(it))
85 | }
86 | },
87 | onDeleteClick = {
88 | onEvent(ContactListEvent.DeleteContact)
89 | }
90 | )
91 | Spacer(Modifier.height(16.dp))
92 | ContactInfoSection(
93 | title = "Phone number",
94 | value = selectedContact?.phoneNumber ?: "-",
95 | icon = Icons.Rounded.Phone,
96 | modifier = Modifier.fillMaxWidth()
97 | )
98 | Spacer(Modifier.height(16.dp))
99 | ContactInfoSection(
100 | title = "Email",
101 | value = selectedContact?.email ?: "-",
102 | icon = Icons.Rounded.Email,
103 | modifier = Modifier.fillMaxWidth()
104 | )
105 | }
106 | IconButton(
107 | onClick = {
108 | onEvent(ContactListEvent.DismissContact)
109 | }
110 | ) {
111 | Icon(
112 | imageVector = Icons.Rounded.Close,
113 | contentDescription = "Close"
114 | )
115 | }
116 | }
117 | }
118 | }
119 |
120 | @Composable
121 | private fun EditRow(
122 | onEditClick: () -> Unit,
123 | onDeleteClick: () -> Unit,
124 | modifier: Modifier = Modifier
125 | ) {
126 | Row(modifier) {
127 | FilledTonalIconButton(
128 | onClick = onEditClick,
129 | colors = IconButtonDefaults.filledTonalIconButtonColors(
130 | containerColor = MaterialTheme.colorScheme.secondaryContainer,
131 | contentColor = MaterialTheme.colorScheme.onSecondaryContainer
132 | )
133 | ) {
134 | Icon(
135 | imageVector = Icons.Rounded.Edit,
136 | contentDescription = "Edit contact"
137 | )
138 | }
139 | FilledTonalIconButton(
140 | onClick = onDeleteClick,
141 | colors = IconButtonDefaults.filledTonalIconButtonColors(
142 | containerColor = MaterialTheme.colorScheme.errorContainer,
143 | contentColor = MaterialTheme.colorScheme.onErrorContainer
144 | )
145 | ) {
146 | Icon(
147 | imageVector = Icons.Rounded.Delete,
148 | contentDescription = "Delete contact"
149 | )
150 | }
151 | }
152 | }
153 |
154 | @Composable
155 | private fun ContactInfoSection(
156 | title: String,
157 | value: String,
158 | icon: ImageVector,
159 | modifier: Modifier = Modifier
160 | ) {
161 | Row(
162 | modifier = modifier,
163 | verticalAlignment = Alignment.CenterVertically
164 | ) {
165 | Icon(
166 | imageVector = icon,
167 | contentDescription = null,
168 | modifier = Modifier
169 | .clip(CircleShape)
170 | .background(MaterialTheme.colorScheme.secondaryContainer)
171 | .padding(8.dp),
172 | tint = MaterialTheme.colorScheme.onSecondaryContainer
173 | )
174 | Spacer(Modifier.width(16.dp))
175 | Column(
176 | modifier = Modifier.weight(1f),
177 | verticalArrangement = Arrangement.spacedBy(8.dp)
178 | ) {
179 | Text(
180 | text = title,
181 | modifier = Modifier.fillMaxWidth(),
182 | color = MaterialTheme.colorScheme.secondary,
183 | fontSize = 12.sp
184 | )
185 | Text(
186 | text = value,
187 | modifier = Modifier.fillMaxWidth(),
188 | color = MaterialTheme.colorScheme.onBackground,
189 | fontSize = 18.sp
190 | )
191 | }
192 | }
193 | }
--------------------------------------------------------------------------------
/iosContactsMP/iosContactsMP.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 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
14 | C33AA41D2A48B5870007B6F9 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33AA41C2A48B5870007B6F9 /* ComposeView.swift */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXCopyFilesBuildPhase section */
18 | 7555FFB4242A642300829871 /* Embed Frameworks */ = {
19 | isa = PBXCopyFilesBuildPhase;
20 | buildActionMask = 2147483647;
21 | dstPath = "";
22 | dstSubfolderSpec = 10;
23 | files = (
24 | );
25 | name = "Embed Frameworks";
26 | runOnlyForDeploymentPostprocessing = 0;
27 | };
28 | /* End PBXCopyFilesBuildPhase section */
29 |
30 | /* Begin PBXFileReference section */
31 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
32 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
33 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
34 | 7555FF7B242A565900829871 /* iosContactsMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosContactsMP.app; sourceTree = BUILT_PRODUCTS_DIR; };
35 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
36 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
37 | C33AA41C2A48B5870007B6F9 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; };
38 | /* End PBXFileReference section */
39 |
40 | /* Begin PBXFrameworksBuildPhase section */
41 | 7555FF78242A565900829871 /* Frameworks */ = {
42 | isa = PBXFrameworksBuildPhase;
43 | buildActionMask = 2147483647;
44 | files = (
45 | );
46 | runOnlyForDeploymentPostprocessing = 0;
47 | };
48 | /* End PBXFrameworksBuildPhase section */
49 |
50 | /* Begin PBXGroup section */
51 | 058557D7273AAEEB004C7B11 /* Preview Content */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,
55 | );
56 | path = "Preview Content";
57 | sourceTree = "";
58 | };
59 | 7555FF72242A565900829871 = {
60 | isa = PBXGroup;
61 | children = (
62 | 7555FF7D242A565900829871 /* iosContactsMP */,
63 | 7555FF7C242A565900829871 /* Products */,
64 | 7555FFB0242A642200829871 /* Frameworks */,
65 | );
66 | sourceTree = "";
67 | };
68 | 7555FF7C242A565900829871 /* Products */ = {
69 | isa = PBXGroup;
70 | children = (
71 | 7555FF7B242A565900829871 /* iosContactsMP.app */,
72 | );
73 | name = Products;
74 | sourceTree = "";
75 | };
76 | 7555FF7D242A565900829871 /* iosContactsMP */ = {
77 | isa = PBXGroup;
78 | children = (
79 | 058557BA273AAA24004C7B11 /* Assets.xcassets */,
80 | 7555FF82242A565900829871 /* ContentView.swift */,
81 | 7555FF8C242A565B00829871 /* Info.plist */,
82 | 2152FB032600AC8F00CF470E /* iOSApp.swift */,
83 | 058557D7273AAEEB004C7B11 /* Preview Content */,
84 | C33AA41C2A48B5870007B6F9 /* ComposeView.swift */,
85 | );
86 | path = iosContactsMP;
87 | sourceTree = "";
88 | };
89 | 7555FFB0242A642200829871 /* Frameworks */ = {
90 | isa = PBXGroup;
91 | children = (
92 | );
93 | name = Frameworks;
94 | sourceTree = "";
95 | };
96 | /* End PBXGroup section */
97 |
98 | /* Begin PBXNativeTarget section */
99 | 7555FF7A242A565900829871 /* iosContactsMP */ = {
100 | isa = PBXNativeTarget;
101 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosContactsMP" */;
102 | buildPhases = (
103 | 7555FFB5242A651A00829871 /* ShellScript */,
104 | 7555FF77242A565900829871 /* Sources */,
105 | 7555FF78242A565900829871 /* Frameworks */,
106 | 7555FF79242A565900829871 /* Resources */,
107 | 7555FFB4242A642300829871 /* Embed Frameworks */,
108 | );
109 | buildRules = (
110 | );
111 | dependencies = (
112 | );
113 | name = iosContactsMP;
114 | productName = iosContactsMP;
115 | productReference = 7555FF7B242A565900829871 /* iosContactsMP.app */;
116 | productType = "com.apple.product-type.application";
117 | };
118 | /* End PBXNativeTarget section */
119 |
120 | /* Begin PBXProject section */
121 | 7555FF73242A565900829871 /* Project object */ = {
122 | isa = PBXProject;
123 | attributes = {
124 | LastSwiftUpdateCheck = 1130;
125 | LastUpgradeCheck = 1130;
126 | ORGANIZATIONNAME = orgName;
127 | TargetAttributes = {
128 | 7555FF7A242A565900829871 = {
129 | CreatedOnToolsVersion = 11.3.1;
130 | };
131 | };
132 | };
133 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosContactsMP" */;
134 | compatibilityVersion = "Xcode 9.3";
135 | developmentRegion = en;
136 | hasScannedForEncodings = 0;
137 | knownRegions = (
138 | en,
139 | Base,
140 | );
141 | mainGroup = 7555FF72242A565900829871;
142 | productRefGroup = 7555FF7C242A565900829871 /* Products */;
143 | projectDirPath = "";
144 | projectRoot = "";
145 | targets = (
146 | 7555FF7A242A565900829871 /* iosContactsMP */,
147 | );
148 | };
149 | /* End PBXProject section */
150 |
151 | /* Begin PBXResourcesBuildPhase section */
152 | 7555FF79242A565900829871 /* Resources */ = {
153 | isa = PBXResourcesBuildPhase;
154 | buildActionMask = 2147483647;
155 | files = (
156 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,
157 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,
158 | );
159 | runOnlyForDeploymentPostprocessing = 0;
160 | };
161 | /* End PBXResourcesBuildPhase section */
162 |
163 | /* Begin PBXShellScriptBuildPhase section */
164 | 7555FFB5242A651A00829871 /* ShellScript */ = {
165 | isa = PBXShellScriptBuildPhase;
166 | buildActionMask = 2147483647;
167 | files = (
168 | );
169 | inputFileListPaths = (
170 | );
171 | inputPaths = (
172 | );
173 | outputFileListPaths = (
174 | );
175 | outputPaths = (
176 | );
177 | runOnlyForDeploymentPostprocessing = 0;
178 | shellPath = /bin/sh;
179 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n";
180 | };
181 | /* End PBXShellScriptBuildPhase section */
182 |
183 | /* Begin PBXSourcesBuildPhase section */
184 | 7555FF77242A565900829871 /* Sources */ = {
185 | isa = PBXSourcesBuildPhase;
186 | buildActionMask = 2147483647;
187 | files = (
188 | C33AA41D2A48B5870007B6F9 /* ComposeView.swift in Sources */,
189 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
190 | 7555FF83242A565900829871 /* ContentView.swift in Sources */,
191 | );
192 | runOnlyForDeploymentPostprocessing = 0;
193 | };
194 | /* End PBXSourcesBuildPhase section */
195 |
196 | /* Begin XCBuildConfiguration section */
197 | 7555FFA3242A565B00829871 /* Debug */ = {
198 | isa = XCBuildConfiguration;
199 | buildSettings = {
200 | ALWAYS_SEARCH_USER_PATHS = NO;
201 | CLANG_ANALYZER_NONNULL = YES;
202 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
203 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
204 | CLANG_CXX_LIBRARY = "libc++";
205 | CLANG_ENABLE_MODULES = YES;
206 | CLANG_ENABLE_OBJC_ARC = YES;
207 | CLANG_ENABLE_OBJC_WEAK = YES;
208 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
209 | CLANG_WARN_BOOL_CONVERSION = YES;
210 | CLANG_WARN_COMMA = YES;
211 | CLANG_WARN_CONSTANT_CONVERSION = YES;
212 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
214 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
215 | CLANG_WARN_EMPTY_BODY = YES;
216 | CLANG_WARN_ENUM_CONVERSION = YES;
217 | CLANG_WARN_INFINITE_RECURSION = YES;
218 | CLANG_WARN_INT_CONVERSION = YES;
219 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
220 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
221 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
223 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
224 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
225 | CLANG_WARN_STRICT_PROTOTYPES = YES;
226 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
227 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
228 | CLANG_WARN_UNREACHABLE_CODE = YES;
229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
230 | COPY_PHASE_STRIP = NO;
231 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
232 | ENABLE_STRICT_OBJC_MSGSEND = YES;
233 | ENABLE_TESTABILITY = YES;
234 | GCC_C_LANGUAGE_STANDARD = gnu11;
235 | GCC_DYNAMIC_NO_PIC = NO;
236 | GCC_NO_COMMON_BLOCKS = YES;
237 | GCC_OPTIMIZATION_LEVEL = 0;
238 | GCC_PREPROCESSOR_DEFINITIONS = (
239 | "DEBUG=1",
240 | "$(inherited)",
241 | );
242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
244 | GCC_WARN_UNDECLARED_SELECTOR = YES;
245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
246 | GCC_WARN_UNUSED_FUNCTION = YES;
247 | GCC_WARN_UNUSED_VARIABLE = YES;
248 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
250 | MTL_FAST_MATH = YES;
251 | ONLY_ACTIVE_ARCH = YES;
252 | SDKROOT = iphoneos;
253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
255 | };
256 | name = Debug;
257 | };
258 | 7555FFA4242A565B00829871 /* Release */ = {
259 | isa = XCBuildConfiguration;
260 | buildSettings = {
261 | ALWAYS_SEARCH_USER_PATHS = NO;
262 | CLANG_ANALYZER_NONNULL = YES;
263 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
264 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
265 | CLANG_CXX_LIBRARY = "libc++";
266 | CLANG_ENABLE_MODULES = YES;
267 | CLANG_ENABLE_OBJC_ARC = YES;
268 | CLANG_ENABLE_OBJC_WEAK = YES;
269 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
270 | CLANG_WARN_BOOL_CONVERSION = YES;
271 | CLANG_WARN_COMMA = YES;
272 | CLANG_WARN_CONSTANT_CONVERSION = YES;
273 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
274 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
275 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
276 | CLANG_WARN_EMPTY_BODY = YES;
277 | CLANG_WARN_ENUM_CONVERSION = YES;
278 | CLANG_WARN_INFINITE_RECURSION = YES;
279 | CLANG_WARN_INT_CONVERSION = YES;
280 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
281 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
282 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
283 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
284 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
285 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
286 | CLANG_WARN_STRICT_PROTOTYPES = YES;
287 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
288 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
289 | CLANG_WARN_UNREACHABLE_CODE = YES;
290 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
291 | COPY_PHASE_STRIP = NO;
292 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
293 | ENABLE_NS_ASSERTIONS = NO;
294 | ENABLE_STRICT_OBJC_MSGSEND = YES;
295 | GCC_C_LANGUAGE_STANDARD = gnu11;
296 | GCC_NO_COMMON_BLOCKS = YES;
297 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
298 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
299 | GCC_WARN_UNDECLARED_SELECTOR = YES;
300 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
301 | GCC_WARN_UNUSED_FUNCTION = YES;
302 | GCC_WARN_UNUSED_VARIABLE = YES;
303 | IPHONEOS_DEPLOYMENT_TARGET = 14.1;
304 | MTL_ENABLE_DEBUG_INFO = NO;
305 | MTL_FAST_MATH = YES;
306 | SDKROOT = iphoneos;
307 | SWIFT_COMPILATION_MODE = wholemodule;
308 | SWIFT_OPTIMIZATION_LEVEL = "-O";
309 | VALIDATE_PRODUCT = YES;
310 | };
311 | name = Release;
312 | };
313 | 7555FFA6242A565B00829871 /* Debug */ = {
314 | isa = XCBuildConfiguration;
315 | buildSettings = {
316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
317 | CODE_SIGN_STYLE = Automatic;
318 | DEVELOPMENT_ASSET_PATHS = "\"iosContactsMP/Preview Content\"";
319 | ENABLE_PREVIEWS = YES;
320 | FRAMEWORK_SEARCH_PATHS = (
321 | "$(inherited)",
322 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
323 | );
324 | INFOPLIST_FILE = iosContactsMP/Info.plist;
325 | LD_RUNPATH_SEARCH_PATHS = (
326 | "$(inherited)",
327 | "@executable_path/Frameworks",
328 | );
329 | OTHER_LDFLAGS = (
330 | "$(inherited)",
331 | "-framework",
332 | shared,
333 | "-lsqlite3",
334 | );
335 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosContactsMP;
336 | PRODUCT_NAME = "$(TARGET_NAME)";
337 | SWIFT_VERSION = 5.0;
338 | TARGETED_DEVICE_FAMILY = "1,2";
339 | };
340 | name = Debug;
341 | };
342 | 7555FFA7242A565B00829871 /* Release */ = {
343 | isa = XCBuildConfiguration;
344 | buildSettings = {
345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
346 | CODE_SIGN_STYLE = Automatic;
347 | DEVELOPMENT_ASSET_PATHS = "\"iosContactsMP/Preview Content\"";
348 | ENABLE_PREVIEWS = YES;
349 | FRAMEWORK_SEARCH_PATHS = (
350 | "$(inherited)",
351 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
352 | );
353 | INFOPLIST_FILE = iosContactsMP/Info.plist;
354 | LD_RUNPATH_SEARCH_PATHS = (
355 | "$(inherited)",
356 | "@executable_path/Frameworks",
357 | );
358 | OTHER_LDFLAGS = (
359 | "$(inherited)",
360 | "-framework",
361 | shared,
362 | "-lsqlite3",
363 | );
364 | PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosContactsMP;
365 | PRODUCT_NAME = "$(TARGET_NAME)";
366 | SWIFT_VERSION = 5.0;
367 | TARGETED_DEVICE_FAMILY = "1,2";
368 | };
369 | name = Release;
370 | };
371 | /* End XCBuildConfiguration section */
372 |
373 | /* Begin XCConfigurationList section */
374 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosContactsMP" */ = {
375 | isa = XCConfigurationList;
376 | buildConfigurations = (
377 | 7555FFA3242A565B00829871 /* Debug */,
378 | 7555FFA4242A565B00829871 /* Release */,
379 | );
380 | defaultConfigurationIsVisible = 0;
381 | defaultConfigurationName = Release;
382 | };
383 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosContactsMP" */ = {
384 | isa = XCConfigurationList;
385 | buildConfigurations = (
386 | 7555FFA6242A565B00829871 /* Debug */,
387 | 7555FFA7242A565B00829871 /* Release */,
388 | );
389 | defaultConfigurationIsVisible = 0;
390 | defaultConfigurationName = Release;
391 | };
392 | /* End XCConfigurationList section */
393 | };
394 | rootObject = 7555FF73242A565900829871 /* Project object */;
395 | }
396 |
--------------------------------------------------------------------------------