티스토리 뷰

1. 안드로이드 계산기 어플 앱 만들기 with 코틀린

이번에 진행한 프로젝트는 안드로이드 계산기 어플리케이션 만들기입니다. 간단하게 사용자가 입력한 식을 연산할 수 있고 계산 기록을 확인할 수 있는 기능을 가진 앱을 만들어보았습니다.

특징

  • 사용자가 입력한 식 더하기, 빼기, 곱하기, 나누기, 나머지 연산
  • 식 Clear 기능
  • 계산 히스토리 확인 가능 및 기록 초기화 기능
  • room을 사용한 로컬 DB: 종료 후 재실행 하여도 계산 기록 저장

사용된 프로젝트 전체 코드는 저의 깃허브 저장소에서 확인이 가능합니다.

마찬가지로 이번 글에서도 프로젝트를 진행하며 작성된 모든 코드를 기록하는 방식이 아닌 제가 느끼기에 생소한 기술들이나 참신했던 코드 위주로 요약해서 다음에 참고해서 개발이 가능하도록 작성하도록 하겠습니다.

1.2. 해당 프로젝트에 사용된 기술 및 자원

  • 레이아웃
    • TableLayout 사용
    • ConstraintLayout 사용
    • LayoutInflater 사용
  • Thread 사용
    • 메인 스레드 이외 타 Thread 만들어서 사용하기
    • runOnUiThread 사용하기
  • Room 사용하기
  • Kotlin
    • 확장 함수 사용
    • data class 사용

2. TableLayout 으로 계산기 버튼 구성하기

우리가 보통 사용하는 계산기 어플과 마찬가지로 5행 4열로 구성한 테이블 레이아웃을 사용해서 연산자 및 숫자 그리고 기능 버튼들을 추가하였습니다.

shrinkColumns

이때 android:shrinkColumns="*" 속성으로 테이블 행의 너비가 테이블 레이아웃을 넘어버리는 경우 해당 열의 길이를 자동으로 줄여서 테이블 레이아웃의 전체 너비를 벗어나지 않도록 할 수 있습니다.

* 를 사용해서 전체 열에 대해서 설정을 해준 모습입니다. 0 베이스로 만약에 첫번째 세번째 열을 자동으로 조절하겠다 하면 "0,2" 를 값으로 주면 되겠죠?

androidx.appcompat.widget.AppCompatButton

버튼 대신에 AppCompatButton을 사용한 이유는 기본 테마로 지정된 보라색 버튼 말고 우리는 하얀 바탕의 배경의 검은색 텍스트를 기반으로한 버튼을 아용하기 위해서입니다. 또한 android:stateListAnimator="@null"를 사용해서 애니메이션을 제거해줄 수도 있습니다.

뷰 바인드로 버튼 이벤트 처리

각각의 버튼에 사용된 onClick 속성으로는 모두 buttonClicked 함수를 지정해서 buttonClicked 함수 내부에서 해당 뷰의 id를 가져와서 각각에 해당하는 id에 맞는 기능을 수행하도록 when 문을 사용해서 정의해주었습니다.

여기서 들었던 궁금증 그런데 저렇게 buttonClicked 라는 명칭만으로 어떻게 MainActivity.kt 코드에 있는 함수를 참조할까 생각해보니 아무래도 매니페스트 파일에 있는 메인 엑티비티의 name 부분에 있는 .MainActivity 라는 명칭을 가지고 참조하게 될 거란 것을 추측할 수 있었습니다.

2.1. 특정 화면 비율로 뷰 나누기

보통 계산기 화면을 보면 키패드 부분의 영역이 계산식과 결과가 표시되는 영역보다 약간 큰 것을 알 수 있죠. 그래서 마찬가지로 해당 앱에서도 뷰를 일정한 비율인 1:1.5로 표시되도록 나타내주는 것이 필요했습니다.

이는 app:layout_constraintVertical_weight="1" 코드를 통한 수직 가중치 비율을 정해주는 것으로 설정할 수 있습니다. 테이블 레이아웃 위를 차지할 View를 하나 정의하고 그 가중치를 1로 줍니다. 이후 테이블 레이아웃에서는 가중치를 1.5로 설정합니다. 아래와 같이 뷰가 특정 비율로 잘 나눠지는 것을 확인할 수 있습니다.

2.2 버튼 둥글게 하고 ripple 효과 주기

다음은 계산기 버튼을 둥글게하고 ripple 애니메이션 효과를 주는 방법입니다. drawable xml파일을 2개 생성합니다. (각각 초록버튼 하얀버튼) 그리고 ripple 태그로 감싸고 아래와 같은 코드로 추가해주시면 되겠습니다.

이때 shape로 rectangle을 주어 사각형을 기반으로하고 radius를 100dp로 설정해서 가능한한 최대의 원형 값을 갖도록 해주면 알아서 동글동글한 버튼의 형태로 drawable 객체를 만들 수 있습니다.

그리고 메인 엑티비티로 xml 파일로 넘어가서 background로 방금 만들어주었던 drawable 객체를 설정해주시면 되겠습니다. 아래는 초록색 버튼 백그라운드로 설정하는 코드 예시입니다. 이처럼 모든 버튼에 각각 배경을 설정해주시면됩니다.

3. 계산 기록 레이아웃 구성

계산 기록 레이아웃은 메인 엑티비티에 ConstraintLayout을 추가해서 구성합니다. 구성 시 프리뷰에 계산기 버튼이 가려질 수 있는데 이는 코드상에서 히스토리 버튼을 누를 때 마다 해당 뷰를 띄워주고 가려주고 할 것이기 때문에 크게 신경쓰지 맙니다.

ConstraintLayout 내부에 닫기 버튼 - 스크롤 뷰 - 계산기록 삭제 버튼 순서대로 달아주도록 하겠습니다. 제약은 계산식과 결과창 아래로 뜨게 설정해줍니다.

스크롤 뷰를 조금 더 자세히 설명하자면, 레이아웃 제약으로 바닥은 [검색 기록 삭제] 버튼 바로 위 그리고 천장은 [닫기] 버튼 바로 아래로 설정해주도록 합니다.

내부에는 레이아웃이 필요하며 LinearLayout을 넣어 나중에 계산 기록을 여기에 LayoutInflater로 하나하나 수직으로 쌓아주도록 설정합니다. 이때 layout_height 속성의 경우에는 wrap_content가 권장됩니다. 그래야 스크롤 뷰를 사용하느 의미가 있겠죠.

3.1. LayoutInflater 로 계산기록 넣기

계산 기록은 사용자가 계산할 때마다 로컬 디비에 저장 한뒤 이를 히스토리 버튼을 누를 때 레이아웃 인플레이터로 즉각 생성해서 위에서 작성해준 스크롤뷰 내부의 리니어 레이아웃 안에 넣어줘서 유저가 확인할 수 있게 할겁니다.

그래서 먼저 계산 기록 레이아웃을 따로 만들어주도록 합니다. 아래와 같이 history_row.xml 을 따로 만든 후 제약 레이아웃에 텍스트 뷰 2개를 넣어서 계산 식과 계산 결과를 볼 수 있도록 정의해주었습니다.

이후에 사용자가 히스토리 버튼을 누르면 LayoutInflater로 우리가 작성해둔 히스토리 레이아웃은 history_row 를 불러와서 각각의 텍스트뷰에 계산식과 결과를 넣어서 히스토리 LinearLayout에 넣어주면 끝입니다.

        // 디비에서 모든 기록 가져오기
        // 뷰에 모든 기록 할당하기
        Thread(Runnable {
            db.historyDao().getAll().reversed().forEach {
                // 뷰 생성하여 넣어주기
                // 레이아웃 인플레이터 기능 사용 해보기
                // ui 스레드 열기
                runOnUiThread {
                    // 핸들러에 포스팅될 내용 작성

                    // R.layout.history_row 에서 인플레이트를 시킴., root랑 attachToRoot. 나중에 addview를 통해 붙일거라 null, false
                    val historyView =
                        LayoutInflater.from(this).inflate(R.layout.history_row, null, false)
                    historyView.findViewById<TextView>(R.id.expressionTextView).text = it.expression
                    historyView.findViewById<TextView>(R.id.resultTextView).text = "= ${it.result}"

                    historyLinearLayout.addView(historyView) // 뷰 추가
                }
            } // 리스트 뒤집어서 가져오기
        }).start()

4. Room 활용한 로컬 DB 사용하기

Room 을 활용하면 SQLite를 좀더 쉽게 사용하면서 로컬디비를 구성할 수 있습니다. Room 을 사용해서 로컬 디비를 구성 및 사용하는 방법 및 타 스레드 사용 방법은 분량이 길어서 따로 작성해두었으니 이전 포스팅을 참고하시면 되겠습니다.

4.1. 확장 함수 사용하여 String isNumber 구현

확장 함수는 말 그대로 이미 정의된 클래스에 확장해서 우리가 필요한 기능을 넣어줄 때 사용할 수 있는 아주 유용한 기능으로 C# 등에도 있는 기능입니다. 여기서는 입력받은 숫자 문자열이 숫자로 변환이 가능하지 않다면 예외처리 시 false를 반환하면서 숫자가 아닌 것을 확인할 수 있도록 작성한 확장 함수를 추가해서 사용했습니다.

// 확장 함수 정의
fun String.isNumber(): Boolean {
    return try {
        this.toBigInteger() // 무한대 까지 저장 가능한 자료형
        true
    } catch (e: NumberFormatException) {
        false
    }
}

4.2. Kotlin Data Class

코틀린에서는 데이터 클래스라는 문법을 제공하는데 클래스 사용시 데이터를 보관할 목적으로 사용할 클래스라면 따로 toString(), hashCode(), equals(), copy() 매소드 및 getter, setter를 우리가 만들어줄 필요없이 자동으로 만들어주므로 편리하게 사용이 가능합니다.

데이터 클래스는 생성자로 1개 이상 프로퍼티가 선언되어야 하며, 생성자 프로퍼티는 val 또는 var로 선언해야합니다. 데이터 클래스에 abstract, open, sealed, inner를 붙일 수 없고 상속을 받을 수 없다는 특징이 있습니다.

package lilcode.aop.p2.c04.calculator.model

import androidx.room.ColumnInfo
import androidx.room.Entity // room 사용
import androidx.room.PrimaryKey

// 데이터 클래스로 선언
@Entity(tableName = "history_table") // room 사용(room의 데이터 클래스) db 테이블
data class History(
    @PrimaryKey val uid: Int?, // 유니크한 아이디 (기본키로 사용되는 듯)
    @ColumnInfo(name = "expression") val expression: String?,
    @ColumnInfo(name = "result") val result: String?
)
댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함