본문 바로가기

Android

Layer / DatePickerDialog / Spinner / SharedPreference

오늘은 저번에 xml - style 구성 / Intent 관리 포스팅 다음 내용으로 정리해보았습니다.

MainActivity에서 FloatingButton을 클릭하여 EditActivity로 이동한 다음 여기서 구현할 내용에 대해서 정리하였습니다.

 

전체 코드를 먼저 입력한 다음 하나씩 떼가면서 분석하겠습니다.

 

[edit_activity.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".EditActivity">

    <TextView
        android:id="@+id/nameTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="36dp"
        android:text="이름"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/nameEditText"
        style="@style/Value"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="36dp"
        android:inputType="text"
        app:layout_constraintBaseline_toBaselineOf="@id/nameTextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideLine" />

    <TextView
        android:id="@+id/birthdateTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="생년월일"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/nameTextView" />

    <TextView
        android:id="@+id/birthdateValueTextView"
        style="@style/Value"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:paddingEnd="8dp"
        android:text="0000-00-00"
        app:layout_constraintBaseline_toBaselineOf="@id/birthdateTextView"
        app:layout_constraintEnd_toStartOf="@+id/birthdateImageView"
        app:layout_constraintStart_toStartOf="@id/guideLine" />

    <ImageView
        android:id="@+id/birthdateImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/baseline_10k_24"
        app:layout_constraintBottom_toBottomOf="@id/birthdateTextView"
        app:layout_constraintEnd_toEndOf="@id/nameEditText"
        app:layout_constraintTop_toTopOf="@id/birthdateTextView" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideLine"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.4" />

    <TextView
        android:id="@+id/bloodTypeTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="혈액형"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/birthdateTextView" />

    <TextView
        android:id="@+id/emergencyContactTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="비상 연락처"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/bloodTypeTextView" />

    <TextView
        android:id="@+id/warningTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="주의사항"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/emergencyContactTextView" />


    <RadioGroup
        android:id="@+id/bloodTypeRadioGroup"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="@id/bloodTypeTextView"
        app:layout_constraintStart_toStartOf="@id/guideLine"
        app:layout_constraintTop_toTopOf="@id/bloodTypeTextView">

        <RadioButton
            android:id="@+id/bloodTypePlus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rh+" />

        <RadioButton
            android:id="@+id/bloodTypeMinus"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Rh-" />

    </RadioGroup>

    <Spinner
        android:id="@+id/bloodTypeSpinner"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="@id/bloodTypeTextView"
        app:layout_constraintEnd_toEndOf="@id/nameEditText"
        app:layout_constraintStart_toEndOf="@+id/bloodTypeRadioGroup"
        app:layout_constraintTop_toTopOf="@id/bloodTypeTextView" />

    <CheckBox
        android:id="@+id/warningCheckBox"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:gravity="end|center_vertical"
        android:text="주의사항 노출"
        app:layout_constraintBottom_toBottomOf="@+id/warningTextView"
        app:layout_constraintEnd_toEndOf="@+id/nameEditText"
        app:layout_constraintStart_toStartOf="@+id/guideLine"
        app:layout_constraintTop_toTopOf="@+id/warningTextView" />

    <EditText
        android:id="@+id/emergencyContactEditText"
        style="@style/Value"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="0000-0000-0000"
        android:inputType="phone"
        app:layout_constraintBaseline_toBaselineOf="@id/emergencyContactTextView"
        app:layout_constraintEnd_toEndOf="@id/nameEditText"
        app:layout_constraintStart_toStartOf="@id/guideLine" />

    <EditText
        android:id="@+id/warningEditText"
        style="@style/Value"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="주의사항"
        app:layout_constraintEnd_toEndOf="@id/nameEditText"
        app:layout_constraintStart_toStartOf="@id/guideLine"
        app:layout_constraintTop_toBottomOf="@id/warningCheckBox" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/saveButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="36dp"
        android:clickable="true"
        android:src="@drawable/outline_adb_24"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/birthDateLayer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="birthdateImageView, birthdateValueTextView"
        tools:ignore="MissingConstraints" />


</androidx.constraintlayout.widget.ConstraintLayout>

 

1) Layer

겹쳐져 있는 두 위젯에 대해서 눌렀을때 동일한 작업을 수행하도록 구현한 것으로, 예를 들어 생일일자가 나와 있는 문구를

클릭했을 때 캘린더가 뜨고 마찬가지로 문구 옆 캘린더 이미지를 눌렀을 때에도 캘린더가 뜨게하는 효과입니다.

    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/birthDateLayer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="birthdateImageView, birthdateValueTextView"
        tools:ignore="MissingConstraints" />

 

이렇게 constraint_referenced_ids에 id값을 추가하면 됩니다.  사용 방법은 팔레트에 나와 있는 위젯에 우클릭하면

Add helpers라는 보기가 있습니다.

여기서 Layer를 선택하여 추가해주시면 됩니다. 


2. DatePickerDialog

이것은 날짜 선택하는 팝업으로 캘린더를 눌렀을때 팝업으로 캘린더가 뜨면서 선택하도록 하는 방법입니다.

// DatePicker 추가
binding.birthDateLayer.setOnClickListener {
   var listener = OnDateSetListener { date, year, month, dateOfMonth ->
       binding.birthdateValueTextView.text = "$year.${month.inc()}.$dateOfMonth"
   } 
   DatePickerDialog(this, listener, 2000, 1, 1).show()
}

 

방금 설정한 Layer를 binding을 통해 찾아오고 클릭했을때 DatePickerDialog를 띄워서 날짜 선택하도록 만들것입니다.

이 때, DatePickerDialog는 인자로 현재 컨텍스트와 리스너, 그리고 선택 안할시 기본적으로 셋팅되는 년, 월, 일을 정의해줍니다.

listener는 따로 구현하여 넣어주는데, onDateSetListener를 호출하여 정의합니다.

람다 함수 형식으로 리스너의 내용을 정의하는데, 날짜, 년, 월, 일을 키로 두고 생년월일로 들어가는 객체에

텍스토로 선택한 년, 월, 일을 추가해줍니다.

이 때, 월은 선택한 달보다 한 달 적게 나오기 때문에 ${month.inc()} 를 통해 하나 증가하여 정의합시다.


 

3. Spinner

드롭다운 형식으로 뭘 선택할 지 정의하는 건데 xml에다가만 셋팅하면 안되고 코틀린 파일에서 내부에 들어갈 아이템을

셋팅을 해줘야합니다.

또한 선택한 값에 대해서 결과를 정의해줘야 합니다.

 

binding.bloodTypeSpinner.adapter = ArrayAdapter.createFromResource(
      this,
      R.array.blood_type,
      android.R.layout.simple_list_item_1
)
private fun getBloodType(): String {
    val bloodAlphabet = binding.bloodTypeSpinner.selectedItem.toString()
    val bloodSign = if (binding.bloodTypePlus.isChecked) "+" else "-"
    val result = bloodAlphabet.plus(bloodSign)
    return result
}

private fun getWarning(): String {
    return if (binding.warningCheckBox.isChecked) {
        binding.warningEditText.text.toString()
    } else {
        ""
    }
}

 

binding을 통해 스피너 객체에 접근하고 여기다 adapter를 꽂아줍니다.

ArrayAdapter를 사용할 것이며 리소스로부터 어댑터를 생성하도록 하여 [1) 컨텍스트 객체 2) 스피너에 들어갈 요소 (배열)

3) 어떤 방식으로 보여줄 지 형태 정의] 다음 3가지 인자에 맞도록 정의합니다.

 

이전 포스팅에서 styles.xml을 정의한 것처럼 arrays.xml을 또한 정의할 수 있습니다.

 

[arrays.xml]

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="blood_type">
        <item>A형</item>
        <item>B형</item>
        <item>AB형</item>
        <item>O형</item>
    </string-array>
</resources>

 

다음과 같이 셋팅하고 접근하면 스피너는 아이템에 들어간 값들을 리스트로 보여주게 될 것입니다.

 

getBloodType() 함수에서처럼 스피너가 선택한 아이템을 객체에 넣고 라디오 버튼에서 bloodTypePlus가 

선택되었을경우 (isChecked) '+' 아니면 '-'로 값 할당하도록 정의합니다.

이 두 내용을 합치고자 .plus() 연산자를 두어 마치 concat과 같은 효과로 문자열을 합칩니다.

라디오 버튼에서 isChecked를 사용해서 클릭 여부를 확인했는데, 이는 체크박스에서도 동일하게 적용됩니다.

getWarning()이라는 함수를 보더라도 아실 겁니다.

 


4. SharedPreference

이건 내부적으로 데이터를 관리해주는 딕셔너리로 K-V 형태로 값을 저장합니다. 

간단한 설정 값이나 데이터를 저장하거나 문자열 관리 등 DB를 활용하기에는 뭔가 과하다 생각할 때 많이 쓰입니다.

private fun saveData() {
     // sharedpreference 사용 > 보통 세션을 관리할 때 쓰이며 키-값 데이터로 저장
     // Key - Value & Mode 설정
     // Context. MODE_PRIVATE 와 같은 종류가 많음
     with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE).edit()) {
         putString(NAME, binding.nameEditText.text.toString())
             .putString(BLOOD_TYPE, getBloodType())
             .putString(EMERGENCY_CONTACT, binding.emergencyContactEditText.text.toString())
             .putString(BIRTHDATE, binding.birthdateTextView.text.toString())
             .putString(WARNING, getWarning())
          apply()
     }

     Toast.makeText(this, "저장을 완료했습니다.", Toast.LENGTH_SHORT).show()
}

 

[Const.kt]

package com.example.chapter4

const val USER_INFORMATION = "userInformation"
const val NAME = "name"
const val BIRTHDATE = "birthdate"
const val BLOOD_TYPE = "bloodType"
const val EMERGENCY_CONTACT = "emergencyContact"
const val WARNING = "warning"

 

SharedPreference를 사용하기 위해서는 가져와야 하므로 getSharedPreferences를 호출하여 가져옵니다.

SharePreference의 이름을 정의해주고자 userInformation이라는 키로 셋팅합니다. (이는 상수값으로 Const.kt에 정의함)

그리고 MODE_PRIVATE로 두어 모드를 설정합니다. 

 

[SharedPreference - Mode]

더보기

MODE의 종류

  • MODE_PRIVATE : 생성한 Application에서만 사용 가능하다.
  • MODE_WORLD_READABLE : 외부 App에서 사용 가능, But 읽기만 가능
  • MODE_WORLD_WRITEABLE : 외부 App에서 사용 가능, 읽기/쓰기 가능

그리고 값 셋팅할 때 리턴 값이 없기 때문에 with 확장함수를 이용해서 정의합니다.

SharedPreference 내 속성값을 정의하기 위해서 edit()을 호출하여 수정한다고 정의합니다.

이후엔 putString(키, 값) 형태로 내용을 정의합니다. 

마치 Intent에서 putString으로 Key-Value 전달한거랑 같은 느낌입니다.

 

다 셋팅하고 edit()으로 수정했으니 이제 반영해야합니다.  반영할 때에는 .commit()과 .apply() 두 가지 방식이 있는데

둘 다 써도 상관은 없지만 .apply()를 권장합니다.

이유는 아래와 같습니다.

 

commit()을 사용하게 되면 현재 SharedPreference를 사용하는 스레드에서 내용 변경을 반영하는데 동기적으로 

진행되기 때문에 반영하는 중에는 다른 작업을 하지 못합니다.

반영할 것이 많고 그러다보면 자연스레 딜레이가 발생할 수 밖에 없는 구조입니다.

하지만 apply()를 통해 수정사항을 반영하게 되면 비동기처리로 진행되기 때문에 좀 더 빠르게 내용이 반영됩니다.

다음 내용을 표로 다시 한 번 정리하겠습니다.

 

commit() apply()
1. 현재 스레드를 Block() 시킨다
2. 내용 반영후 저장한 다음 스레드를 다시 작동시킨다.
3. 처리 결과에 대한 결과값 T/F를 반환합니다.
1. 호출한 다음 바로 작동하므로 스레드를 Block() 시키지 않는다.
2. 커널 모드에서 파일에 데이터를 저장하도록 한다.
3. 처리 결과에 대한 결과값 반환이 없다.
동기적인 수행방법 비동기적인 수행방법

 

 

이제 받는 쪽 액티비티에서는 어떻게 접근하는지 한 번 확인해보겠습니다.

private fun getDataUiUpdate() {
     with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE)) {
         binding.birthDate.text = getString(BIRTHDATE, "미정")
         binding.name.text = getString(NAME, "미정")
         binding.bloodType.text = getString(BLOOD_TYPE, "미정")
         binding.phoneBook.text = getString(EMERGENCY_CONTACT, "미정")
         binding.etc.isVisible = getString(WARNING, "").isNullOrEmpty().not()
         binding.txtEtc.isVisible = getString(WARNING, "").isNullOrEmpty().not()
         if (!getString(WARNING, "").isNullOrEmpty()) {
            binding.etc.text = getString(WARNING, "미정")
        }
    }
}

 

보낼때와 마찬가지로 getSharedPreferences를 통해 가져오는데 키 값이 동일하게하여 SharedPreference의 객체를

먼저 찾고나서 getString(키, "없으면 나오는 기본값")으로 저장된 내용을 가져옵니다. 

앞서 putString으로 보낼려고 정의한것과는 다르게 이는 값을 그냥 가져오는 것에 그치기 때문에 edit()을 호출하지 않았습니다.

따라서 apply()나 commit()을 호출할 필요가 없게 됩니다.

가져온 내용은 binding으로 가져온 텍스트뷰에 텍스트로 넣어주고 값의 유무에 따라서 isVisible도 정의했습니다.

 

아래 내용을 초기화하는 코드이며, 초기화 또한 SharedPreference 내 값을 건드는 것이기 때문에 edit()을 호출하고

clear() 함수를 호출합니다.   또한 정리 후에는 반드시 apply()나 commit()을 두어 정의합니다. 

private fun initData() {
    with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE).edit()) {
        clear()
        apply()
        getDataUiUpdate()
    }
    Toast.makeText(this, "초기화를 완료했습니다.", Toast.LENGTH_SHORT).show()
}

 


 

마지막으로 MainActivity.kt, EditActivity.kt 전체 코드입니다.

MainActivity.kt에서 암시적 인텐트가 나오긴 하지만 이 부분은 간단히 설명하자면 명시적으로 어딜 이동한다거나

값을 보낸다거나 하는 것이 아니라 내부적으로 정의된 내용을 호출하는 것입니다. 

 

[MainActivity.kt]

package com.example.chapter4

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import com.example.chapter4.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "MainActivity"
    }

    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        binding.floatingActionButton.setOnClickListener {
            intent = Intent(this, EditActivity::class.java).apply { putExtra("MOVE", "EditData") }
            startActivity(intent)
        }

        binding.deleteButton.setOnClickListener {
            initData()
        }

        binding.btnCall.setOnClickListener {
            with(Intent(Intent.ACTION_VIEW)) {
                val phoneNumber = binding.phoneBook.text.toString().replace("-","")
                data = Uri.parse("tel:$phoneNumber")
                startActivity(this)
            }
        }

        getDataUiUpdate()
    }

    override fun onResume() {
        getDataUiUpdate()
        super.onResume()
    }

    override fun onRestart() {
        getDataUiUpdate()
        super.onRestart()
    }

    private fun getDataUiUpdate() {
        with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE)) {
            binding.birthDate.text = getString(BIRTHDATE, "미정")
            binding.name.text = getString(NAME, "미정")
            binding.bloodType.text = getString(BLOOD_TYPE, "미정")
            binding.phoneBook.text = getString(EMERGENCY_CONTACT, "미정")
            binding.etc.isVisible = getString(WARNING, "").isNullOrEmpty().not()
            binding.txtEtc.isVisible = getString(WARNING, "").isNullOrEmpty().not()
            if (!getString(WARNING, "").isNullOrEmpty()) {
                binding.etc.text = getString(WARNING, "미정")
            }
        }
    }//deleteButton

    private fun initData() {
        with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE).edit()) {
            clear()
            apply()
            getDataUiUpdate()
        }
        Toast.makeText(this, "초기화를 완료했습니다.", Toast.LENGTH_SHORT).show()
    }
}

 

 

[EditActivity.kt]

package com.example.chapter4

import android.app.DatePickerDialog
import android.app.DatePickerDialog.OnDateSetListener
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.example.chapter4.databinding.EditActivityBinding

class EditActivity : AppCompatActivity() {
    companion object {
        const val TAG = "InputActivity"
    }

    private lateinit var binding: EditActivityBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = EditActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)
        Log.d(TAG, intent.getStringExtra("IntentMessage").toString())

        binding.bloodTypeSpinner.adapter = ArrayAdapter.createFromResource(
            this,
            R.array.blood_type,
            android.R.layout.simple_list_item_1
        )
        // DatePicker 추가
        binding.birthDateLayer.setOnClickListener {
            var listener = OnDateSetListener { date, year, month, dateOfMonth ->
                binding.birthdateValueTextView.text = "$year.${month.inc()}.$dateOfMonth"
            }
            DatePickerDialog(this, listener, 2000, 1, 1).show()
        }

        binding.warningCheckBox.setOnCheckedChangeListener { _, isChecked ->
            binding.warningEditText.isVisible = isChecked
        }

        binding.warningEditText.isVisible = binding.warningCheckBox.isChecked

        binding.saveButton.setOnClickListener {
            saveData()
            finish()
        }
    }

    private fun saveData() {
        // sharedpreference 사용 > 보통 세션을 관리할 때 쓰이며 키-값 데이터로 저장
        // Key - Value & Mode 설정
        // Context. MODE_PRIVATE 와 같은 종류가 많음
        with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE).edit()) {
            putString(NAME, binding.nameEditText.text.toString())
                .putString(BLOOD_TYPE, getBloodType())
                .putString(EMERGENCY_CONTACT, binding.emergencyContactEditText.text.toString())
                .putString(BIRTHDATE, binding.birthdateTextView.text.toString())
                .putString(WARNING, getWarning())
            apply()
        }

        Toast.makeText(this, "저장을 완료했습니다.", Toast.LENGTH_SHORT).show()
    }

    private fun getBloodType(): String {
        val bloodAlphabet = binding.bloodTypeSpinner.selectedItem.toString()
        val bloodSign = if (binding.bloodTypePlus.isChecked) "+" else "-"
        val result = bloodAlphabet.plus(bloodSign)
        return result
    }

    private fun getWarning(): String {
        return if (binding.warningCheckBox.isChecked) {
            binding.warningEditText.text.toString()
        } else {
            ""
        }
    }
}

 

 

이상 응급의료 앱 단위 프로젝트를 통해 안드로이드 개념에 대해 좀 더 정리해보는 시간이었습니다.

감사합니다. 

반응형