diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41ad7c2a..290f1b29 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,4 +56,5 @@ dependencies { testImplementation(libs.junit) implementation(libs.compose.navigation) implementation(libs.koin.compose) + implementation(libs.google.services) } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt index c259a2df..397e28b5 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt @@ -8,6 +8,7 @@ import co.touchlab.kampkit.core.AppInfo import co.touchlab.kampkit.core.initKoin import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewModel +import co.touchlab.kampkit.ui.signin.SignInViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.parameter.parametersOf import org.koin.dsl.module @@ -27,6 +28,7 @@ class MainApp : Application() { params.get(), get(), get { parametersOf("BreedDetailsViewModel") } ) } + viewModel { SignInViewModel() } single { get().getSharedPreferences("KAMPSTARTER_SETTINGS", MODE_PRIVATE) } diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt index 426640e2..a48c0157 100644 --- a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/MainNavCoordinator.kt @@ -15,10 +15,13 @@ private const val BREEDS = "breeds" private const val BREED_DETAILS = "breedDetails" private const val BREED_ID_ARG = "breedId" +private const val SIGN_IN = "signIn" + @Composable fun MainNavCoordinator() { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "breeds") { + NavHost(navController = navController, startDestination = SIGN_IN) { + composable(SIGN_IN) { SignInScreen(koinViewModel()) } composable(BREEDS) { BreedsScreen( viewModel = koinViewModel(), diff --git a/app/src/main/kotlin/co/touchlab/kampkit/android/ui/SignInScreen.kt b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/SignInScreen.kt new file mode 100644 index 00000000..6cbc02bf --- /dev/null +++ b/app/src/main/kotlin/co/touchlab/kampkit/android/ui/SignInScreen.kt @@ -0,0 +1,76 @@ +package co.touchlab.kampkit.android.ui + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.touchlab.kampkit.ui.signin.GoogleSignInData +import co.touchlab.kampkit.ui.signin.SignInViewModel +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException + +@Composable +fun SignInScreen(viewModel: SignInViewModel) { + val signInLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { signInActivityResult -> + val signInData = signInActivityResult.extractGoogleSignInData() + viewModel.onSignInClick(signInData) + } + + val state by viewModel.signInState.collectAsStateWithLifecycle() + Column { + state.error?.let { error -> + Text(error, color = Color.Red) + } + + if (state.isUserLoggedIn) { + Text("Logged in as ${state.currentUserName}") + Button(onClick = { viewModel.onSignOutClick() }) { + Text("Log out") + } + } else { + val context = LocalContext.current + Button( + onClick = { signInLauncher.launch(getGoogleSignInIntent(context)) } + ) { + Text("Google Sign In") + } + } + } +} + +private fun ActivityResult.extractGoogleSignInData(): GoogleSignInData { + if (resultCode != Activity.RESULT_OK) { + return GoogleSignInData(error = "Google SignIn activity error") + } + return try { + val task = GoogleSignIn.getSignedInAccountFromIntent(data) + val account = task.getResult(ApiException::class.java) + GoogleSignInData(email = account.email) + } catch (exception: ApiException) { + GoogleSignInData(error = exception.message) + } +} + +private fun getGoogleSignInIntent(context: Context): Intent { + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestProfile() + .build() + val client = GoogleSignIn.getClient(context, signInOptions) + return client.signInIntent +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6f7d7c9..cb28095c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ koin = "3.2.0" multiplatformSettings = "1.0.0-alpha01" turbine = "0.12.1" sqlDelight = "1.5.5" +googleServices = "20.5.0" [libraries] android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugaring" } @@ -92,6 +93,7 @@ touchlab-stately = { module = "co.touchlab:stately-common", version.ref = "state turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +google-services = { module = "com.google.android.gms:play-services-auth", version.ref = "googleServices"} [plugins] ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } diff --git a/ios/KaMPKitiOS.xcodeproj/project.pbxproj b/ios/KaMPKitiOS.xcodeproj/project.pbxproj index 088a4ea6..02e8f294 100644 --- a/ios/KaMPKitiOS.xcodeproj/project.pbxproj +++ b/ios/KaMPKitiOS.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 3C61D1352A55909300D4DF1D /* BreedDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C61D1342A55909300D4DF1D /* BreedDetailsViewModel.swift */; }; + 3C652F282A5D336600B402BA /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 3C652F272A5D336600B402BA /* GoogleSignIn */; }; + 3C652F2A2A5D336600B402BA /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3C652F292A5D336600B402BA /* GoogleSignInSwift */; }; + 3C652F2D2A5D34B300B402BA /* SignInScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C652F2C2A5D34B300B402BA /* SignInScreen.swift */; }; + 3C652F2F2A5D34BD00B402BA /* SignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C652F2E2A5D34BD00B402BA /* SignInViewModel.swift */; }; 3C6AEC072A30881B0003F34A /* BreedDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEC062A30881B0003F34A /* BreedDetailsScreen.swift */; }; 3C6AEC092A30C17A0003F34A /* MainNavCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C6AEC082A30C17A0003F34A /* MainNavCoordinator.swift */; }; 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C73D0492A335103003E6929 /* BreedsViewModel.swift */; }; @@ -45,6 +49,8 @@ 1DFCC00C8DAA719770A18D1A /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; 2A1ED6A4A2A53F5F75C58E5F /* Pods-KaMPKitiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KaMPKitiOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KaMPKitiOS/Pods-KaMPKitiOS.release.xcconfig"; sourceTree = ""; }; 3C61D1342A55909300D4DF1D /* BreedDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedDetailsViewModel.swift; sourceTree = ""; }; + 3C652F2C2A5D34B300B402BA /* SignInScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInScreen.swift; sourceTree = ""; }; + 3C652F2E2A5D34BD00B402BA /* SignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewModel.swift; sourceTree = ""; }; 3C6AEC062A30881B0003F34A /* BreedDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedDetailsScreen.swift; sourceTree = ""; }; 3C6AEC082A30C17A0003F34A /* MainNavCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavCoordinator.swift; sourceTree = ""; }; 3C73D0492A335103003E6929 /* BreedsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreedsViewModel.swift; sourceTree = ""; }; @@ -72,7 +78,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3C652F2A2A5D336600B402BA /* GoogleSignInSwift in Frameworks */, 3DFF917C64A18A83DA010EE1 /* Pods_KaMPKitiOS.framework in Frameworks */, + 3C652F282A5D336600B402BA /* GoogleSignIn in Frameworks */, 3CD290EC2A251417004C7AD1 /* KMPNativeCoroutinesCombine in Frameworks */, 3CD290EE2A251417004C7AD1 /* KMPNativeCoroutinesCore in Frameworks */, ); @@ -104,6 +112,15 @@ path = BreedDetails; sourceTree = ""; }; + 3C652F2B2A5D34A500B402BA /* SignIn */ = { + isa = PBXGroup; + children = ( + 3C652F2C2A5D34B300B402BA /* SignInScreen.swift */, + 3C652F2E2A5D34BD00B402BA /* SignInViewModel.swift */, + ); + path = SignIn; + sourceTree = ""; + }; 3C73D0482A335061003E6929 /* Breeds */ = { isa = PBXGroup; children = ( @@ -182,6 +199,7 @@ F1465EFF23AA94BF0055F7C3 /* KaMPKitiOS */ = { isa = PBXGroup; children = ( + 3C652F2B2A5D34A500B402BA /* SignIn */, 3C61D1332A55908200D4DF1D /* BreedDetails */, 3C73D0482A335061003E6929 /* Breeds */, F1465F0023AA94BF0055F7C3 /* AppDelegate.swift */, @@ -235,6 +253,8 @@ packageProductDependencies = ( 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */, 3CD290ED2A251417004C7AD1 /* KMPNativeCoroutinesCore */, + 3C652F272A5D336600B402BA /* GoogleSignIn */, + 3C652F292A5D336600B402BA /* GoogleSignInSwift */, ); productName = KaMPKitiOS; productReference = F1465EFD23AA94BF0055F7C3 /* KaMPKitiOS.app */; @@ -310,6 +330,7 @@ mainGroup = F1465EF423AA94BF0055F7C3; packageReferences = ( 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */, + 3C652F262A5D336600B402BA /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, ); productRefGroup = F1465EFE23AA94BF0055F7C3 /* Products */; projectDirPath = ""; @@ -416,7 +437,9 @@ files = ( 46B5284D249C5CF400A7725D /* Koin.swift in Sources */, 3C73D04A2A335103003E6929 /* BreedsViewModel.swift in Sources */, + 3C652F2D2A5D34B300B402BA /* SignInScreen.swift in Sources */, 3C61D1352A55909300D4DF1D /* BreedDetailsViewModel.swift in Sources */, + 3C652F2F2A5D34BD00B402BA /* SignInViewModel.swift in Sources */, 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */, 46A5B5EF26AF54F7002EFEAA /* BreedsScreen.swift in Sources */, F1465F0123AA94BF0055F7C3 /* AppDelegate.swift in Sources */, @@ -778,6 +801,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 3C652F262A5D336600B402BA /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/rickclephas/KMP-NativeCoroutines.git"; @@ -789,6 +820,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 3C652F272A5D336600B402BA /* GoogleSignIn */ = { + isa = XCSwiftPackageProductDependency; + package = 3C652F262A5D336600B402BA /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignIn; + }; + 3C652F292A5D336600B402BA /* GoogleSignInSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 3C652F262A5D336600B402BA /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignInSwift; + }; 3CD290EB2A251417004C7AD1 /* KMPNativeCoroutinesCombine */ = { isa = XCSwiftPackageProductDependency; package = 3CD290EA2A251417004C7AD1 /* XCRemoteSwiftPackageReference "KMP-NativeCoroutines" */; diff --git a/ios/KaMPKitiOS/Info.plist b/ios/KaMPKitiOS/Info.plist index af8868b2..f4804369 100644 --- a/ios/KaMPKitiOS/Info.plist +++ b/ios/KaMPKitiOS/Info.plist @@ -41,5 +41,16 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + GIDClientID + 40914781142-l0i1eau2hbmgnciqj438kah5i5t61mus.apps.googleusercontent.com + CFBundleURLTypes + + + CFBundleURLSchemes + + com.googleusercontent.apps.40914781142-l0i1eau2hbmgnciqj438kah5i5t61mus + + + diff --git a/ios/KaMPKitiOS/MainNavCoordinator.swift b/ios/KaMPKitiOS/MainNavCoordinator.swift index 3f90e810..8d29ff43 100644 --- a/ios/KaMPKitiOS/MainNavCoordinator.swift +++ b/ios/KaMPKitiOS/MainNavCoordinator.swift @@ -17,7 +17,7 @@ class MainNavCoordinator: BreedsNavCoordinator { } func start() { - let controller = buildBreedsController(navCoordinator: self) + let controller = buildSignInController(viewController: navController) navController.pushViewController(controller, animated: false) } @@ -37,6 +37,11 @@ private func buildBreedDetailsController(breedId: Int64) -> UIHostingController< return UIHostingController(rootView: BreedDetailsScreen(viewModel: viewModel)) } +private func buildSignInController(viewController: UIViewController) -> UIHostingController { + let viewModel = SignInViewModel(viewController: viewController) + return UIHostingController(rootView: SignInScreen(viewModel: viewModel)) +} + protocol BreedsNavCoordinator { func onBreedDetailsRequest(breedId: Int64) } diff --git a/ios/KaMPKitiOS/SignIn/SignInScreen.swift b/ios/KaMPKitiOS/SignIn/SignInScreen.swift new file mode 100644 index 00000000..d5e90c18 --- /dev/null +++ b/ios/KaMPKitiOS/SignIn/SignInScreen.swift @@ -0,0 +1,57 @@ +// +// SignInScreen.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 11/07/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import Foundation +import GoogleSignInSwift +import SwiftUI + +struct SignInScreen: View { + @StateObject + var viewModel: SignInViewModel + + var body: some View { + SignInScreenContent( + username: viewModel.state.currentUserName, + error: viewModel.state.error, + onSignOut: viewModel.onSignOutClick, + onSignIn: viewModel.onSignInClick + ) + .onAppear(perform: { + viewModel.subscribeState() + }) + .onDisappear(perform: { + viewModel.unsubscribeState() + }) + } +} + +struct SignInScreenContent: View { + var username: String? + var error: String? + var onSignOut: () -> Void + var onSignIn: () -> Void + + var body: some View { + if username == nil { + GoogleSignInButton(action: onSignIn) + } else { + VStack { + if let email = username { + Text("Logged in as " + email) + } + if let error = error { + Text(error) + .foregroundColor(.red) + } + Button("Log out") { + onSignOut() + } + } + } + } +} diff --git a/ios/KaMPKitiOS/SignIn/SignInViewModel.swift b/ios/KaMPKitiOS/SignIn/SignInViewModel.swift new file mode 100644 index 00000000..9cb08754 --- /dev/null +++ b/ios/KaMPKitiOS/SignIn/SignInViewModel.swift @@ -0,0 +1,57 @@ +// +// SignInViewModel.swift +// KaMPKitiOS +// +// Created by Bartłomiej Pedryc on 11/07/2023. +// Copyright © 2023 Touchlab. All rights reserved. +// + +import Foundation +import shared +import Combine +import KMPNativeCoroutinesCombine +import GoogleSignInSwift +import GoogleSignIn + +class SignInViewModel: ObservableObject { + + @Published var state: SignInViewState = SignInViewState.companion.default() + + private var viewModelDelegate: SignInViewModelDelegate = KotlinDependencies.shared.getSignInViewModel() + private var viewController: UIViewController + private var cancellables = [AnyCancellable]() + init(viewController: UIViewController) { + self.viewController = viewController + } + + deinit { + viewModelDelegate.clear() + } + + func subscribeState() { + createPublisher(for: viewModelDelegate.signInStateFlow) + .sink { _ in } receiveValue: { [weak self] (signInState: SignInViewState) in + self?.state = signInState + } + .store(in: &cancellables) + } + + func unsubscribeState() { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + func onSignInClick() { + GIDSignIn.sharedInstance.signIn(withPresenting: viewController) { [weak self] signInResult, error in + let signInData = GoogleSignInData( + email: signInResult?.user.profile?.email, + error: error?.localizedDescription + ) + self?.viewModelDelegate.onSignInClick(signInData: signInData) + } + } + + func onSignOutClick() { + viewModelDelegate.onSignOutClick() + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/GoogleSignInData.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/GoogleSignInData.kt new file mode 100644 index 00000000..676fd9fc --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/GoogleSignInData.kt @@ -0,0 +1,6 @@ +package co.touchlab.kampkit.ui.signin + +data class GoogleSignInData( + val email: String? = null, + val error: String? = null +) diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewModel.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewModel.kt new file mode 100644 index 00000000..b88a72a7 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewModel.kt @@ -0,0 +1,32 @@ +package co.touchlab.kampkit.ui.signin + +import co.touchlab.kampkit.core.ViewModel +import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlin.native.ObjCName + +@ObjCName("SignInViewModelDelegate") +class SignInViewModel : ViewModel() { + + private val mutableSignInState = MutableStateFlow(SignInViewState()) + + @NativeCoroutinesState + val signInState: StateFlow = mutableSignInState + + fun onSignInClick(signInData: GoogleSignInData) { + mutableSignInState.update { + it.copy( + currentUserName = signInData.email, + error = signInData.error + ) + } + } + + fun onSignOutClick() { + mutableSignInState.update { + it.copy(currentUserName = null) + } + } +} diff --git a/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewState.kt b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewState.kt new file mode 100644 index 00000000..35d8bc84 --- /dev/null +++ b/shared/src/commonMain/kotlin/co/touchlab/kampkit/ui/signin/SignInViewState.kt @@ -0,0 +1,12 @@ +package co.touchlab.kampkit.ui.signin + +data class SignInViewState( + val currentUserName: String? = null, + val error: String? = null +) { + val isUserLoggedIn + get() = currentUserName != null + companion object { + fun default() = SignInViewState() + } +} diff --git a/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt index faf9243f..dc97a31b 100644 --- a/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt +++ b/shared/src/iosMain/kotlin/co/touchlab/kampkit/core/KoinIOS.kt @@ -3,6 +3,7 @@ package co.touchlab.kampkit.core import co.touchlab.kampkit.db.KaMPKitDb import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel import co.touchlab.kampkit.ui.breeds.BreedsViewModel +import co.touchlab.kampkit.ui.signin.SignInViewModel import co.touchlab.kermit.Logger import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings @@ -36,6 +37,8 @@ actual val platformModule = module { factory { BreedsViewModel(get(), getWith("BreedsViewModel")) } factory { params -> BreedDetailsViewModel(params.get(), get(), getWith("BreedDetailsViewModel")) } + + factory { SignInViewModel() } } // Access from Swift to create a logger @@ -47,4 +50,5 @@ fun Koin.loggerWithTag(tag: String) = object KotlinDependencies : KoinComponent { fun getBreedsViewModel() = getKoin().get() fun getBreedDetailsViewModel(breedId: Long) = getKoin().get { parametersOf(breedId) } + fun getSignInViewModel() = getKoin().get() }