├── .idea ├── .name ├── .gitignore ├── vcs.xml ├── compiler.xml ├── kotlinc.xml ├── misc.xml └── gradle.xml ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── values-land │ │ │ │ └── dimens.xml │ │ │ ├── values-w1240dp │ │ │ │ └── dimens.xml │ │ │ ├── values-w600dp │ │ │ │ └── dimens.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ │ └── activity_login.xml │ │ │ ├── layout-w1240dp │ │ │ │ └── activity_login.xml │ │ │ ├── layout-w936dp │ │ │ │ └── activity_login.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── jetbrains │ │ │ │ └── simplelogin │ │ │ │ └── androidapp │ │ │ │ ├── ui │ │ │ │ └── login │ │ │ │ │ ├── LoginResult.kt │ │ │ │ │ ├── LoggedInUserView.kt │ │ │ │ │ ├── LoginFormState.kt │ │ │ │ │ ├── LoginViewModel.kt │ │ │ │ │ ├── LoginActivity.kt │ │ │ │ │ └── LoginScreen.kt │ │ │ │ └── data │ │ │ │ ├── model │ │ │ │ └── LoggedInUser.kt │ │ │ │ ├── Result.kt │ │ │ │ ├── LoginDataSource.kt │ │ │ │ ├── LoginDataValidator.kt │ │ │ │ └── LoginRepository.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── jetbrains │ │ │ └── simplelogin │ │ │ └── androidapp │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── jetbrains │ │ └── simplelogin │ │ └── androidapp │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── gradle.properties ├── .gitignore ├── settings.gradle.kts ├── README.md ├── .github └── workflows │ └── build-project.yml ├── gradlew.bat ├── gradlew └── LICENSE /.idea/.name: -------------------------------------------------------------------------------- 1 | SimpleLogin -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /app/src/main/res/values-w1240dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 200dp 3 | -------------------------------------------------------------------------------- /app/src/main/res/values-w600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kotlin/kmp-integration-sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 3 | org.gradle.configuration-cache=true 4 | org.gradle.caching=true 5 | 6 | #Kotlin 7 | kotlin.code.style=official 8 | kotlin.daemon.jvmargs=-Xmx3072M 9 | 10 | #Android 11 | android.useAndroidX=true 12 | android.nonTransitiveRClass=true 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoginResult.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.ui.login 2 | 3 | /** 4 | * Authentication result : success (user details) or error message. 5 | */ 6 | data class LoginResult( 7 | val success: LoggedInUserView? = null, 8 | val error: Int? = null 9 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/data/model/LoggedInUser.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.data.model 2 | 3 | /** 4 | * Data class that captures user information for logged in users retrieved from LoginRepository 5 | */ 6 | data class LoggedInUser( 7 | val userId: String, 8 | val displayName: String 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoggedInUserView.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.ui.login 2 | 3 | /** 4 | * User details post authentication that is exposed to the UI 5 | */ 6 | data class LoggedInUserView( 7 | val displayName: String 8 | //... other data fields that may be accessible to the UI 9 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .kotlin 3 | .gradle 4 | **/build/ 5 | xcuserdata 6 | !src/**/build/ 7 | local.properties 8 | .idea 9 | .DS_Store 10 | captures 11 | .externalNativeBuild 12 | .cxx 13 | *.xcodeproj/* 14 | !*.xcodeproj/project.pbxproj 15 | !*.xcodeproj/xcshareddata/ 16 | !*.xcodeproj/project.xcworkspace/ 17 | !*.xcworkspace/contents.xcworkspacedata 18 | **/xcshareddata/WorkspaceSettings.xcsettings 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoginFormState.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.ui.login 2 | 3 | /** 4 | * Data validation state of the login form. 5 | */ 6 | data class LoginFormState( 7 | val usernameError: String?, 8 | val passwordError: String? 9 | ) { 10 | val isDataValid: Boolean 11 | get() = usernameError == null && passwordError == null 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "SimpleLogin" 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | include(":app") 19 | 20 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/jetbrains/simplelogin/androidapp/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/data/Result.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.data 2 | 3 | /** 4 | * A generic class that holds a value with its loading status. 5 | * @param 6 | */ 7 | sealed class Result { 8 | 9 | data class Success(val data: T) : Result() 10 | data class Error(val exception: Exception) : Result() 11 | 12 | override fun toString(): String { 13 | return when (this) { 14 | is Success<*> -> "Success[data=$data]" 15 | is Error -> "Error[exception=$exception]" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Simple login 3 | 4 | Email 5 | Password 6 | Sign in or register 7 | Sign in 8 | "Welcome !" 9 | Not a valid username 10 | Password must be >5 characters 11 | "Login failed" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![official project](http://jb.gg/badges/official.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | 3 | # Simple login 4 | 5 | This is a sample project for the 6 | [Make your Android application work on iOS](https://kotlinlang.org/docs/mobile/integrate-in-existing-app.html) 7 | tutorial. 8 | 9 | The master branch contains the project’s initial state: 10 | it’s a simple Android application generated with the Android Studio **Login Activity** wizard. 11 | 12 | You could find the final state with the iOS application and the shared module in the 13 | [final branch](https://github.com/Kotlin/kmm-integration-sample/tree/final). 14 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/jetbrains/simplelogin/androidapp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.jetbrains.simplelogin.androidapp", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/data/LoginDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.data 2 | 3 | import com.jetbrains.simplelogin.androidapp.data.model.LoggedInUser 4 | import java.io.IOException 5 | 6 | /** 7 | * Class that handles authentication w/ login credentials and retrieves user information. 8 | */ 9 | class LoginDataSource { 10 | 11 | fun login(username: String, password: String): Result { 12 | try { 13 | // TODO: handle loggedInUser authentication 14 | val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") 15 | return Result.Success(fakeUser) 16 | } catch (e: Throwable) { 17 | return Result.Error(IOException("Error logging in", e)) 18 | } 19 | } 20 | 21 | fun logout() { 22 | // TODO: revoke authentication 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/data/LoginDataValidator.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.data 2 | 3 | import android.util.Patterns 4 | 5 | class LoginDataValidator { 6 | 7 | sealed class Result { 8 | object Success : Result() 9 | class Error(val message: String): Result() 10 | } 11 | 12 | fun checkUsername(username: String): Result { 13 | return if (username.contains('@')) { 14 | if (isEmailValid(username)) Result.Success else Result.Error("Email is not valid") 15 | } else { 16 | if (username.isNotBlank()) Result.Success else Result.Error("Username is blank") 17 | } 18 | } 19 | 20 | fun checkPassword(password: String): Result { 21 | return if (password.length > 5) Result.Success else Result.Error("Password must be >5 characters") 22 | } 23 | 24 | private fun isEmailValid(email: String) = Patterns.EMAIL_ADDRESS.matcher(email).matches() 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/data/LoginRepository.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.data 2 | 3 | import com.jetbrains.simplelogin.androidapp.data.model.LoggedInUser 4 | 5 | /** 6 | * Class that requests authentication and user information from the remote data source and 7 | * maintains an in-memory cache of login status and user credentials information. 8 | */ 9 | 10 | class LoginRepository(val dataSource: LoginDataSource) { 11 | 12 | // in-memory cache of the loggedInUser object 13 | var user: LoggedInUser? = null 14 | private set 15 | 16 | val isLoggedIn: Boolean 17 | get() = user != null 18 | 19 | init { 20 | // If user credentials will be cached in local storage, it is recommended it be encrypted 21 | // @see https://developer.android.com/training/articles/keystore 22 | user = null 23 | } 24 | 25 | fun logout() { 26 | user = null 27 | dataSource.logout() 28 | } 29 | 30 | fun login(username: String, password: String): Result { 31 | // handle login 32 | val result = dataSource.login(username, password) 33 | 34 | if (result is Result.Success) { 35 | setLoggedInUser(result.data) 36 | } 37 | 38 | return result 39 | } 40 | 41 | private fun setLoggedInUser(loggedInUser: LoggedInUser) { 42 | this.user = loggedInUser 43 | // If user credentials will be cached in local storage, it is recommended it be encrypted 44 | // @see https://developer.android.com/training/articles/keystore 45 | } 46 | } -------------------------------------------------------------------------------- /.github/workflows/build-project.yml: -------------------------------------------------------------------------------- 1 | name: Build Project 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | inputs: 10 | checkout-ref: 11 | description: "The branch, tag or SHA to checkout. See actions/checkout 'ref'." 12 | required: false 13 | type: string 14 | 15 | concurrency: 16 | group: "Build Project: ${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | 21 | validate-gradle-wrapper: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout the repo 25 | uses: actions/checkout@v4 26 | with: 27 | ref: ${{ inputs.checkout-ref || github.ref }} 28 | 29 | - name: Validate Gradle Wrapper 30 | uses: gradle/actions/wrapper-validation@v4 31 | 32 | 33 | build-gradle: 34 | needs: validate-gradle-wrapper 35 | strategy: 36 | matrix: 37 | os: 38 | - macos-latest 39 | - ubuntu-latest 40 | - windows-latest 41 | fail-fast: true 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - name: Checkout the repo 45 | uses: actions/checkout@v4 46 | with: 47 | ref: ${{ inputs.checkout-ref || github.ref }} 48 | 49 | - name: Setup JDK 50 | uses: actions/setup-java@v4 51 | with: 52 | distribution: "temurin" 53 | java-version: "21" 54 | 55 | - name: Setup Gradle 56 | uses: gradle/actions/setup-gradle@v4 57 | with: 58 | gradle-home-cache-cleanup: true 59 | cache-encryption-key: ${{ secrets.GRADLE_CONFIGURATION_CACHE_ENCRYPTION_KEY }} 60 | 61 | - name: Run tests 62 | run: ./gradlew build --scan --stacktrace 63 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.ui.login 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.jetbrains.simplelogin.androidapp.data.LoginRepository 6 | import com.jetbrains.simplelogin.androidapp.data.Result 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.StateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.launch 11 | 12 | import com.jetbrains.simplelogin.androidapp.R 13 | import com.jetbrains.simplelogin.androidapp.data.LoginDataValidator 14 | 15 | class LoginViewModel(private val loginRepository: LoginRepository, private val dataValidator: LoginDataValidator) : ViewModel() { 16 | 17 | private val _loginFormState = MutableStateFlow(LoginFormState(null, null)) 18 | val loginFormState: StateFlow = _loginFormState.asStateFlow() 19 | 20 | private val _loginResult = MutableStateFlow(null) 21 | val loginResult: StateFlow = _loginResult.asStateFlow() 22 | 23 | fun login(username: String, password: String) { 24 | // can be launched in a separate asynchronous job 25 | viewModelScope.launch { 26 | val result = loginRepository.login(username, password) 27 | 28 | if (result is Result.Success) { 29 | _loginResult.value = LoginResult(success = LoggedInUserView(displayName = result.data.displayName)) 30 | } else { 31 | _loginResult.value = LoginResult(error = R.string.login_failed) 32 | } 33 | } 34 | } 35 | 36 | fun loginDataChanged(username: String, password: String) { 37 | _loginFormState.value = LoginFormState( 38 | usernameError = (dataValidator.checkUsername(username) as? LoginDataValidator.Result.Error)?.message, 39 | passwordError = (dataValidator.checkPassword(password) as? LoginDataValidator.Result.Error)?.message 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 2 | 3 | plugins { 4 | alias(libs.plugins.androidApplication) 5 | alias(libs.plugins.kotlinAndroid) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | kotlin { 10 | compilerOptions { 11 | jvmTarget = JVM_11 12 | } 13 | } 14 | 15 | android { 16 | compileSdk = libs.versions.android.compileSdk.get().toInt() 17 | 18 | defaultConfig { 19 | applicationId = "com.jetbrains.simplelogin.androidapp" 20 | minSdk = libs.versions.android.minSdk.get().toInt() 21 | targetSdk = libs.versions.android.targetSdk.get().toInt() 22 | versionCode = 1 23 | versionName = "1.0" 24 | 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | release { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_11 39 | targetCompatibility = JavaVersion.VERSION_11 40 | } 41 | 42 | namespace = "com.jetbrains.simplelogin.androidapp" 43 | } 44 | 45 | dependencies { 46 | implementation(libs.androidx.core.ktx) 47 | implementation(libs.androidx.appcompat) 48 | implementation(libs.androidx.material) 49 | implementation(libs.androidx.annotation) 50 | implementation(libs.androidx.lifecycle.viewmodel.ktx) 51 | implementation(libs.androidx.lifecycle.runtime.ktx) 52 | implementation(libs.androidx.lifecycle.runtime.compose) 53 | implementation(libs.androidx.lifecycle.viewmodel.compose) 54 | 55 | // Compose 56 | implementation(libs.androidx.activity.compose) 57 | implementation(libs.compose.ui) 58 | implementation(libs.compose.ui.tooling.preview) 59 | implementation(libs.compose.foundation) 60 | implementation(libs.compose.material3) 61 | implementation(libs.compose.runtime) 62 | debugImplementation(libs.compose.ui.tooling) 63 | 64 | // Coroutines 65 | implementation(libs.kotlinx.coroutines.core) 66 | implementation(libs.kotlinx.coroutines.android) 67 | 68 | testImplementation(libs.junit) 69 | androidTestImplementation(libs.androidx.test.junit) 70 | androidTestImplementation(libs.androidx.espresso.core) 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.simplelogin.androidapp.ui.login 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Surface 11 | import androidx.lifecycle.viewmodel.compose.viewModel 12 | import com.jetbrains.simplelogin.androidapp.R 13 | import com.jetbrains.simplelogin.androidapp.data.LoginDataSource 14 | import com.jetbrains.simplelogin.androidapp.data.LoginDataValidator 15 | import com.jetbrains.simplelogin.androidapp.data.LoginRepository 16 | 17 | class LoginActivity : AppCompatActivity() { 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | enableEdgeToEdge() 21 | super.onCreate(savedInstanceState) 22 | 23 | setContent { 24 | MaterialTheme { 25 | val loginViewModel = viewModel { 26 | LoginViewModel( 27 | loginRepository = LoginRepository( 28 | dataSource = LoginDataSource() 29 | ), 30 | dataValidator = LoginDataValidator() 31 | ) 32 | } 33 | 34 | Surface() { 35 | LoginScreen( 36 | viewModel = loginViewModel, 37 | onLoginSuccess = { 38 | // Show welcome message 39 | val successResult = it.success 40 | successResult?.let { 41 | val welcome = getString(R.string.welcome) 42 | Toast.makeText( 43 | applicationContext, 44 | "$welcome ${it.displayName}", 45 | Toast.LENGTH_LONG 46 | ).show() 47 | } 48 | 49 | // Complete the login process 50 | setResult(Activity.RESULT_OK) 51 | finish() 52 | } 53 | ) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 25 | 26 | 39 | 40 |