TIL) 0901

Hanseul Lee·2022년 9월 1일
0

TIL

목록 보기
3/23
post-custom-banner

String.xml로 string값을 전달하기

값을 전달 받을 곳에 %s.

// string.xml

<string name="tip_amount">Tip Amount: %s</string>

tools 속성으로 텍스트를 세팅하는 것이 포인트다.

// activity_main.xml

<TextView
        android:id="@+id/tip_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Tip Amount: $10"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/calculate_button" />
// MaincActivity.kt

val formattedTip = NumberFormat.getCurrencyInstance().format(tip) // 숫자 포매팅
binding.tipResult.text = getString(R.string.tip_amount, formattedTip)

java.lang.NumberFormatException: empty String

아래 코드에서 위 오류가 나는 부분은 어디일까?

private fun calculateTip() {
        val stringInTextField = binding.costOfService.text.toString()
        val cost = stringInTextField.toDouble()
        val selectedId = binding.tipOptions.checkedRadioButtonId
        val tipPercentage = when (selectedId) {
            R.id.option_twenty_percent -> 0.20
            R.id.option_eighteen_percent -> 0.18
            else -> 0.15
        }
				...
}

바로 cost 값을 받는 부분이다. 문자열을 입력 받는데 값이 비어있거나, 유효하지 않은 자료형일 경우 제목과 같은 오류가 난다. 그렇다면 해결할 수 있는 방법은?

private fun calculateTip() {
        val stringInTextField = binding.costOfService.text.toString()

        val cost = stringInTextField.toDoubleOrNull()
        if (cost == null) {
						binding.tipResult.text = "" // 초기화를 해서 이전에는 잘 보냈다가 새롭게 빈 값을 보냈을 때를 대비하자
            return
        }

        val selectedId = binding.tipOptions.checkedRadioButtonId
        val tipPercentage = when (selectedId) {
            R.id.option_twenty_percent -> 0.20
            R.id.option_eighteen_percent -> 0.18
            else -> 0.15
        }
				...
}

다른 자료형 그러니까 문자열과 같은 경우를 따로 처리하지 않은 이유는, 애초에 키보드 입력을 numberDecimal 로만 받아서 숫자 입력만 가능하기 때문이다.

Themes

앱에서 사용하는 색상과 관련된 속성들은 다음과 같이 12개로 그룹화 되어있다.

cf) 색상코드 앞 #FF는 알파값으로 100% 불투명하다는 의미.

#NameTheme Attribute
1PrimarycolorPrimary
2Primary VariantcolorPrimaryVariant
3SecondarycolorSecondary
4Secondary VariantcolorSecondaryVariant
5BackgroundcolorBackground
6SurfacecolorSurface
7ErrorcolorError
8On PrimarycolorOnPrimary
9On SecondarycolorOnSecondary
10On BackgroundcolorOnBackground
11On SurfacecolorOnSurface
12On ErrorcolorOnError

https://material.io/resources/color/#!/?view.left=0&view.right=0
2023.04.11 new version

미리 정의된 팔레트가 다양하게 있고, 이것을 내 UI 적용했을 때 어떻게 보이는지 쉽게 확인 가능하다.

런처 아이콘 커스텀

Android 8.0(API 26)부터 adaptive laucher icon이 지원되어서 앱 아이콘을 더욱 유연하고 시각적으로 구현할 수 있게 되었다. 앱 아이콘은 다음 그림처럼 back과 fore로 구성되어 있다.

https://developer.android.com/static/codelabs/basic-android-kotlin-training-change-app-icon/img/1af36983e3677abe.gif

다음과 같이 프로젝트 창 > res > mipmap-anydip-v26에서 리소스를 찾아 변경할 수 있다.

기본적으로는 아래 코드.

<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

https://developer.android.com/codelabs/basic-android-kotlin-training-change-app-icon?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fandroid-basics-kotlin-unit-2-pathway-2%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fbasic-android-kotlin-training-change-app-icon#5

Material Design

https://material.io/develop/android

UI 위젯으로 이 컴포넌트를 사용하면 앱이 사용자가 사용하는 장치의 다른 앱과 일관된 방식으로 작동한다. 이게 왜 중요하냐면 사용자에게 익숙하기 때문에 앱 사용법을 빨리 익힐 수 있게 된다!

먼저 뷰에 대해 두 가지 예시를 보자.

  • EditView 기존 방식은 다음과 같다면
    <EditText
            android:id="@+id/cost_of_service"
            android:layout_width="160dp"
            android:layout_height="wrap_content"
            android:hint="@string/cost_of_service"
            android:inputType="numberDecimal"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    새 방식은 감싸는 형태다.
    <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/cost_of_service"
            android:layout_width="160dp"
            android:layout_height="wrap_content"
            android:hint="@string/cost_of_service"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/cost_of_service_edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="numberDecimal" />
    
        </com.google.android.material.textfield.TextInputLayout>
    이런 식으로 생겼기 때문!
  • Switch
    <com.google.android.material.switchmaterial.SwitchMaterial
            android:id="@+id/round_up_switch"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:checked="true"
            android:text="@string/round_up_tip"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="@id/tip_options"
            app:layout_constraintTop_toBottomOf="@id/tip_options" />

Style에 대해서도 지침이 있는데 우선은 TextView로 맛보기.

// style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
        <item name="android:minHeight">48dp</item>
        <item name="android:gravity">center_vertical</item>
        <item name="android:textAppearance">?attr/textAppearanceBody1</item>
    </style>
</resources>
// activity_main.xml

<TextView
        android:id="@+id/tip_result"
        style="@style/Widget.TipTime.TextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Tip Amount: $10"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/calculate_button" />
  • minHeight → 모든 행의 최소 높이는 48dp여야 한다.
  • textAppearance → 지정된 타이포 스타일
    Attribute nameDefault style
    textAppearanceHeadline1Light 96sp
    textAppearanceHeadline2Light 60sp
    textAppearanceHeadline3Regular 48sp
    textAppearanceHeadline4Regular 34sp
    textAppearanceHeadline5Regular 24sp
    textAppearanceHeadline6Medium 20sp
    textAppearanceSubtitle1Regular 16sp
    textAppearanceSubtitle2Medium 14sp
    textAppearanceBody1Regular 16sp
    textAppearanceBody2Regular 14sp
    textAppearanceCaptionRegular 12sp
    textAppearanceButtonMedium all caps 14sp
    textAppearanceOverlineRegular all caps 10sp

cf) 자주 활용하는 값은 dimens.xml에서 관리하면 편하다.

// style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
        <item name="android:minHeight">@dimen/min_text_height</item>
        <item name="android:gravity">center_vertical</item>
        <item name="android:textAppearance">?attr/textAppearanceBody1</item>
    </style>
</resources>
// dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="min_text_height">48dp</dimen>
</resources>

cf) RadioButton이나 Switch, TextLayout.OutlinedBox와 같은 스타일은 Theme.xml에서 수정해야 한다. 다크 버전에도 똑같이!

<!-- Text input fields -->
        <item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
        <!-- For radio buttons -->
        <item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
        <!-- For switches -->
        <item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>

사용자가 키보드 엔터했을 때 키보드 숨기기

binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode) }

private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            // Hide the keyboard
            val inputMethodManager =
                getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
            return true
        }
        return false
    }

단위 테스트(Unit Test)

당장은 작은 프로그램만 만들기 때문에 수동 테스트가 쉽지만, 앱의 규모가 커지면 자동 테스트가 훨씬 유용하다.

  • 자동 테스트 → 소프트웨어를 통해 실행된 테스트
  • 수동 테스트 → 기기와 직접 상호작용하는 사람이 실행한 테스트

딱 봐도 자동 테스트가 유용해보인다. 그럼 자동 테스트의 종류 중 하나인 단위 테스트를 어떻게 할까?

우선은 JUnit가 라이브러리가 필요하다.

testImplementation 'junit:junit:4.12'

단위 테스트는 항상 test 디렉토리에 있다. 아무것도 하지 않은 상태에서 샘플을 열어보자.

  1. 우선 컴파일러가 테스트 메서드인 걸 알 수 있게 @TEST 주석을 달자.
  2. assert 메서드는 단위 테스트의 최종 목표다. 궁극적으로 코드에서 가져온 결과가 특정 상태에 있다고 assert(주장)하고 보는 게 좋다.
  3. 캡쳐에서 알 수 있듯 expected는 예상값, actual은 결과값이다.
  4. assertEquals()는 예상값과 결과값이 일치하지 않으면 테스트가 실패했다 본다.

cf) 다양한 assert 메서드

  • assertEquals()
  • assertNotEquals()
  • assertThat()
  • assertTrue()
  • assertFalse()
  • assertNull()
  • assertNotNull()

계측 테스트(Instrumentation Test)

계측 테스트(Instrumentation Test)를 실습으로 알아보자

Parcelize

Parcel은 꾸러미다. 그러니까 짐을 싸듯 객체를 싸는 클래스를 바로 Parcel 클래스라 한다.

안드로이드에서는 프로세스 간 통신을 위해 Bundle 클래스를 사용하는데, Bundle은 Key와 Value 형태인 Map으로 내부에 많은 데이터가 들어간 것을 Value로 입력하기 쉽지 않다. 이런 때에 사용하는 것이 바로 Parcel이다.

그리고 이 Parcel을 쉽게 만들고 풀어주기 위한(비직렬화) 인터페이스를 Parcelable이라 한다.

참고로 아래 유형을 지원하니 슬쩍 알아두자.

이제 실습으로 어떻게 사용하는지 알아보자. 이 코드는 Room과 Navigation을 활용해 데이터를 변경할 때(업데이트) 사용할 계획이다.

  1. 플러그인을 추가하자.

    plugins {
        id("kotlin-parcelize")
    }
  2. 꾸러미로 만들 데이터 클래스에 @Parcelize 주석을 하자.

    import kotlinx.parcelize.Parcelize
    
    @Parcelize
    @Entity(tableName = "user_table")
    data class User(
        @PrimaryKey(autoGenerate = true)
        val id: Int,
        val firstName: String,
        val lastName: String,
        val age: Int
    ): Parcelable
  3. Nav_Graph에서 Fragment에 매개변수를 추가하자.

  4. Safe Args를 사용해 데이터를 보내자.

    // ListAdpater.kt
    
    inner class MyViewHolder(binding: CustomRowBinding) : RecyclerView.ViewHolder(binding.root) {
            private val id = binding.idTxt
            private val firstName = binding.firstNameTxt
            private val lastName = binding.lastNameTxt
            private val age = binding.ageTxt
            private val container = binding.rowLayout
    
            fun bind(userData: List<User>, position: Int) {
                ...
    					
                container.setOnClickListener {
                    val action = ListFragmentDirections.actionListFragmentToUpdateFragment(currentItem)
                    it.findNavController().navigate(action)
                }
            }
        }

    코드가 잘 이해가지 않는다면 다음 포스트를 참고하자.

    https://velog.io/@hs0204/Navigation을-알아보자#5-안전한-데이터-전달을-위해-safe-args를-사용하자

  5. 데이터를 받을 Fragment에서 필요한 작업을 해주자.

    // UpdateFragment.kt
    
    private val args by navArgs<UpdateFragmentArgs>() // 데이터를 받는 변수
    
    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            _binding = FragmentUpdateBinding.inflate(inflater, container, false)
            val view = binding
            
            binding.updateFirstNameEt.setText(args.currentUser.firstName)
            binding.updateLastNameEt.setText(args.currentUser.lastName)
            binding.updateAgeEt.setText(args.currentUser.age)
    
            return view.root
    }

https://kotlinworld.com/44

https://developer.android.com/kotlin/parcelize

post-custom-banner

0개의 댓글