안드로이드 앱 개발을 진행하다 보면, 앱의 데이터들을 저장하여 관리해야 할 상황이 존재한다. 데이터의 양이 많거나 중요 데이터의 경우 서버나 DB에 저장해야겠지만, 간단한 설정 값이나 문자열 같은 데이터를 저장하기 위해 DB를 사용하기는 부담스럽기 때문에 SharedPreferences를 사용하는 것이 적합하다.
SharedPreferences의 특징
보통 초기 설정값이나 자동 로그인 여부 등 간단한 값을 저장하기 위해 사용
Application에 파일 형태로 데이터를 저장한다.
Application이 삭제되기 전까지 저장한 데이터가 보존된다.
Key-value 방식
MODE의 종류
MODE_PRIVATE : 생성한 Application에서만 사용 가능하다.
MODE_WORLD_READABLE : 외부 App에서 사용 가능, But 읽기만 가능
MODE_WORLD_WRITEABLE : 외부 App에서 사용 가능, 읽기/쓰기 가능
대부분은 MODE_PRIVATE를 많이 사용한다.
사용 예시
binding.btnSave.setOnClickListener { //데이터 저장 버튼 눌렸을 때
val sharedPreferences = getSharedPreferences("파일이름", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("key",binding.editText.text.toString()) //"key" ,"value" 형태 값을 가져올 때 "key" 가 맞아야함
editor.apply() //or commit() // apply() or commit()을 해줘야 적용됨
Toast.makeText(this, "데이터 저장 완료", Toast.LENGTH_SHORT).show()
}
val sharedPreferences = getSharedPreferences("EditText", Context.MODE_PRIVATE)
binding.editText.setText(sharedPreferences.getString("key", "디폴트 값"))
binding.btnDelete.setOnClickListener {//데이터 삭제 버튼 눌렸을 때
val sharedPreferences = getSharedPreferences("EditText", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.clear()//데이터 삭제
editor.apply()//적용
Toast.makeText(this, "데이터 삭제", Toast.LENGTH_SHORT).show()
binding.editText.setText("")
}
}
override fun onResume() {
getSharedPreferences("파일이름", Context.MODE_PRIVATE).run {
binding.editText.setText(getString("key", "디폴트 값"))
}
super.onResume()
}
//요청된 권한 결과 처리
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val readImagePermissionGranted =
requestCode == REQUEST_READ_MEDIA_IMAGES_CODE && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
if (readImagePermissionGranted) { //request코드 와 권한이 허용된 상태면 loadImages()
loadImage()
} else {
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.READ_MEDIA_IMAGES)){
showRequestRationalDialog() //권한이 거부되면 교육용 다이얼로그
}
else{
showSettingDialog() //다시 취소시 세팅다이얼로그
}
}
}
추가로, 교육용 다이얼로그에서도 거부가 되면 showSettingDialog() 가 뜨는데 setPositiveButton 시 디테일 세팅 으로 이동하는 코드
private fun navigateToSettings() { //권한이 취소된 상태에서 다이얼로그에서 확인 눌렀을 때 세팅화면으로 이동
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
전체 코드
class SearchActivity : AppCompatActivity() {
//request code
companion object {
private const val REQUEST_READ_MEDIA_IMAGES_CODE = 100
}
//
private val loadImageLauncher =
registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uriList ->
changeImage(uriList)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
val toolBar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolBar)
val loadImageButton = findViewById<Button>(R.id.btn_LoadImage)
toolBar.apply {
title = "SearchActivity"
setSupportActionBar(this)
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
loadImageButton.setOnClickListener {
checkPermission()
}
}
//선택한 uriList 를 image로 세팅
private fun changeImage(uriList: List<Uri>) {
val image = findViewById<ImageView>(R.id.iv_Image)
image.setImageURI(uriList[0])
}
//갤러리 접근
private fun loadImage() {
loadImageLauncher.launch("image/*")
}
//권한 요청
private fun requestReadMediaImages() {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.READ_MEDIA_IMAGES),
REQUEST_READ_MEDIA_IMAGES_CODE
)
}
//권한 확인
private fun checkPermission() {
when {
ActivityCompat.checkSelfPermission(
this,
Manifest.permission.READ_MEDIA_AUDIO
) == PackageManager.PERMISSION_GRANTED -> {
loadImage()
}
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.READ_MEDIA_AUDIO
) -> {
showRequestRationalDialog()
}
else -> requestReadMediaImages()
}
}
private fun showRequestRationalDialog() {
AlertDialog.Builder(this).apply {
setMessage("사진을 가져오려면 권한이 필요합니다.")
setNegativeButton("취소") { dialogInterface, _ ->
dialogInterface.cancel()
}
setPositiveButton("확인") { _, _ -> requestReadMediaImages() }
show()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
else -> super.onOptionsItemSelected(item)
}
}
//요청된 권한 결과 처리
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val readImagePermissionGranted =
requestCode == REQUEST_READ_MEDIA_IMAGES_CODE && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
if (readImagePermissionGranted) { //request코드 와 권한이 허용된 상태면 loadImages()
loadImage()
} else {
if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.READ_MEDIA_IMAGES)){
showRequestRationalDialog() //권한이 거부되면 교육용 다이얼로그
}
else{
showSettingDialog() //다시 취소시 세팅다이얼로그
}
}
}
private fun showSettingDialog() {//셋팅 다이얼로그
AlertDialog.Builder(this).apply {
setMessage("권한이 취소된 상태입니다. 권한 허용 하러가기")
setNegativeButton("취소") { dialogInterface, _ -> dialogInterface.cancel() }
setPositiveButton("확인") { _, _ -> navigateToSettings() }
show()
}
}
private fun navigateToSettings() { //권한이 취소된 상태에서 다이얼로그에서 확인 눌렀을 때 세팅화면으로 이동
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
}
private fun check(editText: EditText): Boolean { //특수문자 숫자 정규식
val pwd = """^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^+\-=])(?=\S+$).*$"""
val pattern = Pattern.compile(pwd)
return pattern.matcher(editText.text).matches()
}
matches() 리턴 값 -> bollean
SignUpActivity 전체 코드
class SignUpActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_up)
val signUpButton = findViewById<Button>(R.id.btn_SignUp)
val id = findViewById<EditText>(R.id.et_Id)
val password = findViewById<EditText>(R.id.et_Password)
val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
val email = findViewById<EditText>(R.id.et_Email)
val spinner = findViewById<Spinner>(R.id.spinner)
val emailForm = findViewById<EditText>(R.id.et_EmailForm)
createSpinner(spinner, emailForm)
validation(id)
validation(email)
validation(password)
validation(confirmPassword)
focusOut(id)
focusOut(email)
focusOut(password)
focusOut(confirmPassword)
signUpButton.setOnClickListener {
if (id.text.isNullOrEmpty() || confirmPassword.text.isNullOrEmpty() || password.text.isNullOrEmpty() || email.text.isNullOrEmpty() || emailForm.text.isNullOrEmpty()) {
Toast.makeText(this, "입력값이 비어있습니다.", Toast.LENGTH_SHORT).show()
return@setOnClickListener
} else {//모든 필드가 입력된 상태
if (isEqualPassword() && check(password)) { //유효성 검사 후 승인
Intent().apply {
putExtra("updatedId", id.text.toString())
putExtra("updatedPassword", password.text.toString())
setResult(RESULT_OK, this)
}
saveData()
finish()
} else if (check(password)) { //password 와 confirmPassword가 같을 때 처리
Toast.makeText(this, "비밀번호가 일치하지 않습니다.", Toast.LENGTH_SHORT).show()
} else { //둘 다 아닐 때
Toast.makeText(this, "비밀번호는 10자리 이상, 특수문자, 숫자포함 입니다.", Toast.LENGTH_SHORT)
.show()
}
}
}
}
private fun saveData() { //입력 값 저장
val id = findViewById<EditText>(R.id.et_Id)
val password = findViewById<EditText>(R.id.et_Password)
val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
val email = findViewById<EditText>(R.id.et_Email)
val emailForm = findViewById<EditText>(R.id.et_EmailForm)
getSharedPreferences(INFO, Context.MODE_PRIVATE)?.edit {
putString(ID, id.text.toString())
putString(EMAIL, email.text.toString())
putString(EMAIL_FORM, emailForm.text.toString())
putString(PASSWORD, password.text.toString())
putString(CONFIRM_PASSWORD, confirmPassword.text.toString())
apply()
}
}
private fun getSaveData() { //저장한 값 불러오기
val id = findViewById<EditText>(R.id.et_Id)
val password = findViewById<EditText>(R.id.et_Password)
val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
val email = findViewById<EditText>(R.id.et_Email)
val emailForm = findViewById<EditText>(R.id.et_EmailForm)
getSharedPreferences(INFO, Context.MODE_PRIVATE).run {
id.setText(getString(ID, ""))
email.setText(getString(EMAIL, ""))
password.setText(getString(PASSWORD, ""))
confirmPassword.setText(getString(CONFIRM_PASSWORD, ""))
emailForm.setText(getString(EMAIL_FORM, ""))
}
}
override fun onResume() { //onResume 시 값 불러옴
getSaveData()
super.onResume()
}
override fun onDestroy() { //onDestroy 시 값 저장
saveData()
super.onDestroy()
}
private fun focusOut(editText: EditText) {//EditText가 비어 있을때 포커스 아웃처리
editText.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
} else {
editText.error = if (editText.text.length == 0 || editText.text.isNullOrEmpty()
) {
when (editText.id) {
R.id.et_Id -> "아이디를 입력하세요"
R.id.et_Email -> "이메일을 입력하세요"
R.id.et_Password -> "비밀번호를 입력하세요"
R.id.et_ConfirmPassword -> "비밀번호를 확인해주세요"
else -> ""
}
} else null
}
}
}
private fun validation(editText: EditText) { //각 필드 유효성 체크
editText.addTextChangedListener(object : TextWatcher {
val tv_passwordDescription = findViewById<TextView>(R.id.tv_DescriptionPassword)
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
//텍스트 변화 있을 때 마다 실행
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (editText.text.length == 0 || editText.text.isNullOrEmpty() || isEmpty(editText)
) {
when (editText.id) {
R.id.et_Id -> editText.error = "아이디를 입력하세요"
R.id.et_Email -> editText.error = "이메일을 입력하세요"
R.id.et_Password -> {
editText.error = "비밀번호를 입력하세요"
tv_passwordDescription.isVisible = true
}
R.id.et_ConfirmPassword -> editText.error = "비밀번호를 확인해주세요."
else -> ""
}
} else {
if (editText.id == R.id.et_Password) {
tv_passwordDescription.isVisible = false
if (check(editText) && editText.text.length >= 10) { //비밀번호가 유효하고 10자 이상인지
} else {
editText.error = "10자리 이상, 특수문자, 숫자포함"
}
}
if (editText.id == R.id.et_ConfirmPassword) {
if (isEqualPassword()) { //password와confirmPassword가 같은지 체크
} else {
editText.error = "비밀번호가 같지 않음"
}
}
}
}
override fun afterTextChanged(s: Editable?) {
}
})
}
private fun isEmpty(editText: EditText): Boolean { //공백 문자 처리
val empty = editText.text.toString()
empty.replace(" ", "")
return empty.contains(" ")
}
private fun createSpinner(spinner: Spinner, emailForm: EditText) { //스피너 생성 및 리스너
spinner.apply {
adapter = ArrayAdapter.createFromResource(//스피너 생성
baseContext,
R.array.email,
android.R.layout.simple_list_item_1
)
onItemSelectedListener = object : OnItemSelectedListener { //리스너
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
if (spinner.selectedItemPosition == 3) { //드롭메뉴 직접입력 선택시 직접입력하게 비움
emailForm.apply {
setText("")
setHint("직접입력")
}
} else emailForm.setText(spinner.selectedItem.toString()) // 드롭메뉴 아이템선택시 EditText 입력
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
private fun check(editText: EditText): Boolean { //특수문자 숫자 정규식
val pwd = """^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[!@#$%^+\-=])(?=\S+$).*$"""
val pattern = Pattern.compile(pwd)
return pattern.matcher(editText.text).matches()
}
private fun isEqualPassword(): Boolean { //password 와 confirmPassword가 같은지 체크
val password = findViewById<EditText>(R.id.et_Password)
val confirmPassword = findViewById<EditText>(R.id.et_ConfirmPassword)
return password.text.toString() == confirmPassword.text.toString()
}
}
다른 앱으로 전환 시, 비정상 종료 되는 문제 사용자가 앱을 사용하지 않는데, 시스템 리소스가 소비되는 문제 사용자가 앱을 나갔다가 돌아왔을 때, 진행상태가 저장되지 않는 문제 화면이 가로 ↔ 세로 전환 될 때, 비정상 종료되거나, 진행상태가 저장되지 않는 문제 등이 있을 때, 해결하기 위해 인지 하고 있어야 함
콜백
onCreate()
필수적으로 구현해야함
Activity 의 생명주기 중 한 번만 발생해야하는 로직을 실행
멤버 변수 정의
UI 구성 (setContentView, xml 레이아웃 파일 정의)
saveInstanceState 매개 변수 수신 → Activity 이전 저장 상태가 포함된 Bundle 객체
onStart()
Activity 가 사용자에게 표시
앱은 Activity 를 포그라운드로 보내 상호작용할 수 있도록 준비
onResume()
Activity 가 포그라운드에 표시되어, 사용자와 상호 작용 할 수 있는 상태
앱에서 포커스가 떠날 때 까지 onResume 상태에 머무름
onPause()
사용자가 활동을 떠나는 첫 번째 신호
매우 짧음
활동이 포그라운드에 있지 않지만, 잠시 후 다시 시작할 작업을 일시 중지 하거나 조정
ex) 반투명 Activity 가 띄워져 포커스는 없지만 화면에 보이는 상태
이 상태를 통해서, 실행중이지 않을 때 필요하지 않은 리소스를 해지할 수 있음
이 상태에서, 데이터를 저장하거나, 네트워크 호출, DB 의 IO 작업을 하면 안됨
매우 짧은 시간이라 메서드가 끝나기 전에 Activity 가 종료될 수 있음
onStop()
Activity 가 사용자에게 더 이상 표시 되지 않는 상태
CPU 를 비교적 많이 소모하는 종료 작업을 실행해야함
DB 저장
Activity 가 중단되면, Android OS 에서 리소스 관리를 위해, 해당 Activity 가 포함된 프로세스를 소멸시킬 수 있음
onDestroy()
Activity 가 완전히 종료되기 전에 실행
호출되는 케이스
finish 호출 되어 Activity 가 종료될 때
configurationChange (ex 기기 회전, 멀티 윈도우) 로 인해, 시스템이 Activity 를 일시적으로 소멸 시킬 때
이것을 사용해 다른 앱 구성요소(액티비티, 서비스, 브로드 캐스트리시버)로 작업을 요청 할수 있다.
Intent의 유형
1). 명시적 인텐트(Explicit Intent)
명시적 인텐트는 특정한 컴포넌트를 직접적으로 호출할 때 사용되는 인텐트다. 이 방법으로, 개발자는 인텐트 객체에 시작하고자 하는 구성 요소의 이름을 명확하게 설정하고 startActivity() 또는 startService() 메소드를 통해 해당 컴포넌트를 실행시킨다.
활용 예시:
이 방식은 주로 앱 내부에서 다른 액티비티나 서비스를 시작할 때 사용된다. 예를 들어, 메인 액티비티에서 사용자의 입력을 받아 세부 정보를 표시하는 새로운 액티비티를 띄울 때 명시적 인텐트가 활용된다.
2). 암시적 인텐트(Implicit Intent)
암시적 인텐트는 특정한 컴포넌트를 명시하지 않고, 수행하고자 하는 일반적인 작업을 인텐트 객체에 설정하여 startActivity() 메소드에 넘긴다. 이 때, 안드로이드 시스템은 이 인텐트를 처리할 수 있는 모든 애플리케이션을 검색하여 적합한 인텐트 필터를 가진 컴포넌트를 찾아 실행시킨다.
인텐트 필터:
인텐트 필터는 특정 인텐트에 반응하는 액티비티, 서비스 또는 브로드캐스트 리시버의 능력을 정의한다. 즉, 어떤 인텐트를 수신할 준비가 되어 있는지를 나타내는 설정이다.
일반적 사용법:
암시적 인텐트는 주로 다른 앱의 컴포넌트를 실행시키는 데 사용된다. 예를 들어, 사용자가 '지도 보기'를 요청할 때, 해당 작업을 처리할 수 있는 모든 앱 중 사용자가 선택할 수 있도록 안드로이드 시스템이 목록을 제공한다.
3). 인텐트(Intent) 객체 분석하기
인텐트는 안드로이드 앱의 구성 요소들 사이에서 실행을 위임하거나 정보를 전달하는 역할을 한다. 다음은 인텐트의 주요 구성 요소와 그에 대한 실제 사용 예 이다.
1). 컴포넌트 이름 (Component Name)
타겟 컴포넌트의 이름을 명시하여, 인텐트가 전달될 정확한 대상을 지정한다.
예: **new Intent(context, TargetActivity.class)**에서 **TargetActivity.class**가 컴포넌트 이름이다.
컴포넌트 이름이 없으면, 인텐트는 암시적으로 처리되고, 시스템이 적절한 대상을 찾는다.
2). 액션 (Action)
인텐트가 수행해야 할 일반적인 작업을 정의한다.
예: **Intent.ACTION_VIEW**는 사용자에게 데이터를 보여줄 때 사용된다.
예: **Intent.ACTION_DIAL**은 전화 다이얼을 열기 위해 사용된다.
3). 데이터 (Data)
작업을 수행하는 데 필요한 데이터의 URI를 지정합니다.
예: **Uri.parse("tel:12345")**는 전화 앱에 전화번호 데이터를 제공한다.
데이터는 보통 Uri 객체로 액션과 결합하여 사용된다.
4). 카테고리 (Category)
인텐트의 유형을 더 구체적으로 지정하여, 어떤 컴포넌트가 처리할 수 있는지 알려준다.
예: **Intent.CATEGORY_HOME**은 홈 화면 애플리케이션을 시작할 때 사용된다.
5). 엑스트라 (Extras)
복잡한 데이터를 인텐트에 전달하기 위한 키-값 쌍의 추가 정보다.
예: **intent.putExtra("extra_key", "value")**에서 **"extra_key"**는 전달할 데이터의 키이며, **"value"**는 실제 값 이다.
엑스트라를 통해 기본 타입부터 Serializable 객체까지 다양한 데이터를 전달할 수 있다.
4). 명시적인텐트로 다른 액티비티 시작하기
명시적 인텐트(Explicit Intent)는 특정한 액티비티를 시작할 때 사용되며, 안드로이드에서는 이를 통해 현재 액티비티에서 다른 액티비티로 전환할 수 있다.
// 현재 액티비티에서 AnotherActivity를 시작하기 위한 명시적 인텐트 생성
val intent = Intent(this, AnotherActivity::class.java)
// 인텐트에 데이터 추가 (옵션)
intent.putExtra("key", "value")
// AnotherActivity 시작
startActivity(intent)
인텐트 생성: Intent 클래스의 인스턴스를 생성한다. 첫 번째 매개변수로는 현재 컨텍스트(this)를, 두 번째 매개변수로는 시작하고자 하는 액티비티의 Class 객체를 전달한다. 위 예제에서 **AnotherActivity::class.java**는 시작하고자 하는 대상 액티비티를 나타낸다.
데이터 전달 (선택 사항): putExtra 메소드를 사용하여 인텐트에 추가 데이터를 삽입할 수 있다. 이는 키-값 쌍으로 이루어져 있으며, 시작될 액티비티에서 이 데이터를 사용할 수 있다.
액티비티 시작: startActivity 메소드를 호출하여 인텐트를 사용한다. 이 호출이 실행되면 안드로이드 시스템은 명시된 인텐트에 따라 **AnotherActivity**를 시작한다.
명시적 인텐트는 주로 앱 내부의 액티비티 전환에 사용되며, 암시적 인텐트와 달리 시스템이 액티비티를 추론할 필요 없이 개발자가 직접 지정한다.
object 키워드를 써서 특별한 단일 인스턴스를 만들 수 있다. 싱글톤(singleton) 패턴 인스턴스를 의미한다.
싱글톤을 쉽게 만들 수 있는 키워드
여러 스레드에서 동시에 사용하려고 하더라도 딱 하나의 인스턴스만 생성된다.
생성자 사용불가
프로퍼티, 메서드, 초기화 블록은 사용 가능
클래스를 정의함과 동시에 객체를 생성한다.
Object 식
object식을 활용해서 속성들을 담고 있는 오브젝트를 쉽게 만들 수 있으며, 이렇게 간단히 할 때는 클래스를 선언할 필요 없이 오브젝트를 하나 만들어서 그 안에 멤버들을 선언해 두고 접근해 쓸 수 있다.
fun main() {
rentPrice(10, 2, 1)
}
fun rentPrice(standardDays: Int, festivityDays: Int, specialDays: Int){
val dayRates = object {
var standard: Int = 30 * standardDays
var festivity: Int = 50 * festivityDays
var special: Int = 100 * specialDays
}
val total = dayRates.standard + dayRates.festivity + dayRates.special
print("Total price: $$total") //Total price: $500 //함수호출 해당오브젝트 생성
}
Object 단일 인스턴스
object선언을 할 수도 있다. 식의 형태가 아니라면, 변수에 값을 대입하는 용도로 쓸 수 없다. 별도의 인스턴스를 만들지 않고, 해당 오브젝트의 멤버에 곧바로 접근할 수 있다.
fun main() {
println(Counter.count) /*최초 초기화 생성 시점에 최초 초기화
0*/
Counter.countUp()
Counter.countUp()
println(Counter.count) //2
}
object Counter{
init {
println("최초 초기화")
}
var count = 0
fun countUp(){
count++
}
}
싱글톤으로 생성하면 객체를 한 번만 생성해서 계속 사용하기 때문에 초기화는 한번만 진행 됨 (값 공유)
(만약 class로 만들었다면 각각 다른 객체가 생성됨)
다른 클래스나 인터페이스도 상속가능하다.
Object는 안드로이드에서 에러코드를 정의할 때 많이 사용
Companion Objects (동반 객체)
클래스 정의 안쪽에 선언하는동반 오브젝트(객체)를 만들 수 있다. 문법적으로 자바의 정적(static) 메서드와 비슷하게 클래스 이름을 통해서 해당 오브젝트 멤버에 접근할 수 있다.
Java의 static 과 동일한 역할
클래스 내에 하나만 생성할 수 있음
fun main() {
Book.NAME //인스턴스를 만들지 않아도 변수나 상수에 접근가능
Book.create() //인스턴스를 아직 만들지 않아도 클래스 메소드처럼 사용가능
}
class Book{
companion object{
const val NAME = "name"
fun create() = Book()
}
}
자바의 static 과 동일하기 때문에 인스턴스를 만들지 않아도 메소드나 프로퍼티에 바로 접근이 가능하다.
목적 -> 메모리를 효율적으로 사용하기 위해서, null safe 한 value를 사용하기 위해서
lateinit , var
변수 타입을 지정해줘야함
원시타입(primitive type)은 사용할 수 없음 (Int, Long, Double, Boolean, Char, Short, Byte, Float)
선언 후, 나중에 초기화 해줘도 됨
class Test{
lateinit var text : String //primitive 타입 사용불가
fun lateInit (){
if(::text.isInitialized){ // :: 으로 참조
println("초기화 됨")
}else{
println("초기화 전")
}
}
}
fun main() {
val test = Test()
test.lateInit() //초기화 전
test.text = "초기화 시점"
test.lateInit() // 초기화 됨
println("text = ${test.text}") //text = 초기화 시점
}
lazy , val
val(상수) 이기 때문에 값을 다시 변경할 수 없다.
호출 시점에 by lazy {...} 에 정의해둔 블록 부분의 초기화를 진행한다.
클래스 생성자에서 사용불가
원시 타입(primitive type) 사용 가능
지역 변수에서도 사용 가능
class ByLazy{
init {
println("init Block") // 최초 초기화 선언알림
}
val lazy : String by lazy { "Lazy Test" }
fun flow(){
println("초기화 진행 전") //초기화 되기 전
println("$lazy : 최초 초기화") //최초 초기화 시점
println("$lazy : 최초 초기화 후") //초기화된 값 사용(불변)
}
}
fun main(){
val lazyInstance = ByLazy() //init Block
lazyInstance.flow() /*
init Block
초기화 진행 전
Lazy Test : 최초 초기화
Lazy Test : 최초 초기화 후
*/
}