[android : kotlin] 코틀린 shared Preferences 사용방법 및 예제 코드 : 자동 로그인 처리하기
코틀린을 사용하여 안드로이드 개발시 sharedPreferences를 사용하는 방법에 대해 알아봅니다.
간단하게 안드로이드 스튜디오에서 지원하는 템플릿을 이용하여 로그인 액티비티를 추가합니다.
app폴더 위에서 마우스 오른쪽 버튼 클릭 후 New > Activity > Login Activity 를 선택합니다.
자동으로 ui.login 패키지가 생성되며 그 안에 로그인 관련 클래스 파일들이 생성되며, activity_login.xml 레이아웃도 함께 생성됩니다. 자동으로 생성된 로그인 레이아웃의 상단의 LOGIN 텍스트박스와 자동 로그인 체크박스를 추가하였습니다.
[activity_login.xml]
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".ui.login.LoginActivity"> <EditText android:id="@+id/username" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="96dp" android:layout_marginEnd="24dp" android:hint="@string/prompt_email" android:inputType="textEmailAddress" android:selectAllOnFocus="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/password" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="8dp" android:layout_marginEnd="24dp" android:hint="@string/prompt_password" android:imeActionLabel="@string/action_sign_in_short" android:imeOptions="actionDone" android:inputType="textPassword" android:selectAllOnFocus="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/username" /> <Button android:id="@+id/login" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" android:layout_marginStart="48dp" android:layout_marginTop="16dp" android:layout_marginEnd="48dp" android:layout_marginBottom="64dp" android:enabled="false" android:text="@string/action_sign_in" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/password" app:layout_constraintVertical_bias="0.2" /> <ProgressBar android:id="@+id/loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginStart="32dp" android:layout_marginTop="64dp" android:layout_marginEnd="32dp" android:layout_marginBottom="64dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/password" app:layout_constraintStart_toStartOf="@+id/password" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.3" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="LOGIN" android:textSize="30sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <CheckBox android:id="@+id/autoLoginCheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="자동 로그인" android:textAppearance="@style/TextAppearance.AppCompat.Body1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/password" /> </androidx.constraintlayout.widget.ConstraintLayout>
코틀린 shared Preferences 사용방법
자동로그인 체크 후 SIGN IN OR REGISTER 버튼을 클릭시 shared Preferences를 사용하여 로그인 정보를 읽고 쓰는 방법에 대해 알아봅니다.
전역변수로 sharedPreference, editor를 선언하였습니다.
private lateinit var sharedPreferences: SharedPreferences private lateinit var editor: SharedPreferences.Editor
onCreate() 메서드 안에서 늦은초기화(lateinit)를 처리를 하였습니다.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ~~~~ 생략 ~~~~ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) editor = sharedPreferences.edit() checkSharedPreference() ~~~~ 생략 ~~~~ }
checkSharedPreference()메서드를 하나 생성하여 shared Preference 기록한 정보를 체크 합니다.
private fun checkSharedPreference() { autoLogin.isChecked = sharedPreferences.getBoolean(getString(R.string.auto_login), false) username.setText(sharedPreferences.getString(getString(R.string.prompt_email), "")) password.setText(sharedPreferences.getString(getString(R.string.prompt_password), "")) }
SIGN IN OR REGISTER 버튼을 클릭시 setOnClickListener 에서 shared Preferences에 저장하는 코드를 추가합니다.
apply()는 저장 시점을 보장하지 않지만 저장됩니다. commit()는 반드시 저장 후 처리됩니다.
login.setOnClickListener { if (autoLogin.isChecked) { editor.putBoolean(getString(R.string.auto_login), true) editor.apply() editor.putString(getString(R.string.prompt_email), username.text.toString()) editor.putString(getString(R.string.prompt_password), password.text.toString()) editor.commit() } else { editor.putBoolean(getString(R.string.auto_login), false) editor.putString(getString(R.string.prompt_email), "") editor.putString(getString(R.string.prompt_password), "") editor.commit() } }
테스트 가능한 샘플코드 전체는 다음과 같습니다.
[LoginActivity.kt]
package edu.kotlin.study.ui.login import android.app.Activity import android.content.SharedPreferences import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import android.os.Bundle import android.preference.PreferenceManager import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import android.text.Editable import android.text.TextWatcher import android.view.View import android.view.inputmethod.EditorInfo import android.widget.* import edu.kotlin.study.R import kotlinx.android.synthetic.main.activity_login.* class LoginActivity : AppCompatActivity() { private lateinit var loginViewModel: LoginViewModel private lateinit var sharedPreferences: SharedPreferences private lateinit var editor: SharedPreferences.Editor private lateinit var autoLogin: CheckBox private lateinit var username: EditText private lateinit var password: EditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) username = findViewById(R.id.username) password = findViewById(R.id.password) val login = findViewById<Button>(R.id.login) val loading = findViewById<ProgressBar>(R.id.loading) autoLogin = findViewById(R.id.autoLoginCheckBox) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) editor = sharedPreferences.edit() checkSharedPreference() loginViewModel = ViewModelProviders.of(this, LoginViewModelFactory()) .get(LoginViewModel::class.java) loginViewModel.loginFormState.observe(this@LoginActivity, Observer { val loginState = it ?: return@Observer // disable login button unless both username / password is valid login.isEnabled = loginState.isDataValid if (loginState.usernameError != null) { username.error = getString(loginState.usernameError) } if (loginState.passwordError != null) { password.error = getString(loginState.passwordError) } }) loginViewModel.loginResult.observe(this@LoginActivity, Observer { val loginResult = it ?: return@Observer loading.visibility = View.GONE if (loginResult.error != null) { showLoginFailed(loginResult.error) } if (loginResult.success != null) { updateUiWithUser(loginResult.success) } setResult(Activity.RESULT_OK) //Complete and destroy login activity once successful finish() }) username.afterTextChanged { loginViewModel.loginDataChanged( username.text.toString(), password.text.toString() ) } password.apply { afterTextChanged { loginViewModel.loginDataChanged( username.text.toString(), password.text.toString() ) } setOnEditorActionListener { _, actionId, _ -> when (actionId) { EditorInfo.IME_ACTION_DONE -> loginViewModel.login( username.text.toString(), password.text.toString() ) } false } login.setOnClickListener { if (autoLogin.isChecked) { editor.putBoolean(getString(R.string.auto_login), true) editor.apply() editor.putString(getString(R.string.prompt_email), username.text.toString()) editor.putString(getString(R.string.prompt_password), password.text.toString()) editor.commit() } else { editor.putBoolean(getString(R.string.auto_login), false) editor.putString(getString(R.string.prompt_email), "") editor.putString(getString(R.string.prompt_password), "") editor.commit() } loading.visibility = View.VISIBLE loginViewModel.login(username.text.toString(), password.text.toString()) } } } private fun updateUiWithUser(model: LoggedInUserView) { val welcome = getString(R.string.welcome) val displayName = model.displayName // TODO : initiate successful logged in experience Toast.makeText( applicationContext, "$welcome $displayName", Toast.LENGTH_LONG ).show() } private fun showLoginFailed(@StringRes errorString: Int) { Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show() } private fun checkSharedPreference() { autoLogin.isChecked = sharedPreferences.getBoolean(getString(R.string.auto_login), false) username.setText(sharedPreferences.getString(getString(R.string.prompt_email), "")) password.setText(sharedPreferences.getString(getString(R.string.prompt_password), "")) } } /** * Extension function to simplify setting an afterTextChanged action to EditText components. */ fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { this.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(editable: Editable?) { afterTextChanged.invoke(editable.toString()) } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} }) }
[LoginFormState]
package edu.kotlin.study.ui.login /** * Data validation state of the login form. */ data class LoginFormState( val usernameError: Int? = null, val passwordError: Int? = null, val isDataValid: Boolean = false )
[LoginResult]
package edu.kotlin.study.ui.login /** * Authentication result : success (user details) or error message. */ data class LoginResult( val success: LoggedInUserView? = null, val error: Int? = null )
[LoggedInUserView]
package edu.kotlin.study.ui.login /** * User details post authentication that is exposed to the UI */ data class LoggedInUserView( val displayName: String //... other data fields that may be accessible to the UI )
[LoginViewModel]
package edu.kotlin.study.ui.login import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import android.util.Patterns import edu.kotlin.study.data.LoginRepository import edu.kotlin.study.data.Result import edu.kotlin.study.R class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { private val _loginForm = MutableLiveData<LoginFormState>() val loginFormState: LiveData<LoginFormState> = _loginForm private val _loginResult = MutableLiveData<LoginResult>() val loginResult: LiveData<LoginResult> = _loginResult fun login(username: String, password: String) { // can be launched in a separate asynchronous job val result = loginRepository.login(username, password) if (result is Result.Success) { _loginResult.value = LoginResult(success = LoggedInUserView(displayName = result.data.displayName)) } else { _loginResult.value = LoginResult(error = R.string.login_failed) } } fun loginDataChanged(username: String, password: String) { if (!isUserNameValid(username)) { _loginForm.value = LoginFormState(usernameError = R.string.invalid_username) } else if (!isPasswordValid(password)) { _loginForm.value = LoginFormState(passwordError = R.string.invalid_password) } else { _loginForm.value = LoginFormState(isDataValid = true) } } // A placeholder username validation check private fun isUserNameValid(username: String): Boolean { return if (username.contains('@')) { Patterns.EMAIL_ADDRESS.matcher(username).matches() } else { username.isNotBlank() } } // A placeholder password validation check private fun isPasswordValid(password: String): Boolean { return password.length > 5 } }
[LoginViewModelFactory]
package edu.kotlin.study.ui.login import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import edu.kotlin.study.data.LoginDataSource import edu.kotlin.study.data.LoginRepository /** * ViewModel provider factory to instantiate LoginViewModel. * Required given LoginViewModel has a non-empty constructor */ class LoginViewModelFactory : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { return LoginViewModel( loginRepository = LoginRepository( dataSource = LoginDataSource() ) ) as T } throw IllegalArgumentException("Unknown ViewModel class") } }
[build.gradle(Module:app)]
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 defaultConfig { applicationId "edu.kotlin.study" minSdkVersion 22 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }
[build.gradle(Project)]
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
[연관 자료]