코틀린 안드로이드 - 계산기3(계산기록 저장 기능,Room)

Jamwon·2021년 6월 23일
0

Kotlin_Android

목록 보기
14/30
post-thumbnail

이번에는 계산 기록 저정 기능! 화이팅!!

layout 추가해주기

activity_main.xml

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/historyLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/white"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/keypadTableLayout"
        tools:visibility="visible">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_close"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@null"
            android:onClick="closeHistoryButtonClicked"
            android:stateListAnimator="@null"
            android:text="닫기"
            android:textColor="@color/black"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ScrollView
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_margin="10dp"
            app:layout_constraintBottom_toTopOf="@id/btn_historyclear"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn_close">

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

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_historyclear"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="47dp"
            android:layout_marginEnd="47dp"
            android:layout_marginBottom="38dp"
            android:background="@drawable/button_background_green"
            android:onClick="historyClearButtonClicked"
            android:stateListAnimator="@null"
            android:text="계산기록 삭제"
            android:textColor="@color/white"
            android:textSize="18sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

계산기록을 보여주는 constraint layout을 하나 만들어 준다.
app에서는 버튼들을 가리면안되기때문에 visibility 를 gone으로 주고 작업할때는
어떤식으로 보일지 보여야 되기 때문에 visible로 바꿔준다.

ScrollView를 이용한다. scrollView안에는 레이아웃을 넣어줘야된다.

위와같은 layout이 생성이된다. 이제 MainActivity에서 함수를 만들어주자

함수 구현

MainActivity.kt

3개의 함수 historyButtonClicke closeHistoryButtonClicked historyClearButtonClicked를 만들어 준다.

우선 위에서 만들어준 layout을 사용하가위해 선언해준다.

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

closeHistoryButtonClicked

간단하게 historlayout만 안보이게 바꿔주면된다!
historyLayout.isVisible = false 사용!

historyButtonClicked

위에서 만들어준 기록 히스토리 layout을 띄워주는 역활을 한다.
historyLayout.isVisible = true 사용!

여기에 DB에 저장되어있는 수식을 보여주는 기능을 만들어야되는데 이를위해서는 우서 DB를 만들어야된다!

Room DB

model 패키지

원래 있던 calculator 패키지 밑에 model 패키지를 만들어준다!

History.kt

model 패키지 안에 History.kt파일을 생성!
Entity를 사용하기 위해서 gradle에 선언을 해줘야한다.


이거!

plugin - id 'kotlin-kapt'를 추가
dependencies - implemetation "androidx.room:room-runtime:2.2.6"
kapt "androidx.room:room-compiler:2.2.6"
추가!

추가해주고 Sync Now를 적용한다. 그럼 적용이된다!

package com.example.calculator.model

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class History(
    @PrimaryKey val uid: Int?,
    @ColumnInfo(name="expression") val expression: String?,
    @ColumnInfo(name= "result") val result: String?
)

위처럼 DB를 만들때 바로 적용되게 PrimaryKey도 지정해주고 ColumnInfo로 지정해주면 History라는 DB테이블을 만들수 있다 !

HistoryDao.kt


위와같이 dao package를 만들고 HistoryDao 파일을 생성해준다.

이 Dao는 Room에 연결된 Dao이다 .

Dao란?

DAO는 Data Access Object의 약자로 Access하는 대상은 데이터베이스이다!

따라서 데이터베이스에 접근하는 객체로 데이터를 읽기,합입,삭제 등등의 데이터베이스 조작을 처리하는 기능을 가지고있다!

@Query("SELECT *FROM history")
    fun getAll():List<History>

위처럼 쿼리문을 선언할수 있다. 이는 history 데이터베이스의 모든것을 가지고오는 쿼리문이고 getAll() 함수를 통해서 사용할 수있게 선언되어있다. 신기하다...

@Dao
interface HistoryDao {
    @Query("SELECT *FROM history")
    fun getAll(): List<History>

    @Insert
    fun insertHistory(history: History)

    @Query("DELETE FROM history")
    fun deleteAll()
//
//    @Delete
//    fun delete(history: History)
//
//    @Query("SELECT * FROM history WHERE result LIKE :result LIMIT 1")
//    fun findByResult(result :String) :List<History>

}

위처럼 쿼리문들을 함수로 선언할수 있다 !!
밑에 2개는 이런식으로 더 쓰일수도 있다는 예시!
순서대로 특정 history삭제 , 원하는 조건의 history1개 반환이다.

AppDatabase.kt

database를 만들어주는 코드!

Roomdatabase 클래스를 상속받는 추상클래스를 만들어준다.

@Database 에 History table을 리스트형식으로 참조한다고 선언해야된다. 그리고 버전을 작성해줘야한다. 앱이 업데이트 될수록 DB가 변경되므로 변경될때 마다 migration을 해줘야 되는데 그렇기 때문에 version을 명시해줘서 나중에 version을 바뀔때 migration code를 작성할 수 있다.

@Database(entities = [History::class],version =1)
abstract class AppDatabase : RoomDatabase(){
    abstract fun historyDao(): HistoryDao
    
}

위와같이 HistoryDao를 선언해주면 데이터베이스를 Activity에서 사용할 수 있다. !!

다시 MainActivity.kt

    lateinit var  db: AppDatabase

    private var isOperator = false
    private var hasOperator = false

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

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

위처럼 lateinit으로 AppDatabase db를 미리선언해주고
OnCreate에서 db를 Room.databaseBuilder로 선언해서 DB를 생성해줄 수 있다.

DB에 관련된 작업은 Main Thread에서 이루어지면 안되고 다른 Thread에서 이루어져야 하기때문에 새로운 Thread를 만들어준다.

이 기능은 resultButtonClicked 안에다가 넣어준다!!

쓰레드 생성

resultButtonClicked() 함수

    fun resultButtonClicked(v: View) {
        val expressionTexts = expressionTextView.text.split(" ")
        if (expressionTextView.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 = expressionTextView.text.toString()
        val resultText = calculateExpression()


        Thread(Runnable {
            db.historyDao().insertHistory(History(null,expressionText,resultText))
        }).start()

        resultTextView.text = ""
        expressionTextView.text = resultText

        isOperator = false
        hasOperator = false

    }

Thread를 생성하고
uid는 PrimaryKey이기 때문에 자동으로 숫자가 증가하므로 처음에 null로 부여하고 위의 expressionText와 resultText를 저장해준다.!!

histortButtonClicked() 함수

우선 historyLinearLaout.removeALlViews()를 이용해 LinearLayout안에 있는 모든 View들을 삭제한다. 그리고!

DB의 history를 getAll해서 전부 가지고 온다음에 최신순으로 보여주기 위해서 reversed()를 이용해 list를 뒤집어 줍니다. 그리고 forEach를 이용해서 하나하나 꺼내서 View를 만든 다음에 LinearLayout에 붙여준다!!

지금은 붙일 View가 없기 때문에 layoutInflater를 이용해서 View를 만들어야 된다.

history_row.xml 만들기

layout 폴더에다가 history_row.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="wrap_content">

    <TextView
        android:id="@+id/txt_expression"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginTop="44dp"
        android:layout_marginEnd="15dp"
        android:gravity="end"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="9999" />

    <TextView
        android:id="@+id/txt_result"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginEnd="15dp"
        android:layout_marginBottom="15dp"
        android:gravity="end"
        android:textColor="@color/black"

        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/txt_expression"

        tools:text="9999" />
</androidx.constraintlayout.widget.ConstraintLayout>

이렇게만들어서 수식과 결과를 볼수있는 TextView를 선언한다.

Thread Runnable은 MainThread가 아니기 때문에 안에 runOnUiThread를 UIThread를 불러와줘서 거기서 layout작업을 한다.

UI Thread안에서 historyView를 생성해주고 layoutinflater를 이용해서 계산식과 결과를 가지고온다음에 LinearLayout에 하나씩 추가시켜준다.

Scroll View로 만들었기 때문에 history가 엄청 많아서 괜찮!

    fun historyButtonClicked(v: View) {
        historyLayout.isVisible = true
        historyLinearLayout.removeAllViews() //리니어 레이아웃밑에 있는 view들 전부 삭제

        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.txt_expression).text = it.expression
                    historyView.findViewById<TextView>(R.id.txt_result).text = "= ${it.result}"

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

함수 코드는 위와 같이!

historyClearButtonClicked() 함수

클리어 버튼이 눌리면 View에서 모든 기록을 삭제한다.

historyLinearLayout.removeAllViews()

를 이용한다! 그럼 리니어 레이아웃에 있는 View들을 모두 삭제!!

그리고 DB에 모든 기록을 삭제하려면 또 Thread를 열어줘서 deleteAll() 명령을 사용

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

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

함수는 이와같이!

MainActivity.kt 코드

class MainActivity : AppCompatActivity() {

    private val expressionTextView: TextView by lazy {
        findViewById<TextView>(R.id.txt_expression)
    }
    private val resultTextView: TextView by lazy {
        findViewById<TextView>(R.id.txt_result)
    }

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

    lateinit var  db: AppDatabase

    private var isOperator = false
    private var hasOperator = false

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

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

    fun buttonClicked(v: View) {
        when (v.id) {
            R.id.btn_0 -> numberButtonClicked("0")
            R.id.btn_1 -> numberButtonClicked("1")
            R.id.btn_2 -> numberButtonClicked("2")
            R.id.btn_3 -> numberButtonClicked("3")
            R.id.btn_4 -> numberButtonClicked("4")
            R.id.btn_5 -> numberButtonClicked("5")
            R.id.btn_6 -> numberButtonClicked("6")
            R.id.btn_7 -> numberButtonClicked("7")
            R.id.btn_8 -> numberButtonClicked("8")
            R.id.btn_9 -> numberButtonClicked("9")

            R.id.btn_plus -> operatorButtonClicked("+")
            R.id.btn_minus -> operatorButtonClicked("-")
            R.id.btn_multi -> operatorButtonClicked("X")
            R.id.btn_div -> operatorButtonClicked("/")
            R.id.btn_mod -> operatorButtonClicked("%")
        }
    }

    private fun numberButtonClicked(number: String) {
        if (isOperator) {
            expressionTextView.append(" ")
        }
        isOperator = false

        val expressionText = expressionTextView.text.split(" ")
        if (expressionText.isNotEmpty() && expressionText.last().length >= 15) {
            Toast.makeText(this, "15자리 까지만 사용할수 있습니다.", Toast.LENGTH_SHORT).show()
            return
        } else if (expressionText.last().isEmpty() && number == "0") {
            Toast.makeText(this, "0은 제일앞에 올 수 없습니다.", Toast.LENGTH_SHORT).show()
            return
        }
        expressionTextView.append(number)
        resultTextView.text = calculateExpression()
    }

    private fun operatorButtonClicked(operator: String) {
        if (expressionTextView.text.isEmpty()) {
            return
        }

        when {
            isOperator -> {
                val text = expressionTextView.text.toString()
                expressionTextView.text = text.dropLast(1) + operator
            }
            hasOperator -> {
                Toast.makeText(this, "연산자는 한번만 사용할 수 있습니다.", Toast.LENGTH_SHORT).show()
                return
            }
            else -> {
                expressionTextView.append(" $operator")
            }

        }
        val ssb = SpannableStringBuilder(expressionTextView.text)
        ssb.setSpan(
            ForegroundColorSpan(getColor(R.color.green)),
            expressionTextView.text.length - 1, expressionTextView.text.length,
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
        expressionTextView.text = ssb
        isOperator = true
        hasOperator = true
    }

    fun resultButtonClicked(v: View) {
        val expressionTexts = expressionTextView.text.split(" ")
        if (expressionTextView.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 = expressionTextView.text.toString()
        val resultText = calculateExpression()


        Thread(Runnable {
            db.historyDao().insertHistory(History(null,expressionText,resultText))
        }).start()

        resultTextView.text = ""
        expressionTextView.text = resultText

        isOperator = false
        hasOperator = false

    }


    private fun calculateExpression(): String {
        val expressionTexts = expressionTextView.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()
            "X" -> (exp1 * exp2).toString()
            "%" -> (exp1 % exp2).toString()
            "/" -> (exp1 / exp2).toString()
            else -> ""
        }
    }


    fun clearButtonClicked(v: View) {
        expressionTextView.text = ""
        resultTextView.text = ""
        isOperator = false
        hasOperator = false
    }

    fun historyButtonClicked(v: View) {
        historyLayout.isVisible = true
        historyLinearLayout.removeAllViews() //리니어 레이아웃밑에 있는 view들 전부 삭제

        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.txt_expression).text = it.expression
                    historyView.findViewById<TextView>(R.id.txt_result).text = "= ${it.result}"

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


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

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

        Thread(Runnable {
            db.historyDao().deleteAll()
        }).start()
    }
}
fun String.isNumber(): Boolean {
    return try {
        this.toBigInteger()
        true
    } catch (e: NumberFormatException) {
        false
    }
}

이렇게 어플이 완성됬다. 앞에 historyClearButton에 OnClick함수가 잘못 할당되어 있어서 바꿔줘야된다!

잘된다!!! 후우..뭔가 내용이 엄청 많다!

새로 배운것!

TableLayout

키패드를 만들기 위해서 사용! table row와 colume을 정해서 격자 구조를 만들수 있다.

LayoutInflater

LayoutInflater.from(this).infalte(R.layout.history_row,null,null) 과같이
새로운 xml파일을 View파일로 메모리에 올려줄수있고 View안의 인자들을 설정하고 View를 액티비티에서 새로만들어서 add해줄수있다..

Thread

저번에도 썻던것

DB작업이나 네트워크 작업을 할때는 새로운 Thread를 만들어서 사용하자

또 UI를 조절해야될때는 runOnUiThread를 사용해서 UI조정!

Room

@Database(entities = [History::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun historyDao(): HistoryDao

}

이런식으로 DB를 선언해주고

interface형식으로 DAO를 구현해준다


@Dao
interface HistoryDao {
    @Query("SELECT *FROM history")
    fun getAll(): List<History>

    @Insert
    fun insertHistory(history: History)

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

인터페이스에 쿼리를 실행할수있는 메소드들을 넣을수있다.

그리고 Room database에서 사용되는 모델 class인 entity를 dataclass 형식으로 만들어서 사용!

@Entity
data class History(
    @PrimaryKey val uid: Int?,
    @ColumnInfo(name="expression") val expression: String?,
    @ColumnInfo(name= "result") val result: String?
)

위처럼 사용

으어어어어 시험기간전에 하고 시험기간 끝나고 다시 이어서 할려니깐 기억이 뚝끊겨서 힘들었다 새로운 내용들도 많고.. 안드로이드 자체로 DB를 만들고 바로 사용할수 있다는 점이 신기했고...layout을 겹치게 써서 보여줬다가 지웠다가 하는 방법도 유용한거 같다..! 잘 활용할 수 있도록 해보자!!!

profile
한걸음씩 위로 자유롭게

0개의 댓글