아래 내용은 패스트 캠퍼스 [35개 프로젝트로 배우는 Android 앱 개발 feat. Jetpack Compose]의 강의 내용을 기반으로 작성한 자습 기록입니다.



📚응급 의료 정보 앱

ℹ️앱 설명

응급 상황 시 사용자의 관련 정보를 보여주는 앱으로 관련 정보와 정보 입력 화면, 정보를 초기화할 수 있는 버튼으로 구성되어 있는 앱.


✅구현 기능

  • 사용자의 응급 관련 정보를 표시하는 화면
  • 사용자의 응급 관련 정보를 입력하는 화면
  • 응급 관련 정보를 초기화 시키는 버튼


✅사용되는 기능

  • UI
    • ConstraintLayout
    • Style - theme
    • ImageView
    • RadioButton
    • CheckBox
    • Spinner
    • DatePicker
    • Toolbar
  • Kotlin
    • const val
  • Android
    • Manifest
    • SharedPreference
    • Intent
      • 화면 전환
      • 전화 앱 실행
    • Toast


📔UI 구현 - 사용자 응급 정보 화면

사용자의 응급 정보로는 ‘이름’, ‘생년월일’, ‘혈액형’, ‘비상 연락처’, ‘기타 주의사항’ 등 총 5가지의 정보가 기입된다. 해당 정보들은 다음과 같이 표시될 예정이다.

ui1

정보들은 모두 같은 속성들로 정의될 것이므로, 스타일을 사용해서 UI를 구현하였다.

스타일을 지정하기 위해 New -> Values Resource File 에서 styles라는 이름의 파일을 하나 만들어 주었다.

styles 파일은 <style name="스타일명" parent="기반이 되는 뷰"></style> 을 먼저 선언하고, style 태그 내부에 <item name="속성명">속성값</item> 을 사용하여 해당되는 스타일에 공통으로 적용될 속성을 입력해주면 된다.

위와 같은 방법으로 아래와 같이 Title이라는 이름으로 스타일을 지정해 주었다.

📖styles.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Title" parent="Widget.AppCompat.TextView">
        <item name="android:textColor">@color/brown</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">24sp</item>
        <item name="android:layout_marginTop">36dp</item>
    </style>
</resources>

만약 Title 스타일에 다른 속성값을 추가하여 스타일로 사용하고 싶다면 parent 키워드를 사용하여 아래와 같이 Title 스타일을 상속받아 사용할 수 있다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Title" parent="Widget.AppCompat.TextView">
        <item name="android:textColor">@color/brown</item>
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">24sp</item>
        <item name="android:layout_marginTop">36dp</item>
    </style>

    <style name="Value" parent="Title">
      <item name="android:textColor">@color/white</item>
      <item name="android:layout_marginEnd">20dp</item>
</resources>

이제 해당 스타일을 activity_main.xml 파일에 가서 스타일을 지정해 주면 된다.

📖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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <TextView
        android:id="@+id/birthDateTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/birthDate"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/nameTextView" />

    <TextView
        android:id="@+id/bloodTypeTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/bloodType"
        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="@string/emergencyContact"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/bloodTypeTextView" />

    <TextView
        android:id="@+id/emergencyInformationTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/emergencyInformation"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/emergencyContactTextView" />

</androidx.constraintlayout.widget.ConstraintLayout>

스타일을 지정하여 아래와 같이 일관된 형식의 사용자 정보가 나온 것을 볼 수 있다.

ui2

사용자 정보를 표시했으니 사용자 정보에 대한 값을 넣어보도록 하자. 해당 값들은 아래와 같이 표시될 예정이다.

ui3

해당 값에 대해 구현하는 중에 다음과 같은 문제가 발생했다.

ui4

이름 값이 너무 커지게 되어 ‘이름’ 영역에 중첩되어 표시된다. 다음과 같은 문제를 해결하기 위해 가이드라인을 사용하였다. 가이드라인은 레이아웃 영역내에 기준선을 생성하여 제약을 지정할 수 있도록 해주는 기능이다. 가이드라인을 지정하는 방법은 다음과 같다.

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

android:orientation 속성을 사용하여 수평 또는 수직의 방향을 설정하고, app:layout_constraintGuide_percent 속성값을 통해서 가이드라인의 위치를 지정해 줄 수 있다.

ui7

위의 설정을 적용해 가이드라인 범위까지 제약을 지정하고, 최대 표시 줄 수를 지정하는 android:maxLines 속성을 “1”로, 입력 범위 초과시 표시값을 나타내는 android:ellipsize="end"를 지정하면 다음과 같이 ‘이름’ 영역을 초과하지 않고 깔끔하게 값을 표시할 수 있다.

ui6

나머지 값들 또한 같은 방식으로 지정해 주었고, 비슷한 속성들끼리 묶어 Value스타일을 지정하여 만들어 주었다.

ui8




📔기능 구현 - 화면 전환

정보를 추가하기 위한 화면으로 넘어가기 위해 FloatingActionButton을 사용하였다. 버튼을 직관적으로 잘 보이게 하기 위해 하단과 끝 부분에 제약을 주었고 간격을 주기 위해 android:layout_margin 속성을 주었다.

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

ui9

다음으로 정보를 추가하기 위한 화면을 생성해야 한다. 클래스 파일과 xml 파일을 따로 생성하고 Manifest 파일에 추가된 activity 정보를 선언해두지 않으면 ActivityNotFoundException이 발생할 수 있다. 따라서 일일히 생성해주기 보다는 간단한 방법으로 Manifest까지 자동으로 설정해 사용하는 것이 낫다. 순서는 패키지에서 New -> Activity -> Empty Activity를 선택해 주고 이외의 설정은 새 프로젝트 생성 시, 액티비티를 생성하는 방법과 동일하게 설정해 준다. 해당 방법으로 클래스 파일과 xml 파일, 그리고 Manifest 설정까지 알아서 적용해준다.

ui9 ui9 ui9

새 액티비티도 추가했으니 이제 화면을 전환해보자. 화면을 전환할 때에는 Intent를 사용하여 화면을 전환한다. Intent에 대한 설명을 나중에 자세히 다루도록 하겠다.

먼저 MainActivity에서 플로팅 버튼에 setOnClickListener를 사용해서 버튼 클릭 시, 동작하도록 해주었고 그 내부에 Intent 객체를 생성해 주었다.

📖MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.goEditActivityButton.setOnClickListener {
            val intent = Intent(this, EditActivity::class.java)
            intent.putExtra("intentData", "intent")
            startActivity(intent)
        }
    }
}

위 코드에 대해 간단하게 설명하자면, 객체 생성 시, 인자값에 현재 클래스의 Context이동할 클래스를 넣어준다. 그 후 화면 전환이 제대로 되었는지 확인하기 위해 putExtra를 사용해 키와 값을 넘겨준 뒤, startActivity()로 화면 전환을 실행시켜 준다.

이제 화면이 제대로 전환 되었는 지와 값이 제대로 전달 되었는지 확인해 보자.

📖EditActivity.kt

class EditActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_edit)

        Log.d("test", intent.getStringExtra("intentData").toString())
    }
}

전환된 액티비티에서는 값만 확인해주면 되기 때문에 로그를 찍어보았다.

ui9 ui9

아주 잘 연결되었다.





📔UI 구현 - 사용자 응급 정보 입력 화면

MainActivity로부터 화면을 전환시켰으니 사용자 응급 정보 입력 화면에 대한 화면을 구현하여 보자.

입력 화면은 앞의 화면과 일부를 제외한 구성이 비슷하기 때문에 코드를 가져왔다. 단, ‘이름’, ‘비상 연락처’, ‘주의사항’ 란은 TextView에서 EditText로 바꾸어 주었다.

📖activity_edit.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/nameTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="36dp"
        android:text="@string/name"
        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/guideline1" />

        ...
        ...
        ...

    <TextView
        android:id="@+id/emergencyContactTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/emergencyContact"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/bloodTypeTextView" />

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

    <TextView
        android:id="@+id/emergencyInformationTextView"
        style="@style/Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/emergencyInformation"
        app:layout_constraintStart_toStartOf="@id/nameTextView"
        app:layout_constraintTop_toBottomOf="@id/emergencyContactTextView" />

    <EditText
        android:id="@+id/emergencyInformationEditText"
        style="@style/Value"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:hint="주의사항"
        android:maxLines="3"
        app:layout_constraintBottom_toBottomOf="@id/emergencyInformationTextView"
        app:layout_constraintEnd_toEndOf="@id/nameEditText"
        app:layout_constraintStart_toStartOf="@id/guideline1" />

    ...
    ...

</androidx.constraintlayout.widget.ConstraintLayout>

다음으로 ‘생년월일’ 부분에는 달력 모양의 이미지를 추가해 주었다.

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

‘혈액형’ 부분은 다음과 같이 ‘RH+’와 ‘RH-‘ 중 선택을 하고 목록 버튼을 누르면 혈액형을 선택하도록 할 것이다.

radiobutton

먼저, ‘RH+’와 ‘RH-‘를 선택하도록 하기 위해서는 RadioButton을 사용해야 하는데, RadioButton을 사용하려면 RadioGroup 내에 선언되어야 한다. RadioGroupLinearLayout을 상속받기 때문에 android:orientation과 같은 속성을 사용하여 RadioButton을 수직 또는 수평으로 정렬할 수 있기 때문이다. 따라서 RadioGroup을 먼저 생성한 뒤, 그 안에 각각의 RadioButton을 만들어 주었다.

<RadioGroup
    android:id="@+id/bloodTypeRadioGroup"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:orientation="horizontal"
    android:gravity="end"
    app:layout_constraintBottom_toBottomOf="@id/bloodTypeTextView"
    app:layout_constraintEnd_toEndOf="@id/nameEditText"
    app:layout_constraintStart_toStartOf="@id/guideline1"
    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>

아래와 같이 RadioButton이 생성된 것을 볼 수 있다.

radiobutton2


다음으로 4가지의 혈액형을 선택하기 위해서 Spinner를 사용하였다. Spinner를 사용하면 원하는대로 4개의 혈액형 중 하나를 선택해 적용할 수 있다.

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

radiobutton2

‘주의사항’ 입력 부분에는 CheckBox를 추가하여 선택이 되었을 때에만 주의사항을 입력할 수 있도록 하였다. 반대로 선택이 되어있지 않다면 주의사항에 대한 정보는 보이지 않도록 설정할 것이다.

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

checkbox




📔기능 구현 - Spinner와 DatePicker, CheckBox 구현

Spinner의 값이 표시되도록 구현을 해보자. EditActivity에서 뷰 바인딩으로 연결된 bloodTypeSpinner를 가져와 각각의 혈액형들이 리스트로 표시되어야 하기 때문에 adapter를 정의했다. ArrayAdapter를 사용해 정의를 할 건데, 그 전에 혈액형 종류가 입력된 리소스 파일을 하나 새로 만들었다.

📖arrays.xml

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

array.xml

이제 ArrayAdapter를 사용하여 혈액형 값들을 구현한다. ArrayAdapter.createFromResource()를 사용하면 총 3개의 매개변수가 존재한다. Context, @ArrayRes, @LayoutRes가 그것들이다. 각각의 매개변수에 대해 설명하자면, Context는 현재 액티비티의 Context, @ArrayRes는 참조할 배열의 id, @LayoutRes는 배열을 표시할 레이아웃을 의미한다. 따라서 각각의 매개변수에 해당하는 인자를 넣어주었다.

📖EditActivity.kt

class EditActivity : AppCompatActivity() {

    private lateinit var binding: ActivityEditBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityEditBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.bloodTypeSpinner.adapter = ArrayAdapter.createFromResource(
            this,
            R.array.blood_types,
            android.R.layout.simple_list_item_1
        )
    }
}

위 코드의 마지막 인자인 android.R.layout.simple_list_item_1은 안드로이드에서 기본적으로 제공하는 레이아웃의 한 종류이다. 파일을 들여다보면 다음과 같이 간단한 텍스트 뷰 하나로 구성되어 있는 것을 볼 수 있다.

📖simple_list_item_1.xml

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceListItemSmall"
    android:gravity="center_vertical"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:minHeight="?android:attr/listPreferredItemHeightSmall" />

simple_layout

해당 코드를 적용해서 결과를 보면 각각의 혈액형들이 Spinner를 누르면 리스트 형태로 펼쳐지는 것을 볼 수 있다.

spinner_result1 spinner_result2


이번에는 생년월일을 설정해 보도록 하자. 생년월일을 구현하기 위한 방법으로는 DatePicker를 사용할 것이다. DatePicker는 사용자가 날짜 정보를 선택할 시, ui를 통해서 선택할 수 있도록 해주는 클래스이다.

사용하기에 앞서 먼저 DatePicker를 적용할 영역을 만들어 주도록 하자. 이를 위해 birthDateImageViewbirthDateValueTextView를 그룹으로 묶어 Layer로 지정해 주었다.

📖activity_edit.xml

<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" />

Layer는 특별이 시각적인 기능을 하진 않기 때문에 MissingConstraints를 설정해 두었다. 말 그대로 제약 조건을 무시하겠다는 의미이므로 UI 상으로는 보여지지 않는다.

EditActivity로 와서 Layer영역에 대한 클릭 리스너를 만들어 주었다. 그 후, DatePickerDialogOnDateSetListener를 통해 날짜 선택 후에 생성될 값의 형식을 ‘생년월일’ TextView의 값으로 지정해 주었다. 참고로 month값은 생성 후에 선택된 값의 -1이 되어 나오므로 반대로 +1를 해주었다.

위의 과정을 listener 변수로 만들어주고, DatePickerDialog의 인자 값을 넣어준다. 각각의 인자가 의미하는 것은, DatePickerDialog("현재 액티비티의 Context", "OnDateSetListener", "년", "월", "일")과 같다. 여기서 잊지 말아야 할 것은 DatePickerDialogToast와 같이 마지막에 show()를 꼭 붙여주어야 한다.

📖EditActivity.kt

binding.birthDateLayer.setOnClickListener {
    val listener = DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
        binding.birthDateTextView.text = "$year-${month.inc()}-$dayOfMonth"
    }
    DatePickerDialog(
        this,
        listener,
        2000,
        1,
        1
    ).show()
}

결과를 보면, 입력한 값이 잘 구현된 것을 볼 수 있다.

date_picker1 date_picker2


이번에는 ‘주의사항 노출’ CheckBox를 선택하였을 때에만 주의사항을 입력할 수 있도록 하는 기능을 구현해보자.

emergencyInformationCheckBox를 불러와 setOnCheckedChangedListener를 호출해준다. 그 후, 매개변수 값 중 isChecked가 Boolean 값으로 반환되기 때문에 emergencyInformationEditText.isVisible = isChecked로 설정해준다. 이렇게 되면 체크가 되었을 때는 주의사항 입력란이 표시가 되고, 그렇지 않을 때에는 표시가 되지 않게 된다.

📖EditActivity.kt

binding.emergencyInformationCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
    binding.emergencyInformationEditText.isVisible = isChecked
}

하지만 그냥 이렇게만 써놓으면 기본값으로 체크가 되어있지 않을 때에도 입력란이 표시되므로 코드 바로 하단에 같은 다음 코드를 입력해 주었다.

binding.emergencyInformationEditText.isVisible = binding.emergencyInformationCheckBox.isChecked

설정에 맞게 체크박스가 구현된 것을 볼 수 있다.

checkbox2 checkbox3




📔기능 구현 - 데이터 저장하고 불러오기


이제 저장 버튼을 누르면 입력한 정보들이 초기 화면에 저장되도록 해보자. 먼저 복사해왔던 코드 중 FloatingActionButton의 id값과 src값을 변경해 주었다.

📖activity_edit.xml

<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:src="@drawable/ic_baseline_save_24"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

버튼을 누르면 입력했던 값들이 저장될 수 있도록 하기 위해 SharedPreference를 사용한다.

SharedPreference에 대해서 나중에도 정리 하겠지만 간단하게 정리하면 소량의 ‘키’와 ‘값’의 쌍을 다루는 데이터들은 SharedPreference에 의해 관리될 수 있다. SharedPreference는 해당 값들을 파일을 만들어서 관리한다.

코드가 길어지기 때문에 버튼 선택 시 saveData()가 호출되도록 설정해 두었다.

📖EditActivity.kt

binding.saveButton.setOnClickListener {
    saveData()
}

saveData()함수 내에서 SharedPreference를 불러오기 위해 getSharedPreferences()를 호출했다. 여기서 인자가 두 개 필요한데 하나는 ‘Preference의 키 값’, 그리고 ‘Preference를 저장하는 형식’을 의미한다. 두 번째 인자는 클라이언트 환경에서만 동작하게 하므로 MODE_PRIVATE을 지정해 주었다. ‘키 값’은 값을 불러오기 위해 해당하는 Preference 구분하기 위해서도 사용되기 때문에 중요하다. 이후 .edit()을 통해 저장할 값을 지정하는데 단순히 실행만을 수행하므로 with 키워드를 사용해 주었다.

private fun saveData() {
    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.birthDateValueTextView.text.toString())
        putString(EMERGENCY_INFO, getEmergencyInfo())
        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 "-"
    return "$bloodSign$bloodAlphabet"
}

private fun getEmergencyInfo(): String {
    return if (binding.emergencyInformationCheckBox.isChecked) binding.emergencyInformationEditText.text.toString() else ""
}

여기서 getBloodType()함수와 getEmergencyInfo()함수에 대해 설명하자면, 우선 SpinnerCheckBox를 통해 선택된 혈액형을 표현식으로 결합하여 String으로 반환하여 getBloodType()함수로 만들었고, if절을 활용하여 체크가 되었을 때와 안 되었을 때를 구분한 뒤 각각에 맞는 문자열로 반환시켜 getEmergencyInfo()함수를 만들어, 두 함수를 각각의 putString()에 넣어 값을 저장하도록 해주었다.

또한 각각의 putString에 들어가는 ‘키’ 값들은 모두 상수이므로, Const 파일을 따로 만들어 키 값들을 저장해 두었다.

📖Const.kt

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 EMERGENCY_INFO = "emergencyInfo"

저장한 값들을 불러오려면 MainActivity에서 다시 SharedPreference를 호출해 주어야 한다. 값을 저장했을 때와 같은 방법으로 with 키워드와 함께 getSharedPreference로 해당하는 키 값의 Preference에 저장된 값을 불러왔다. 해당하는 코드 또한 길어지므로 getDataAndUiUpdate() 함수로 호출되게끔 설정해 두었다.

📖MainActivity.kt

private fun getDataAndUiUpdate() {
    with(getSharedPreferences(USER_INFORMATION, Context.MODE_PRIVATE)) {
        binding.nameValueTextView.text = getString(NAME, "미정")
        binding.birthDateValueTextView.text = getString(BIRTHDATE, "미정")
        binding.bloodTypeValueTextView.text = getString(BLOOD_TYPE, "미정")
        binding.emergencyContactValueTextView.text = getString(EMERGENCY_CONTACT, "미정")

        val emergencyInfo = getString(EMERGENCY_INFO, "미정")

        binding.emergencyInformationTextView.isVisible = emergencyInfo.isNullOrEmpty().not()
        binding.emergencyInformationValueTextView.isVisible = emergencyInfo.isNullOrEmpty().not()

        if (!emergencyInfo.isNullOrEmpty()) {
            binding.emergencyInformationValueTextView.text = emergencyInfo
        }
    }
}

저장된 값들을 각각의 키 값으로 불러내어 뷰에 적용시켜주었다. 여기서 주의사항값은 생략이 가능하도록 설정되어 있어 불러오는 코드가 좀 더 있는데, 우선 불러온 값을 emergencyInfo 변수에 넣어주었고, 해당 변수가 null 값인지 아닌지에 대한 Boolean 값에 따라 뷰의 visibility속성의 값을 적용시켜 주었다. null 값을 확인하기 위해 isNullOrEmpty() 메서드를 사용했고 뒤에 not()을 붙여야 visibility의 값을 옳게 지정할 수 있다. 예를 들어, 저장된 값이 있다고 가정해보면 값은 이렇게 된다.

주의사항의 표시 유무 = false.not()

값이 들어있기 때문에 isNullOrEmpty()에서는 false가 되고, 이는 즉, 뷰가 보여야 한다는 뜻이다. 뷰가 보여지도록 하기 위해서는 값이 true가 되어야 하기 때문에 뒤에 not()을 붙여준 것이다. 논리값을 완벽히 다루는 것은 아직도 멀게만 느껴진다…

위 코드의 결과, 아무 값도 저장하지 않았을 때, 주의사항을 포함하어 저장할 때, 주의사항 없이 저장할 때 모두 값이 잘 저장된 것을 볼 수 있다.

save_result1 save_result2 save_result3

여기서 중요한 점은 저장된 값을 가져올 때, onCreate()가 아닌 onResume()에서 가져온다는 것이다. 다른 액티비티에 있다가 다시 돌아올 때 액티비티의 생명주기는 onResume()에서부터 시작한다. onCreat()에서 호출할 경우, 저장된 값을 불러오지 못한다. 따라서 불러오는 기능을 수행하는 함수는 onResume()에서 호출된다.

📖MainActivity.kt

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




📔기능 구현 - 데이터 초기화하기

데이터를 저장하고 불러왔으니 이제는 데이터를 초기화 해보도록 하자. 데이터 초기화는 값이 저장되어 있는 Preference에서 clear()를 해주기만 하면 된다.

먼저 activity_main.xml에 초기화 기능을 수행하는 FloatingActionButton을 하나 더 추가하였다.

📖activity_main.xml

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/deleteButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="36dp"
    android:src="@drawable/ic_baseline_delete_24"
    app:layout_constraintBottom_toTopOf="@id/goEditActivityButton"
    app:layout_constraintEnd_toEndOf="parent" />

delete1

이제 클래스 파일로 가서 기능을 구현해보자. 버튼 deleteButton을 가져와 클릭 리스너를 구현하고 그 내부에 deleteData() 함수를 호출해 주었다.

📖MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    binding.deleteButton.setOnClickListener {
        deleteData()
    }
}

...

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

deleteData() 함수에서는 위에서 말한 바와 같이 Preference를 가져와 해당 데이터를 clear() 시켜주었다. 중요한 점은 Preference의 기능을 구현한 뒤, 꼭 commit() 또는 apply()를 호출해야 반영된다는 것이다. DatePickerToastshow()와 비슷하다고 생각하면 된다.

초기화 한 후에는 getDataAndUiUpdate() 함수를 호출하여 변경된 사항과 UI를 업데이트 시켜주었고, 사용자가 직관적으로 이해할 수 있도록 하기 위해 업데이트의 완료를 알리는 Toast 메세지를 띄우도록 하였다.

기능이 잘 구현되었음을 확인할 수 있다.

delete




📔기능 구현 - 전화 앱 실행하기

마지막으로 등록된 전화번호의 텍스트와 이미지 영역을 클릭하면 해당 번호로 전화 앱을 연결시켜주는 기능을 구현해 보도록 하자.

DatePicker에서도 영역을 표시할 때 Layer를 사용했듯이, 이번에도 전화 모양의 이미지 뷰를 추가해준 뒤, 전화번호 값과 함께 영역으로 지정해 주었다.

📖activity_main.xml


    ...

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

    ...

영역이 지정되었다면 클래스 파일에서 클릭 리스너를 구현해 준다. 전화 앱 연결 시 Intent를 사용하는데, 앞서 화면 전환 시 사용했던 Intent와는 용도가 조금 다르다.

화면 전환에 사용되었던 IntentEditActivity라는 호출할 대상을 명확하게 알고 있었다. 이렇듯 호출할 대상을 확실히 알 수 있는 경우, 이를 **명시적 인텐트**라고 한다.

반면, 현재 구현할 Intent는 전화 앱을 실행하지만 어떤 전화 앱을 실행할 지 알 수 없는 경우, 다시 말해 해당되는 액션과 데이터를 지정하지만, 호출할 대상이 달라질 수 있는 경우에는 이를 **암시적 인텐트**라고 한다.

Intent 간의 차이를 알았지만, 이외에도 많은 개념을 보유하고 있기 때문에 Intent에 대해서 나중에 더 자세히 알아보도록 하자.

다시 본론으로 돌아와, 전화 앱을 실행시키도록 구현하려면 Intent 인자에 ‘어떤 기능을 수행할 지’를 넣어주어야 한다. 여기서는 연락처를 표시하기 때문에 인자로 Intent.ACTION_VIEW값을 넣어주었다. 값을 지정해준 다음에는 Uri를 가지고 연락처를 인텐트 데이터로 지정한다. 이 과정에서 번호 사이의 “-“ 문자를 replace()를 사용하여 빈 값으로 대체해 주었다. 마지막으로 startActivity()를 통해 Intent를 실행시키도록 하였다.

📖MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    ...

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

결과가 잘 구현된 것을 볼 수 있다.

intent intent


📔전체 코드

https://github.com/Becomeproo/emergency_information/tree/master

📝마무리

구현하는 과정에 있어서 정리해야 될 부분들이 많이 늘었다. SharedPreference가 파일을 생성해서 저장한다는 점을 다시 알고, Intent 정확한 사용 등 기존에 알고 있던 내용들을 겉핥기 식으로만 알고 있을 뿐이었다는 점에서 많이 반성하게 되었고 정리하면서 확실히 알고 가야한다고 느꼈다. 부지런히 움직이자.