Android/TOYTOY

Android 계산기 앱 (LayoutInflater, Room, Thread)

sh1mj1 2022. 9. 14. 12:00

바로 이전 포스팅에서 Room 에 대해 정리했다. 이 기능을 이용해서 간단한 계산기 앱을 구현할 것이다.

계산기에 이전 계산 기록을 Room에 저장하고 Dao 을 만들어 인서트, 쿼리 등의 함수를 만들 것이다.

크게 사용된 기능은 아래와 같다.

  • Layout
    • TableLayout
      • 키패드 레이아웃 구성할 때 사용
    • Constriantlayout
    • LayoutInflater
      • 계산 기록을 하났기 추가할 때 사용
  • Room
    • local DB 에 계산 기록 저장할 때
  • Thread
    • DB 에서 데이터를 불러오거나 저장, 업데이트할 때 사용
  • 확장함수 
    • isNumber 사용.

 

UI 구성

 

하단에 계산기 키패드 레이아웃을 TableLayout 으로 구성하고 상단에는 따로 View 을 만들어 공간을 만들어준다.

<View
    android:id="@+id/topLayout_V"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/keyPad_Tl"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_weight="1" />
<TableLayout
    android:id="@+id/keyPad_Tl"
    android:layout_width="0dp"
    android:layout_height="0dp"
	padding, constraint 설정 code...
    android:shrinkColumns="*"
    app:layout_constraintVertical_weight="1.5">

constraintVertical_weight 은 얼마나의 공간이  분배될지의 값이다.  위쪽 topLayout_V 에는 1 을, 아래쪽 keyPad_Tl 에는 1.5 을 주어 공간을 분배해주었다.

shrinkColumns = " 줄이고자 하는 column의 인덱스 지정. (시작값은 0)" 이고 * 가 우변에 들어가게 되면 모든 column 이 대상이 된다.

이와 비슷하게

stretchColumns = "늘이고자 하는 column의 인덱스 지정. (시작값은 0)" 이다.

 

TableLayout 에는 TableRow 을 5개 만들어 주고 각 layout_weight = "1" 로 주어서 크기가 같도록 만들어주었다.

각 TableRow 에는 4개의 AppCompatButton 이 들어간다. 이 때 총 20개의 버튼이 만들어진다.

나중에 코틀린 코드에서 이 버튼에 대해 하나하나 xml의 id 을 참조하여 변수를 만들어주고 setOnClickListener 를 지정하려고 하니 코드가 너무 많아질 것이다. 

그래서 xml 에서 뷰바인드를 통해 직접 코드와 연결할 것이다.

<androidx.appcompat.widget.AppCompatButton
    android:id="@+id/clear_Btn"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_margin="7dp"
    android:background="@drawable/button_background"
    android:onClick="clearBtnClicked"
    android:stateListAnimator="@null"
    android:text="C"
    android:textSize="24sp" />

위는 계산기 키패드의 clear( 화면 에서 C 로 표시) 버튼의 예시이다.

onClick 이라는 속성에 이름을 주고 해당 이름의 함수를 MainActivity.kt 에서 지정해줄 것이다.

온클릭 안에는 코드에 정의된 함수 이름이 들어가면 이 버튼이 눌렸을 때 연결된 코틀린 코드에서 그 함수에 있는 것이 실행이되고 id을 통해 어떤 버튼이 눌렸는지 알아내서 그 버튼 기능 수행하도록 할 것이다.

 

android:stateListAnimator 은 버튼을 눌렀을 때 기본적으로 나오는 애니메이션을 설정해 줄 수 있다.

stateListAnimator 을 @null 주면 없어진다. 

버튼의 용도에 따라 onClick을 아래 이름의 함수로 지정하였다.

clearBtnClicked, btnClicked, historyBtnClicked, resultBtnClicked,

 

커스텀 Drwable

그리고 버튼에는 커스텀 drawable 을 만들어주었다.

종류는 두가지 이다.  눌렸을 때(ripple) 버튼 색깔이 진한 회색이 되는 것과 진한 연두색이 되는 것이다. 아래는 진한 회색이 되는 drawable/button_background.xml 코드이다.

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/buttonPressGray">
    <item android:id="@android:id/background">
        <shape android:shape="rectangle">
            <solid android:color="@color/buttonGray" />
            <corners android:radius="100dp" />
            <stroke
                android:width="1dp"
                android:color="@color/buttonPressGray" />
        </shape>
    </item>

</ripple>

 

그리고 화면 상단에 입력하는 계산식이 표시되는 TextView, 중간에 결과가 표시되는 TextView 을 만들었다.

계산기 키패드 화면

 

그리고 시계 모양의 버튼을 클릭하면 이전 계산 기록을 보여준다. 이 때  버튼 클릭 시 기존 키패드 부분을 모두 덮는 새로운 Layout 이 나타나게 할 것이다.

새로운 Layout 은 ConstraintLayout 으로 하고 그 안에 ScrollView, 또 그 안에 LinearLayout, 그리고 계산 기록을 삭제하는 버튼을 만든다.

 

<!--    새 창이 열리는 기능 구현-->
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/history_Cl"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@color/white"
    android:visibility="gone"
    constraint 설정

    >
    <!--        닫기 버튼, 계산 기록을 지울 수 있는 버튼, 스크롤 뷰-->

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/close_Btn"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:onClick="closeHistoryBtnClicked"
        android:text="닫기"
        ...
 />


    <ScrollView
        android:layout_width="0dp"
        android:layout_height="0dp"
        ...>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/history_Ll"
            android:orientation="vertical"
            />

    </ScrollView>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/historyClear_Btn"
        android:onClick="historyClearBtnClicked"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="계산기록 삭제"
        ...

        />


</androidx.constraintlayout.widget.ConstraintLayout>

 

여기서도 onClick 속성을 주었다.

이제 LinearLayout 안에 각 계산 기록이 떠야 한다. 계산 기록 추가는 코틀린 코드에서 동적으로 구현해야 한다. 그러므로 동적으로 추가될 Layout 을 따로 만들어주어야 한다. 이를 history_row.xml 파일로 만듭시다.

동적으로 추가될 Layout인 history_row.xml

이제 UI 관련 xml 파일 작업은 다 끝났다!

 

 

 

 

Room 설정

이 계산기에서는 계산 기록을 Room 에 저장할 것이다. Android Room 을 사용하기 위해서는 전 포스팅에서 봤듯이 build.gradle(Module) 에 추가해 주어야 하는 사항이 있다.

plugins {
	...
    id 'kotlin-kapt'
}

kapt (Anotation Processing for kotlin) 는 코틀린이 자바 파일의 Annotation 처리 시 kotlin 파일의 Annotation 처리를 할 수 있도록 해준다. 

Project 내부에서 Hilt, Room, Databinding 등의 라이브러리가 사용된다면 기존에 자바 Annotation Process에서 kapt 로 Processing 되는 것이다. 

kapt 는 ksp 가 탄생한 후 ksp 를 사용하여 빌드 속도를 더 빠르게 할 수 있다고 하는데 이는 나중에 알아봅시다.

dependencies {
    def room_version = "2.4.3"
    implementation("androidx.room:room-runtime:$room_version")
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // To use Kotlin annotation processing tool (kapt)
    kapt("androidx.room:room-compiler:$room_version")

Room 사용을 위한 종속성 추가이다.

 

계산기록이 저장될 History 데이터 클래스 (model)

@Entity
data class History(

    @PrimaryKey val uid: Int?, // uid는 Primary key 태그를 달아줄 것임.
    @ColumnInfo(name = "expression") val expression: String?,
    @ColumnInfo(name = "result") val result: String?

)

자바에서는 데이터가 저장될 클래스를 만들고 다른 클래스에서 이를 사용할 때 getter 와 setter 을 사용해야 하지만 코틀린을 사용하면 그러한 과정 없이 단순히 데이터 클래스만 사용하면 된다!! 또 네 가지 변수가 자동으로 생성되는 코드 효과를 얻을 수 있다. 이것 때문에 자바에 비해 코틀린으로 개발할 때 굉장히 편하다~ 이렇게 생성자를 입력해서 손쉽게 만들어준다. 

변수는 val 이기 때문에 setter 는 생성되지 않고 데이터 클래스로 만들어졌기 때문에 toEqualHashcode이다. 

데이터 클래스를 DB의 테이블로 사용하기 위해서는 클래스 위에 어노테이션 엔티티를 추가해준다. 또 각 변수들도 어떠한 이름으로 DB에 저장이 되는지 @ColumnInfo 로 명시를 해주었다. 

 

HistoryDao 설정 (DAO)

@Dao
interface HistoryDao {

    // TODO:  데이터 클래스 History 을 굳이 대분자로 적지 않아도 되는 듯? 나중에 바꿔보자
    @Query("SELECT * FROM history")
    fun getAll(): List<History>

    @Insert
    fun insertHistory(history: History)

    @Query("DELETE FROM history")
    fun deleteAll()

    @Delete
    fun delete(history: History)

// WHERE 문에 조건으로 걸리면서 해당 조건에 맞는 것들을 LIMIT 숫자 만큼 반환됨.
    @Query("SELECT * FROM history WHERE result LIKE :result LIMIT 1")
    fun findByResult(result: String): List<History>

}


여기서 Room에 있는 데이터들을 어떻게 관리할 것인지 정해준다. 여러 쿼리문으로 여러 함수를 만들 수 있지만 이번 앱에서는 간단히 위에서 세가지 기능만 사용할 것이다. MySQL 문법은 천천히 더 공부하도록 합시다.

 

AppDatabase 설정 (Database)

@Database(entities = [History::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
    abstract fun historyDao(): HistoryDao // 앱데이터 베이스를 생성할 때 HistoryDao 을 가져올 수 있도록.
}

 

엔티티가 무엇인지 지정을 잘 해주어야 하고, 버전을 지정해주어야 한다.

버전을 지정하는 이유는 아래와 같다.

  • 앱을 업데이트 할 수록 디비가 바뀔 수도 있다.버전 1 -> 버전 2 로 넘어갈 때 migration을 해줌.
    • 예를 들어 result 컬럼이 빠지ㅏ고 expression 에 전부 넣는 것으로 바뀐다하면 데이터들을 이전하면서 데이터테이블 구조도 바뀌어야 할 필요가 있다.
    • 버전이 올라가게 되면 마이그레이션 코드를 작성을 해서 테이블 구조도 동기화되도록 해야 한다.

여기까지 하면 Room 설정이 모두 끝났다. 이제 코드를 짜도록 합시다.

 

MainActivity

Properties

// Mark -Properties/////////////////////////////////////////
private val expressionTV: TextView by lazy {
    findViewById(R.id.expression_Tv)
}
private val resultTV: TextView by lazy {
    findViewById(R.id.result_Tv)
}
private var isOperator = false
private var hasOperator = false

private val historyLayout: View by lazy {
    findViewById(R.id.history_Cl)
}
private val historyLinearLayout: LinearLayout by lazy {
    findViewById(R.id.history_Ll)
}

lateinit var db: AppDatabase

isOperator 와 hasOperator 는 계산 수식이 잘 완성되었는지를 판단하기 위해서 만든 변수이다.

계산 기록을 볼 때와 보지 않을 때 뷰가 보였다가 보이지 않았다가 해야 한다.  (historyLayout)

또 계산기록이 하나하나 차곡차곡 쌓일 것이므로 historyLinearLayout 을 선언해준다.

db 도 미리 선언해주겠습니다.

LifeCycle onCreate

// Mark -LifeCycle/////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    db = Room.databaseBuilder(
        applicationContext,
        AppDatabase::class.java,
        "historyDB"
    ).build()
}

db 을 액티비티가 생성되었을 대 미리 빌드해줍니다.

이제 아래 생성한 함수들을 살펴봅시다.

fun btnClicked(v: View) {
    when (v.id) {
        R.id.Btn_0 -> numBtnClicked("0")
        R.id.Btn_1 -> numBtnClicked("1")
        R.id.Btn_2 -> numBtnClicked("2")
        R.id.Btn_3 -> numBtnClicked("3")
        R.id.Btn_4 -> numBtnClicked("4")
        R.id.Btn_5 -> numBtnClicked("5")
        R.id.Btn_6 -> numBtnClicked("6")
        R.id.Btn_7 -> numBtnClicked("7")
        R.id.Btn_8 -> numBtnClicked("8")
        R.id.Btn_9 -> numBtnClicked("9")
        R.id.Btn_plus -> opBtnClicked("+")
        R.id.Btn_minus -> opBtnClicked("-")
        R.id.Btn_multiply -> opBtnClicked("×")
        R.id.Btn_divide -> opBtnClicked("÷")
    }

}

activity_xml 에서 onClick 속성으로 함수 btnClicked 을 썼으므로 이 함수는 private 이면 안된다.

버튼을 클릭함에 있어 어떤 메소드를 실행할 지 입력한 매우 높은 계층의 함수이다.

 

그렇다면 numBtnClicked 함수의 기능은? 아래에 바로 작성하겠다.

numBtnClicked

private fun numBtnClicked(number: String) {

    if (isOperator) {
        expressionTV.append(" ")
    }
    isOperator = false

    val expressionText = expressionTV.text.split(" ")
    if (expressionText.isNotEmpty() && expressionText.last().length >= 15) {
        Toast.makeText(this, "숫자는 15자리 이상으로 입력할 수 없습니다.", Toast.LENGTH_SHORT).show()
        return
    } else if (number == "0" && expressionText.last().isEmpty()) {
        Toast.makeText(this, "0은 제일 앞에 올 수 없습니다..", Toast.LENGTH_SHORT).show()
    }
    expressionTV.append(number)

	// resultView에 실시간으로 연산 결과를 넣을 예정.
    resultTV.text = calculateExpression()
}

계산식은 피연산자(숫자), 연산자, 피연산자(숫자) (91 - 20 의 형태) 로만 입력할 수 있게 디자인했다. (숫자를 넣고 한칸 띄우고 연산자, 다시 한칸 띄우고 숫자로 배치할 것이다.)  그리고 numBtnClicked 는  숫자를 입력한 것이므로 isOperator 를 다시 false 로 해준다.

 

이 함수의 핵심은 아래와 같다.

  • 숫자끼리 뭉쳐넣고 한칸 띄고 연산자 한칸 띄고 숫자로 배치할 것.
  • String 에서는 split 이라는 함수로 리스트에 넣어준다. split 에서 사용하는 구분자를 공백문자(space)로 할 것임.
  • 숫자가 들어왔을 대 expressionText 가 비어있을 때는 무조건 숫자 입력 가능
  • 실시간으로 연산결과를 화면에 표시

마지막에 resultTV 에 실시간으로 연산 결과를 넣어야 한다. calculateExpression() 함수는 아래처럼 코드를 구성했다.

 

calculateExpression()

private fun calculateExpression(): String {
    val expressionTexts = expressionTV.text.split(" ")

    if (hasOperator.not() || expressionTexts.size != 3) {
        return ""
    } else if (expressionTexts[0].isNumber().not() || expressionTexts[2].isNumber().not()) {
        return ""
    }
    val exp1 = expressionTexts[0].toBigInteger()
    val exp2 = expressionTexts[2].toBigInteger()
    val op = expressionTexts[1]

    return when (op) {
        "+" -> (exp1 + exp2).toString()
        "-" -> (exp1 - exp2).toString()
        "×" -> (exp1 * exp2).toString()
        "÷" -> (exp1 / exp2).toString()
        else -> ""
    }

}

calculateExpression() 은 문자열을 반환한다.

이 함수는 계산 결과를 표시해주는 것이므로 계산결과를 표시해주면 안될 때의 코드도 작성해야 한다. 

 

expressionTexts 라는 리스트에 피연산자, 연산자, 피연산자를 넣어준다. 물론 이 세가지가 없을 수도 있다. 이 때는 몇가지만 리스트에 들어가게 될 것이다.

연산자가 없거나 세가지 아이템이 리스트에 없을 때 리턴

리스트에 첫 아이템이 숫자가 아니거나 마지막 아이템이 숫자가 아니면 리턴.

복잡한 연산을 수행하는 것이 아니므로 이 정도로만 조건을 작성해도 모든 에러의 경우를 커버할 수 있을 것이다.

 

조건을 모두 통과한 경우 올바른 수식이 입력된 경우이므로 연산결과를 리턴한다.

 

이 때 나는 isNumber() 라는 이름의 확장함수를 사용했다. 확장함수는 클래스 외부에서 이렇게 선언해 주었다.

fun String.isNumber(): Boolean {
    return try {
        this.toBigInteger()
        true
    } catch (e: NumberFormatException) {
        false
    }
}
String 을 확장하는 확장함수. 앞에 개체(타입)를 넣으면 해당 타입 메소드를 만들 수 있다.!!!
함수형 프로그래밍에 굉장히 중요한 테크닉!!!

확장함수를 선언할 때 타입을 앞에 적고 dot(.) 을 적은 후 새로 만들 함수 이름을 적으면 된다.

toBigInteger는 toInteger 보다 더 큰 정수를 커버할 수 있는 메소드이다.

 

이제 연산자를 입력했을 때의 함수를 만들어 줍시다.

optBtnClicked

private fun opBtnClicked(operator: String) {
    if (expressionTV.text.isEmpty()) {
        return
    }
    when {
        isOperator -> {
            val text = expressionTV.text.toString()
            expressionTV.text = text.dropLast(1) + operator
        }
        hasOperator -> {
            Toast.makeText(this, "연산자는 한번만 사용할 수 있습니다.", Toast.LENGTH_SHORT).show()
            return
        }

        else -> {
            expressionTV.append(" " + operator)
        }
    }
    val ssb = SpannableStringBuilder(expressionTV.text)
    ssb.setSpan(
        ForegroundColorSpan(getColor(R.color.green)),
        expressionTV.text.length - 1,
        expressionTV.text.length,
        Spannable.SPAN_INCLUSIVE_EXCLUSIVE
    )
    expressionTV.text = ssb

    isOperator = true
    hasOperator = true

}
  • 수식에 아무 것도 없을 때 리턴
    • 수식에 연산자가 마지막으로 적혔을 때 또 연산자를 입력한 경우 연산자를 교체, 
    • 수식이 완성된 상태에서 연산자를 또 입력했을 시 토스트 메시지
    • 위 두가지 경우가 아니라면, 즉 피연산자가 수식에 하나이고 정상적인 순서에 연산자가 입력됬으면 수식에 추가해준다.

SpannableStringBuilder 라는 클래스를 사용하여 수식에 특정 설정을 해주었다. 공식 문서에는 아래처럼 말한다.

This is the class for text whose content and markup can both be changed.

setSpan 사용법, 인수

연산자의 색만 초록 색으로 하기 위해서 

초록색으로 컬러 설정, 수식의 총 길이 -1 부터 총 길이까지 설정한다.

SPAN_INCLUSIVE_EXCLUSIVE 는 색깔(또는 다른 설정)의  설정을 setSpan 의 두번째 인수를 포함하고 세번째 인수는 포함하지 않는다는 의미이다.

예를 들어 (10 + ) 이렇게 되어 있으면 총길이-1 위치인 +를 포함한 곳부터 마지막 위치인 공백까지, 이 때 공백은 설정에 포함되지 않게 해준다는 것이다.

 

그 후 isOperator, hasOperator 를 true 로 해준다. 이는 위에서 적은 함수에서 사용되는 조건이므로 잊지 말고 잘 적어주어야 한다.

 

clearBtnClicked

fun clearBtnClicked(v: View) {
    expressionTV.text = ""
    resultTV.text = ""
    isOperator = false
    hasOperator = false

}

수식, 결과식 초기화

 

historyBtnClicked

fun historyBtnClicked(v: View) {
    // DB에서 히스토리의 값을 가져와서 historyLinearLayout에 그 값을 넣고, historyLayout VISIBLE
    historyLayout.isVisible = true
    historyLinearLayout.removeAllViews()

    // TODO: DB에서 모든 기록 가져오기
    // TODO: 뷰에 모든 기록 할당하기
    // 저장할 떄 최신의 데이터가 아래에 보여짐. 계산기 히스토리에서는 최신 것이 위에 보여져야 되므로 뒤집기.
    Thread(Runnable {
        db.historyDao().getAll().reversed().forEach {
            // 현재 이 스레드는 메인 스레드가 아니다. 메인 스레드로 전환을 해주어야 함.
            runOnUiThread {
                val historyView =
                    LayoutInflater.from(this).inflate(R.layout.history_row, null, false)
                historyView.findViewById<TextView>(R.id.expression_Tv).text = it.expression
                historyView.findViewById<TextView>(R.id.result_Tv).text = " = ${it.result}"


                historyLinearLayout.addView(historyView)
            }
        }
    }).start()

LayoutInlflater, Thread, Room 을 모두 사용하는 이 프로젝트에서 가장 핵심 함수라고 볼 수 있다.

  • 히스토리 버튼이 클릭되면 먼저 DB에서 히스토리의 값을 가져와서 historyLinearLayout에 그 값을 넣고, historyLayout 이 보이게 해야 한다.
  • Room 에서 데이터를 가져오고 화면에 보이게 해야 하므로 메인 스레드가 아닌 다른 스레드에서 db 에 접근하여 모든 데이터를 가져온다.
    • 이 때 계산 기록 순서는 최신의 것이 가장 위로 가게 하도록 reversed() 해준다.
    • forEach 라는 메서드를 사용해서 계산 기록을 하나하나 LinearLayout 에 넣어준다. 
    • 이 때 UI 작업을 해야하므로 runOnUiThread 을 사용한다. 이전 포스팅에서도 언급했듯이 메인스레드에서만 UI 작업을 할 수 있다! (이전 포스팅을 보지 않았다면 보고 오시는 게 도움이 됩니다.)

LayoutInflater 

  • setContentView 의 인수로 들어가는 통(whole) Layout xml 파일 말고도 작은 layout xml 파일을 여러 개 사용할 수 있다. 이런 작은 layout을 메모리에 올려줄 수 있게 도와주는 것이 layoutInflater.
  • 반복적으로 사용되는 xml 파일을 한꺼번에 올리는 것이 아니라 view 단위로 쪼개서 사용해보고 layoutInflater 을 통해서 메모리에 할당해 불러오는 것을 하고 있다.
  • 성능 상의 이유로 view inflation 은 XML 파일의 전처리(build time에 수행됨)에 의존한다. 그래서 런타임에서 plain XML 을 통해 XmlPullParser 로 LayoutInflater 을 사용하는 것은 불가능하다. 컴파일된 리소스로부터 리턴된 XmlPullParser 로만 동작한다.
    • inflate 의 인수는
      • resource - XML 아이디.
      • root - view의 parent ViewGroup 을 지정한다. (view 의 root)
      • attachToRoot - child 뷰를 parent 뷰에 지금 즉시 붙일지 결정.
        • true : child 뷰를 parent 뷰에 지금 즉시 붙임.
        • false: child 뷰를 parent 뷰에 지금 붙이지 않음. ViewGroup은 LayoutParams 을 전달하는 역할만 하고 parent 뷰는 child 뷰의 터치 이벤트를 가져오지 않는다. (LayoutParams 는 parent 뷰 안에 뷰가 어떻게 배치될지 결정하는 속성임). 그 후 addView 을 수행했을 때 뷰가 붙는다.
      • 리턴 - 확장된 계층의 루트 뷰. 만약 루트가 제공되고 attachToRoot 가 참이면 이것이 루트이다. 그게 아니면 확장된 XML 파일의 루트이다.

historyClearBtnClicked, closeHistoryBtnClicked 

fun historyClearBtnClicked(v: View) {
    historyLinearLayout.removeAllViews()

    Thread(Runnable {
        db.historyDao().deleteAll()
    }).start()

}

이 함수 역시 XML 에서 OnClick 속성으로 준 함수이다. 

뷰에서 모든 기록 삭제 

Thread 에서 디비의 모든 기록 삭제
뷰의 모든 기록은 이번에는 다른 쓰레드에서 삭제할 필요가 없다.

fun closeHistoryBtnClicked(v: View) {
    historyLayout.isVisible = false
}

 

resultBtnclicked

fun resultBtnClicked(v: View) {
    // 연산을 했던 것과 똑같이 해주면 댐.
    val expressionTexts = expressionTV.text.split(" ")
    if (expressionTV.text.isEmpty() || expressionTexts.size == 1) {
        return
    }
    if (expressionTexts.size != 3 && hasOperator) {
        Toast.makeText(this, "아직 완성되지 않은 수식입니다.", Toast.LENGTH_SHORT).show()
        return
    }
    if (expressionTexts[0].isNumber().not() || expressionTexts[2].isNumber()
            .not()
    ) { // 사실 이 오류가 발생한다는 것은 이전에 코드가 잘못되었다는 것이지만 이중 오류 제어
        Toast.makeText(this, "오류가 발생했습니다.", Toast.LENGTH_SHORT).show()
        return
    }

    val expressionText = expressionTV.text.toString()
    val resultText = calculateExpression()

    // expressionText와 resultText 는 아래에서 바뀌니까 위에서 미리 변수로 저장해 둠.
    Thread(Runnable {
        db.historyDao().insertHistory(History(null, expressionText, resultText))
    }).start()

    resultTV.text = ""
    expressionTV.text = resultText

    isOperator = false
    hasOperator = false
}

마지막 함수이다.

위에 calculateExpression() 함수를 이해했다면 어렵지 않게 이해할 수 있다.

 

 

앱 시연

 

아래 깃허브에서 모든 코드를 확인할 수 있다.

https://github.com/sh1mj1/Calculator_04

 

GitHub - sh1mj1/Calculator_04: Android Calculator_04

Android Calculator_04. Contribute to sh1mj1/Calculator_04 development by creating an account on GitHub.

github.com

 

사용하는 코드들이 많아지면서 이제 함수를 하나하나 살펴보는 것은 힘들 것 같다. 다음 포스팅부터는 대략적인 설계도만 살펴보고 그 중에서 헷갈리거나 중요하다고 생각되는 것만 적어야 겠다.. 너무 시간이 오래 걸려서... ㅎ

 

 

https://developer.android.com/reference/android/view/LayoutInflater

https://blog.naver.com/guen5997/222625879574

https://3edc.tistory.com/65