[Kotlin] 13 - Shared preference & ViewModel

2024. 6. 27. 10:38· 공부/Kotlin
목차
  1. 실습 프로젝트
  2. Android Application Local Data
  3. Context(문맥)
  4. SharedPreference
  5. String 정의
  6. activity_main.xml 작성
  7. MainActivity
  8. 문제점
  9. ViewModel
  10. ViewModel의 수명 주기
  11. ViewModel과 LiveData를 위한 Dependency
  12. ViewModel 사용하기
  13. MainActivity에서 불러 쓰기
  14. ViewModel 코드
  15. ViewModel 코드
반응형

실습 프로젝트

  • 이름 : TEA(Time Elapsed After..)
  • package : com.example.tea
  • 음식을 먹을 때마다 기록하면, 마지막으로 음식을 먹은 후 경과 시간을 볼 수 있다.
  • View Binding 적용

Android Application Local Data

  • 중요한 데이터는 Back-end 서버에 저장하는 것이 맞지만 각 개인의 앱 설정 데이터, 서버 데이터의 캐시 등은 Local Data로 저장하는 것이 좋다.
  • Shared Preference: key-value 형식의 간단한 데이터를 파일에 저장.
    • 앱이 종료되어도 데이터가 유지된다.
    • 참고) 설정 값 만을 저장하는 것은 Preference
    안드로이드에 앱이 깔리면 안드로이드가 해당 앱에 마음대로 읽고 쓰기를 할 수 있는 폴더를 할당해준다. 해당 폴더에 key-value 형식의 간단한 데이터를 파일로 저장한다.
  • ViewModel: UI가 유지될 동안 유지되는 데이터

안드로이드에서는 파일형 DB인 SQLite를 각 어플리케이션에 지원하고 있다.

Context(문맥)

  • Context(문맥) = 생략되어 있어도 무엇을 대상으로 하고 있는지?
  • Abstract class - 구현은 안드로이드 시스템에서 제공
  • 앱 환경에 대한 전역 정보를 가지는 interface - resource, database, classes등 앱 레벨 정보에 접근할 수 있게 해 줌
  • 주요 subclass들
    • Activity, Application, Service 등
  • Application Context: 싱글 인스턴스. 앱의 라이프사이클과 연결
  • Activity Context: Activity의 라이프라이클과 연결

val str = String(””) ← 금방 사라지기 때문에 Context가 아니다.

LifeCycle을 가지는 것들 :
Application —————————>

→ MainActivity → CourseActivity

  • Application이 LifeCycle이 가장 길다(오랫동안 살아있다)
  • CourseActivity가 화면으로 가도 MainActivity가 꺼진것은 아니다.

Application, Activity ← 둘 다 Context(문맥)임

val intent = Intent(this, ChatRoomActivity::class.java)

여기에서 Intent의 생성자의 this 부분이 Context이다.

Context : 나는 누구와 함께 LifeCycle을 함께 하는 것인가? 라고 생각하면 됨

SharedPreference

  • Key-value 형태의 파일로 저장 - 앱이 재시작되어도 이전 기록이 남는다.
  • 파일 생성 및 파일 내용을 불러오기
    • getSharedPreferences(파일이름, Context.MODE_PRIVATE)
      • 앱의 모든 Context*들이 불러 쓸 수 있는 파일을 이름을 지정하여 생성
    • getPreferences(Context.MODE_PRIVATE): 특정 Activity에 속한 파일 생성

현재 보안상의 문제로 PUBLIC 등 다른 모드들이 없어져 Context.MODE_PRIVATE밖에 사용하지 못함

  • 값 읽기
    • sharedPreference 객체에서 getInt, getString과 같은 함수를 호출한다.
  • 값 쓰기
    • sharedPreference.edit()을 통해 editor 객체를 받고
    • editor.putInt(), editor.putString()등의 함수로 객체에 새 값을 저장한 다음
    • editor.apply() 를 통해 최종 파일에 저장한다.
  • editor.commit도 있지만 동기식이라서 저장중에는 UI가 멈춘다. 그래서 비동기식인 apply를 개발자가 자주 사용함

getSharedPreferences(파일이름, Context.MODE_PRIVATE)를 하면 해당 파일을 읽어서 메모리 상에 올려서 사용을 하는 것이기 때문에 해당 파일을 수정을 하면 메모리 상에서만 수정이 된것이기 때문에 꼭 apply로 저장을 해주어야 함

  • 값 읽기와 값 쓰기 예

edit.apply() - 수정된 값을 객체에 즉시 반영하고 파일 저장은 비동기로 수행한다.

edit.commit() - 수정된 값을 객체에 즉시 반영하고 파일 저장을 동기로 수행한다.

String 정의

res/values/strings.xml

<resources>
    <string name="app_name">TEA</string>
    <string name="elapsed_time_default">00:00:00</string>
    <string name="elapsed_time">%02d:%02d:%02d</string>
    <string name="no_time_saved">저장된 시간이 없습니다.</string>
    <string name="no_food_saved">저장된 음식 정보가 없습니다.</string>
    <string name="enter_food_name">섭취하신 음식을 입력하세요.</string>
    <string name="save">저장</string>
</resources>

activity_main.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/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/textViewLastTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/no_time_saved"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.15" />
        <TextView
            android:id="@+id/textViewLastFood"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/no_food_saved"
            app:layout_constraintEnd_toEndOf="@+id/textViewLastTime"
            app:layout_constraintStart_toStartOf="@+id/textViewLastTime"
            app:layout_constraintTop_toBottomOf="@+id/textViewLastTime" />
        <TextView
            android:id="@+id/textViewElapsedTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:text="@string/elapsed_time_default"
            android:textSize="30sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textViewLastFood" />
        <EditText
            android:id="@+id/editTextFood"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:autofillHints=""
            android:ems="12"
            android:gravity="center"
            android:hint="@string/enter_food_name"
            android:inputType="text"
            android:minHeight="48dp"
            android:textColorHint="#546E7A"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textViewElapsedTime" />
        <Button
            android:id="@+id/buttonSave"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/save"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/editTextFood" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

ElapsedTime Data Class 추가

data class ElapsedTime (
    val hours:Long = 0,
    val minutes:Long = 0,
    val seconds:Long = 0
)

MainActivity

  • getElapstedTime, saveRecord 함수를 직접 구현해본다.
class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } // binding 생성
    private lateinit var preference:SharedPreferences // SharedPreferences 변수 생성

    overri @de fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root) // binding.root를 화면에 표시
        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        preference = getSharedPreferences("food_record", MODE_PRIVATE) // SharedPreferences 객체 생성
        val lastFood = preference.getString("food", null) // food_record.xml 파일에서 food 키의 값을 가져옴
        val lastTime = preference.getString("time", null) // food_record.xml 파일에서 time 키의 값을 가져옴
        updateWidgets(lastFood, lastTime) // updateWidgets 함수 호출

        binding.buttonSave.setOnClickListener{ saveRecord() }
    }

    private fun saveRecord() {
        val time = LocalDateTime.now().toString() // 현재 시간을 문자열로 변환
        val food = binding.editTextFood.text.toString() // editTextFood의 텍스트를 가져와 food 변수에 저장
        if(food.isNotEmpty()) {
            preference.edit().run {
                putString("food", food)
                putString("time", time)
                apply()
            }
            updateWidgets(food, time)
        }
    }

    private fun getElapsedTime(time:String?):ElapsedTime {
        if(time == null)
            return ElapsedTime()
        val now = LocalDateTime.now() // 현재 시간을 LocalDateTime 객체로 변환
        val before = LocalDateTime.parse(time) // 문자열로 된 시간을 LocalDateTime 객체로 변환

        val hours = ChronoUnit.HOURS.between(before, now) // 시간 차이를 시간 단위로 계산
        val minutes = ChronoUnit.MINUTES.between(before, now) % 60 // 시간 차이를 분 단위로 계산
        val seconds = ChronoUnit.SECONDS.between(before, now) % 60 // 시간 차이를 초 단위로 계산

        return ElapsedTime(hours, minutes, seconds) // ElapsedTime 객체 반환
    }

    private fun updateWidgets(lastFood:String?, lastTime:String?) {
        val elapsedTime = getElapsedTime(lastTime)

        binding.textViewLastFood.text = lastFood?:getString(R.string.no_food_saved)
        binding.textViewLastTime.text = lastTime?:getString(R.string.no_time_saved)
        binding.textViewElapsedTime.text =
            getString(R.string.elapsed_time, elapsedTime.hours, elapsedTime.minutes, elapsedTime.seconds)
    }
}

time을 DateTime으로 저장안한 이유 SharedPreferences는 DateTime을 저장하지 못함

문제점

  • 경과 시간이 업데이트가 안된다.
  • Timer를 사용하면?
  • 안드로이드에서는 Original Thread(MainThread) 에서만 UI를 업데이트 할 수 있도록 허용한다.
  • 타이머를 돌리는 것은 가능하지만 해당 정보로 UI를 업데이트하는데 제약이 있다.

Activity ——————————————————————————> (계속유지)

onCreate → onStart → onResume → 가로/세로 변경 → onCreate부터 다시 호출


문제점 : Activity를 새로 만드는건 아니지만 레이아웃이 다시 그려지면서 작성된 데이터가 날아갈 수도 있다.


Activity가 살아 있는 동안 유지되는 데이터를 만들어주세요. (가로, 세로 변경되어도 데이터는 유지되도록 해주세요 → onCreate가 다시 불려도 초기화 되지 않는 데이터) == ViewModel

ViewModel

내 앱이 켜져서 꺼질 때까지는 유지되는 데이터

  • ViewModel: Activity, Fragment 등의 요소의 수명 주기를 고려한 UI 관련 데이터 저장소. -> 화면 회전 등의 경우에 데이터를 보존할 수 있다.
  • LiveData: 자신의 정보가 변경되면 Observer 객체에게 이를 알리는 데이터.
  • Observer: ViewModel을 관찰하는 클래스. Observer가 START 또는 RESUMED 상태 일때 관찰자가 활성화 되었다고 판단. -> 활성화된(엑티비티가 화면에 떠있는 상태) 관찰자에게만 업데이트를 알림.

LiveData → 관찰자에게 데이터의 업데이트를 알려줌

Activity가 Observe로 LiveData를 구독한다고 생각하면 됨

안드로이드에서는 평시적으로 Background Thread에서는 UI를 새로 고치는걸 못하는데 ViewModel을 사용하면 Background Thread에서도 UI를 업데이트를 할 수 있다.

  • 기존 구조
    • 데이터를 변경한 함수에서 UI 업데이트까지 수행해야 함
    • MainThread 에서만 UI 변경
  • ViewModel을 사용하면:
    • (실행되는 Thread 상관 없이)함수에서는 데이터만 변경.
    • 관련된 observer가 UI를 업데이트한다
    → 즉, 로직을 수행하는 함수에서 UI 구성까지 알지 않아도 되며 Thread를 신경쓰지 않아도 된다.

ViewModel의 수명 주기

  • Activity의 회전 등에 의해 onCreate 가 다시 실행되어도 ViewModel은 유지된다.
  • 서버로 전송할 영구 데이터가 아닌 UI를 그리는데 필요한 데이터를 적절히 보관한다

ViewModel과 LiveData를 위한 Dependency

  • libs.versions.toml
[versions] 
...
livedata = "2.7.0"

[libraries]
androidx-viewmodel = {group="androidx.lifecycle", name="lifecycle-viewmodel-ktx", version.ref="livedata"} 
androidx-livedata = {group="androidx.lifecycle", name="lifecycle-livedata-ktx", version.ref="livedata"}
  • build.gradle.kts(Module:app)
dependencies { 
	...
	implementation(libs.androidx.livedata) 
	implementation(libs.androidx.viewmodel) 
}

ViewModel 사용하기

  • ViewModel은 일반 객체처럼 생성해서는 안된다. → 처음에만 생성하고 그 이후 onCreate 될 때 다시 생성되면 안되기 때문에
  • → 처음에 ViewModel 생성 후 다른 곳에서는 이미 만들어진 ViewModel을 참조하는 형식으로 사용
  • ViewModel 객체를 생성할 때 ViewModelProvider를 사용한다.
    • val model:MyViewModel = ViewModelProvider(this)[MyViewModel::class.java]
  •  libs.version.toml, build.gradle(Module:app) 에서 각각 다음 확인
    • androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
    • implementation(libs.androidx.activity) 
      • Delegation → by viewModels() 를 사용한다.
        • val model:MyViewModel by viewModels() 또는 val viewModel by viewModels<MainViewModel>()

MainActivity에서 불러 쓰기

  • 변수 선언부
import android.content.SharedPreferences
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.lunadev.tea.ElapsedTime
import com.lunadev.tea.MainViewModel
import com.lunadev.tea.R
import com.lunadev.tea.databinding.ActivityMainBinding
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.Timer

class MainActivity : AppCompatActivity() {
    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } // binding 생성
    private lateinit var preference:SharedPreferences // SharedPreferences 변수 생성
    private val timer = Timer()
    private val viewModel: MainViewModel by viewModels() // ViewModel 객체 생성

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root) // binding.root를 화면에 표시
        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        preference = getSharedPreferences("food_record", MODE_PRIVATE) // SharedPreferences 객체 생성
        val lastFood = preference.getString("food", null) // food_record.xml 파일에서 food 키의 값을 가져옴
        val lastTime = preference.getString("time", null) // food_record.xml 파일에서 time 키의 값을 가져옴
        updateWidgets(lastFood, lastTime) // updateWidgets 함수 호출

        binding.buttonSave.setOnClickListener{ saveRecord() }

        viewModel.lastFood.value = lastFood // ViewModel의 lastFood 변수를 변경함
        viewModel.lastTime.value = lastTime // ViewModel의 lastTime 변수를 변경함

        // LiveData observe값이 수정될 경우 람다식이 동작하여 UI가 업데이트 됨
        viewModel.lastFood.observe(this) { binding.textViewLastFood.text = it }
        viewModel.lastTime.observe(this) { binding.textViewLastTime.text = it }
        viewModel.updateElapsedTime.observe(this){
            val elapsedTime = getElapsedTime(viewModel.lastTime.value)
            binding.textViewElapsedTime.text = getString(
                R.string.elapsed_time,
                elapsedTime.hours,
                elapsedTime.minutes,
                elapsedTime.seconds)
        }

        viewModel.startTimer() // BackgroundThread에서 LiveData 값 수정
    }

    private fun saveRecord() {
        val time = LocalDateTime.now().toString() // 현재 시간을 문자열로 변환
        val food = binding.editTextFood.text.toString() // editTextFood의 텍스트를 가져와 food 변수에 저장
        if(food.isNotEmpty()) {
            preference.edit().run {
                putString("food", food)
                putString("time", time)
                apply()
            }
            viewModel.lastTime.value = time
            viewModel.lastFood.value = food
            // MainThread에서 LiveData 값 수정 값만 수정하고 UI 업데이트는 신경 쓰지 않아도 됨
        }
    }

    private fun getElapsedTime(time:String?): ElapsedTime {
        if(time == null)
            return ElapsedTime()
        val now = LocalDateTime.now() // 현재 시간을 LocalDateTime 객체로 변환
        val before = LocalDateTime.parse(time) // 문자열로 된 시간을 LocalDateTime 객체로 변환

        val hours = ChronoUnit.HOURS.between(before, now) // 시간 차이를 시간 단위로 계산
        val minutes = ChronoUnit.MINUTES.between(before, now) % 60 // 시간 차이를 분 단위로 계산
        val seconds = ChronoUnit.SECONDS.between(before, now) % 60 // 시간 차이를 초 단위로 계산

        return ElapsedTime(hours, minutes, seconds) // ElapsedTime 객체 반환
    }

    private fun updateWidgets(lastFood:String?, lastTime:String?) {
        val elapsedTime = getElapsedTime(lastTime)

        binding.textViewLastFood.text = lastFood?:getString(R.string.no_food_saved)
        binding.textViewLastTime.text = lastTime?:getString(R.string.no_time_saved)
        binding.textViewElapsedTime.text =
            getString(R.string.elapsed_time, elapsedTime.hours, elapsedTime.minutes, elapsedTime.seconds)
    }

}

ViewModel 코드

package com.lunadev.tea

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.Timer
import kotlin.concurrent.schedule
class MainViewModel: ViewModel() {
    val lastTime = MutableLiveData<String>().apply { value="00" }
    val lastFood = MutableLiveData<String>().apply { value="00" }
    val updateElapsedTime = MutableLiveData<Boolean>()
    private val timer = Timer()
    fun startTimer(){
        timer.schedule(1000, 1000) {
            // Background Thread 에서는 postValue 를 사용해 값을 업데이트한다.
            updateElapsedTime.postValue(true)
        }
    }
    override fun onCleared() {
        super.onCleared()
        timer.cancel()
    }
}

ViewModel 코드

package com.lunadev.tea

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.Timer
import kotlin.concurrent.schedule
class MainViewModel: ViewModel() {
    val lastTime = MutableLiveData<String>().apply { value="00" }
    val lastFood = MutableLiveData<String>().apply { value="00" }
    val updateElapsedTime = MutableLiveData<Boolean>()
    private val timer = Timer()
    fun startTimer(){
        timer.schedule(1000, 1000) {
            // Background Thread 에서는 postValue 를 사용해 값을 업데이트한다.
            updateElapsedTime.postValue(true)
        }
    }
    override fun onCleared() {
        super.onCleared()
        timer.cancel()
    }
}
반응형
저작자표시 비영리 (새창열림)
  1. 실습 프로젝트
  2. Android Application Local Data
  3. Context(문맥)
  4. SharedPreference
  5. String 정의
  6. activity_main.xml 작성
  7. MainActivity
  8. 문제점
  9. ViewModel
  10. ViewModel의 수명 주기
  11. ViewModel과 LiveData를 위한 Dependency
  12. ViewModel 사용하기
  13. MainActivity에서 불러 쓰기
  14. ViewModel 코드
  15. ViewModel 코드
'공부/Kotlin' 카테고리의 다른 글
  • [Kotlin] 15 - Fragment의 활용
  • [Kotlin] 14 - Permissions
  • [Kotlin] 12 - Android RecyclerView
  • [Kotlin] 9 - Widget And Listener
Future0_
Future0_
rm -rf /
Future0_
Luna Developer Blog
Future0_
전체
오늘
어제
  • 분류 전체보기 (112)
    • 프로그래밍 (4)
      • 알고리즘 (4)
    • 보안 (14)
      • Dreamhack (4)
      • Hackthebox (1)
      • Webhacking (9)
    • 프로젝트 (4)
    • 공부 (80)
      • Database (2)
      • Python (11)
      • System (4)
      • Java (13)
      • JSP (13)
      • Spring (11)
      • Kotlin (16)
      • 자료구조 (10)
      • 기계학습 (0)
    • Docker (4)
    • Github (2)
    • Tip (1)
    • 잡담 (2)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • React
  • 프로그래밍
  • Python
  • 컴퓨터
  • docker
  • Database
  • 1.9.22
  • 코틀린기본문법
  • android studio 삭제
  • 알고리즘
  • api 통신
  • Kotlin
  • 자바빈즈
  • Android Studio
  • SpringBoot
  • dreamhack
  • 상속
  • jsp
  • webhacking
  • 보안
  • cs
  • shared preference
  • 디버깅키해시
  • Java
  • Computer science
  • 자료구조
  • 키 해시
  • native app
  • spring
  • ViewModel

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
Future0_
[Kotlin] 13 - Shared preference & ViewModel
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.