연습장/실습

Basic 6주차_ 1. Enum Class, Sealed Class 사용

아이른 2024. 5. 1. 20:26

Main My Records

 

더보기
activity_main.xml activity_second.xml item_current_record.xml item_wrong_record.xml

 

1. Record.kt

① Enum Class

data class Record(
    val trial: Int,
    val target: Int,
    val record: Int,
    val isCorrect: AnswerType
)

enum class AnswerType(val answerValue: Int, val text: String) {
    CORRECT(answerValue = 1, text = "Correct!"),
    WRONG(answerValue = 0, text = "Wrong!")
}

 

② Sealed Class

더보기
data class Record(
    val trial: Int,
    val target: Int,
    val record: Int,
    val isCorrect: AnotherAnswerType,
)

sealed interface AnotherAnswerType {
    data class Correct(
        val answerValue: Int = 1,
        val text: String = "Correct!"
    ) : AnotherAnswerType

    data class Wrong(
        val answerValue: Int = 0,
        val text: String = "Wrong!",
    ) : AnotherAnswerType
}

 

③ Enum Class vs Sealed Class (1)

  • Enum Class : 생성자에 따른 매개변수를 반드시 추가

  • Sealed Class : 필요한 생성자만 작성 가능

 

2. DummyData.kt

val myRecords = mutableListOf<Record>()

 

3. RecordRecyclerViewAdapter.kt

 

① Enum Class

interface RecordClickListener {
    fun onClickItem(record: Record)
}//클릭 이벤트 처리

/*
이전까지 
val RecordClickListener: RecordClickListener? = null 
와 같은 방식으로 클릭이벤트 처리를 하였는데, 이는 예전 자바의 방식을 따른 것.
널이 될 수 있기 때문에 널이 될 수 있는 공간도 필요로 하기 때문에(메모리/성능)
불필요한 널체크는 지양
*/

class RecordRecyclerViewAdapter(
    private val recordClickListener: RecordClickListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var records: List<Record> = emptyList()

    class CorrectViewHolder(
        private val binding: ItemCorrectRecordBinding,
        private val recordClickListener: RecordClickListener
    ) : RecyclerView.ViewHolder(binding.root) {
        private var record: Record? = null

        init {
            binding.root.setOnClickListener {
                record?.let {
                    recordClickListener.onClickItem(record = it)
                }
            }
        }

        fun bind(recordItem: Record) {
            this.record = recordItem
            binding.tvTrial.text = recordItem.trial.toString()
            binding.tvTarget.text = recordItem.target.toString()
            binding.tvRecord.text = recordItem.record.toString()
            binding.tvCorrect.text = recordItem.isCorrect.text
        }
    }
    /*
    RecordClickListener 생성자를 주입해서 널처리를 하는 이유
    만약,  bind() 메서드가 데이터가 많아져 구문이 10초 걸린다고 가정하였을 때
    클릭(RecordClickListener)은 가능하나 bind()는 아직 시간이 걸리므로 
    그 전까지는 널이기 때문
    */

    class WrongViewHolder(
        private val binding: ItemWrongRecordBinding,
        private val recordClickListener: RecordClickListener
    ) : RecyclerView.ViewHolder(binding.root) {
        private var record: Record? = null

        init {
            binding.root.setOnClickListener {
                record?.let {
                    recordClickListener.onClickItem(record = it)
                }
            }
        }

        fun bind(recordItem: Record) {
            this.record = recordItem
            binding.tvTrial.text = recordItem.trial.toString()
            binding.tvTarget.text = recordItem.target.toString()
            binding.tvRecord.text = recordItem.record.toString()
            binding.tvCorrect.text = recordItem.isCorrect.text
        }
    }

    override fun getItemViewType(position: Int): Int {
        return records[position].isCorrect.answerValue
    }

    override fun getItemId(position: Int): Long {
        return records[position].trial.toLong()
    }
    /*
    아이템의 고유 아이디가 중복됨을 확인하는 작업을 도와주는 setHasStableIds(true)을
    사용하기위해 재정의(리사이클러뷰의 성능 향상을 위해 onBindViewHolder 호출 최소화)
    추후에, 데이터를 삭제할 일이 있다면 고유 아이디를 통해 해당 아이디만 삭제할 수 있음
    */

    fun submitList(items: List<Record>) { // List<T>
        this.records = items
        notifyDataSetChanged()
    }//데이터 갱신

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val answerType = AnswerType.entries.find { it.answerValue == viewType }
        val layoutInflater = LayoutInflater.from(parent.context)
        return when (answerType) {
            AnswerType.CORRECT -> CorrectViewHolder(
                binding = ItemCorrectRecordBinding.inflate(layoutInflater, parent, false),
                recordClickListener = recordClickListener
            )

            AnswerType.WRONG -> WrongViewHolder(
                binding = ItemWrongRecordBinding.inflate(layoutInflater, parent, false),
                recordClickListener = recordClickListener
            )

            else -> throw IllegalStateException("answerType cannot be null!")
        }
    }

    override fun getItemCount(): Int {
        return this.records.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val recordItem = records[position]
        when (holder) {
            is CorrectViewHolder -> {
                holder.bind(recordItem = recordItem)
            }
            
            is WrongViewHolder -> {
                holder.bind(recordItem = recordItem)
            }
        }
    }
}

 

② Sealed Class

더보기
interface RecordClickListener {
    fun onClickItem(record: Record)
}

class RecordRecyclerViewAdapter(
    private val recordClickListener: RecordClickListener
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var records: List<Record> = emptyList()

    class CorrectViewHolder( // Static Nested class
        private val binding: ItemCorrectRecordBinding,
        private val recordClickListener: RecordClickListener
    ) : RecyclerView.ViewHolder(binding.root) {
        private var record: Record? = null

        init {
            binding.root.setOnClickListener {
                record?.let {
                    recordClickListener.onClickItem(record = it)
                }
            }
        }

        fun bind(recordItem: Record) {
            this.record = recordItem
            binding.tvTrial.text = recordItem.trial.toString()
            binding.tvTarget.text = recordItem.target.toString()
            binding.tvRecord.text = recordItem.record.toString()
            binding.tvCorrect.text = when (recordItem.isCorrect) {
                 is AnotherAnswerType.Correct -> recordItem.isCorrect.text
                 is AnotherAnswerType.Wrong -> recordItem.isCorrect.text
             }
        }
    }

    class WrongViewHolder(
        private val binding: ItemWrongRecordBinding,
        private val recordClickListener: RecordClickListener
    ) : RecyclerView.ViewHolder(binding.root) {
        private var record: Record? = null

        init {
            binding.root.setOnClickListener {
                record?.let {
                    recordClickListener.onClickItem(record = it)
                }
            }
        }

        fun bind(recordItem: Record) {
            this.record = recordItem
            binding.tvTrial.text = recordItem.trial.toString()
            binding.tvTarget.text = recordItem.target.toString()
            binding.tvRecord.text = recordItem.record.toString()
            binding.tvCorrect.text = when (recordItem.isCorrect) {
                is AnotherAnswerType.Correct -> recordItem.isCorrect.text
                is AnotherAnswerType.Wrong -> recordItem.isCorrect.text
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (val isCorrect = records[position].isCorrect) {
             is AnotherAnswerType.Correct -> isCorrect.answerValue
             is AnotherAnswerType.Wrong -> isCorrect.answerValue
         }
    }

    override fun getItemId(position: Int): Long {
        return records[position].trial.toLong()
    }

    fun submitList(items: List<Record>) { // List<T>
        this.records = items
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
             0 -> WrongViewHolder(
                 binding = ItemWrongRecordBinding.inflate(layoutInflater, parent, false),
                 recordClickListener = recordClickListener
             )

             1 -> CorrectViewHolder(
                 binding = ItemCorrectRecordBinding.inflate(layoutInflater, parent, false),
                 recordClickListener = recordClickListener
             )

             else -> throw IllegalStateException()
         }
    }

    override fun getItemCount(): Int {
        return this.records.size
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val recordItem = records[position]
        when (holder) {
            is CorrectViewHolder -> {
                holder.bind(recordItem = recordItem)
            }

            is WrongViewHolder -> {
                holder.bind(recordItem = recordItem)
            }
        }
    }
}

 

③ Enum Class vs Sealed Class (2)

  • Enum Class : 필요한 부분만 구현
  • Sealed Class : 모든 하위 클래스 구현

 

4. MainActivity.kt

 

① Enum Class

class MainActivity : AppCompatActivity(), View.OnClickListener {
    private var job: Job? = null

    private lateinit var binding: ActivityMainBinding

    private var counter = 1
    private var randomValue = (1..100).random()
    private var isStopped = false


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        if (savedInstanceState != null) {
            randomValue = savedInstanceState.getInt("randomValue")
            isStopped = savedInstanceState.getBoolean("isStopped")
        }

        initButtons()
        setRandomValueBetweenOneToHundred()
    }

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

    override fun onPause() {
        super.onPause()
        job?.cancel()
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        counter = savedInstanceState.getInt("counter")
        if (counter > 100) {
            counter = 100
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("counter", counter)
        outState.putInt("randomValue", randomValue)
        outState.putBoolean("isStopped", isStopped)
    }

    private fun initButtons() {
        binding.clickButton.setOnClickListener {
            checkAnswerAndShowToast()
            job?.cancel()
            isStopped = true
        }
        binding.restartButton.setOnClickListener(this)
        binding.checkRecordButton.setOnClickListener(this)
    }

    private fun setRandomValueBetweenOneToHundred() {
        binding.textViewRandom.text = randomValue.toString()
    }


    private fun setJobAndLaunch() {
        job?.cancel()
        job = lifecycleScope.launch {
            while (counter <= 100) {
                if (isActive) {
                    binding.spartaTextView.text = counter.toString()
                    if (isStopped) {
                        break
                    }
                    delay(250)
                    counter += 1
                }
            }
        }
    }

    private fun checkAnswerAndShowToast() {
        val spartaText = binding.spartaTextView.text.toString()
        val randomText = binding.textViewRandom.text.toString()
        if (spartaText == randomText) {
            Toast.makeText(this, "Correct!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1, 
                    //myRecords(비어있는 리스트)가 최상위 함수
                    //인덱스 0부터 더하기 때문에 Wrong이 되어도 
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnswerType.CORRECT
                )
            )
        } else {
            Toast.makeText(this, "Wrong!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1,
                    //이미 리스트에 값이 쌓여있기 때문에 그 인덱스 값에서 +1을 하여도
                    //리스트 trial은 1부터 순서대로 번호가 들어가게 됨
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnswerType.WRONG
                )
            )
        }
    }

    override fun onClick(p0: View?) {
        p0?.let {
            when (it) {
                binding.restartButton -> {
                    isStopped = false
                    counter = 1
                    randomValue = (1..100).random()
                    setRandomValueBetweenOneToHundred()
                    setJobAndLaunch()
                }

                binding.checkRecordButton -> {
                    if (myRecords.isEmpty()) {
                        Toast.makeText(this, "You don't have any record!", Toast.LENGTH_SHORT)
                            .show()
                        return
                    }
                    val intent = Intent(this, SecondActivity::class.java)
                    intent.putExtra("test", AnswerType.CORRECT);
                    //데이터를 넘길 수 있음을 보여주기 위해 CORRECT만 처리
                    startActivity(intent)
                }
            }
        }
    }
}

 

② Sealed Class

더보기
class MainActivity : AppCompatActivity(), View.OnClickListener {
    private var job: Job? = null

    private lateinit var binding: ActivityMainBinding

    private var counter = 1
    private var randomValue = (1..100).random()
    private var isStopped = false


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        if (savedInstanceState != null) {
            randomValue = savedInstanceState.getInt("randomValue")
            isStopped = savedInstanceState.getBoolean("isStopped")
        }

        initButtons()
        setRandomValueBetweenOneToHundred()
    }

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

    override fun onPause() {
        super.onPause()
        job?.cancel()
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        counter = savedInstanceState.getInt("counter")
        if (counter > 100) {
            counter = 100
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("counter", counter)
        outState.putInt("randomValue", randomValue)
        outState.putBoolean("isStopped", isStopped)
    }

    private fun initButtons() {
        binding.clickButton.setOnClickListener {
            checkAnswerAndShowToast()
            job?.cancel()
            isStopped = true
        }
        binding.restartButton.setOnClickListener(this) 
        binding.checkRecordButton.setOnClickListener(this) 
    }

    private fun setRandomValueBetweenOneToHundred() {
        binding.textViewRandom.text = randomValue.toString()
    }


    private fun setJobAndLaunch() {
        job?.cancel() 
        job = lifecycleScope.launch {
            while (counter <= 100) {
                if (isActive) {
                    binding.spartaTextView.text = counter.toString()
                    if (isStopped) {
                        break
                    }
                    delay(250)
                    counter += 1
                }
            }
        }
    }

    private fun checkAnswerAndShowToast() {
        val spartaText = binding.spartaTextView.text.toString()
        val randomText = binding.textViewRandom.text.toString()
        if (spartaText == randomText) {
            Toast.makeText(this, "Correct!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1,
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnotherAnswerType.Correct()
                )
            )
        } else {
            Toast.makeText(this, "Wrong!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1,
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnotherAnswerType.Wrong()
                )
            )
        }
    }

    override fun onClick(p0: View?) {
        p0?.let {
            when (it) {
                binding.restartButton -> {
                    isStopped = false
                    counter = 1
                    randomValue = (1..100).random()
                    setRandomValueBetweenOneToHundred()
                    setJobAndLaunch()
                }

                binding.checkRecordButton -> {
                    if (myRecords.isEmpty()) {
                        Toast.makeText(this, "You don't have any record!", Toast.LENGTH_SHORT)
                            .show()
                        return
                    }
                    val intent = Intent(this, SecondActivity::class.java)
                }
            }
        }
    }
}

③ Enum Class vs Sealed Class (3)

  • Enum Class : @Parcelable, @Serializable 처리가 없어도 자체적으로 처리가 가능하기 때문에 intent로 값을 넘길 수 있음
//데이터 넘기기
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("test", AnswerType.CORRECT)

//데이터 받기
intent.getSerializableExtra("test", AnswerType::class.java)
  • Sealed Class : @Parcelable, @Serializable 처리를 따로 해주어야만 데이터를 넘길 수 있음

 

5. SecondActivity.kt

① Enum Class

class SecondActivity : AppCompatActivity() {
    private val TAG = "SecondActivity"
    private lateinit var binding: ActivitySecondBinding
    private lateinit var adapter: RecordRecyclerViewAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySecondBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            Toast.makeText(
                this,
                "Test : " + intent.getSerializableExtra("test", AnswerType::class.java),
                Toast.LENGTH_SHORT
            ).show()
        } else {
            Toast.makeText(
                this,
                "Test : " + intent.getSerializableExtra("test"),
                Toast.LENGTH_SHORT
            ).show()
        }

        adapter = RecordRecyclerViewAdapter(recordClickListener = object : RecordClickListener {
            override fun onClickItem(record: Record) {
                Toast.makeText(this@SecondActivity, "${record.isCorrect.text}", Toast.LENGTH_SHORT)
                    .show()
            }
        })
        adapter.setHasStableIds(true)
        binding.recyclerView.adapter = this.adapter
        adapter.submitList(myRecords)
    }
}

 

② Sealed Class

더보기
class SecondActivity : AppCompatActivity() {
    private val TAG = "SecondActivity"
    private lateinit var binding: ActivitySecondBinding
    private lateinit var adapter: RecordRecyclerViewAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySecondBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        adapter = RecordRecyclerViewAdapter(recordClickListener = object : RecordClickListener {
            override fun onClickItem(record: Record) {
                when (val isCorrect = record.isCorrect) {
                    is AnotherAnswerType.Correct -> Toast.makeText(
                        this@SecondActivity,
                        isCorrect.text, Toast.LENGTH_SHORT
                    ).show()

                    is AnotherAnswerType.Wrong -> Toast.makeText(
                        this@SecondActivity,
                        "${isCorrect.text}, You are stupid, and stupid level: ${isCorrect.stupidLevel}",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        })
        adapter.setHasStableIds(true)
        binding.recyclerView.adapter = this.adapter
        adapter.submitList(myRecords) 
    }
}

③ Enum Class vs Sealed Class (3-1)

    private fun checkAnswerAndShowToast() {
        val spartaText = binding.spartaTextView.text.toString()
        val randomText = binding.textViewRandom.text.toString()
        if (spartaText == randomText) {
            Toast.makeText(this, "Correct!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1,
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnswerType.CORRECT
                )
            )
        } else {
            Toast.makeText(this, "Wrong!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                element = Record(
                    trial = myRecords.size + 1,
                    target = randomText.toInt(),
                    record = spartaText.toInt(),
                    isCorrect = AnswerType.WRONG
                )
            )
        }
    }
  • Enum Class : Record에 선언한 생성자를 전부 기입 = 디폴트 값이 없기 때문
  • Sealed Class : 디폴트 값을 설정하여 작성 가능
sealed interface AnotherAnswerType {
    data class (클래스이름)(
...
    ) : AnotherAnswerType
    
    data object Default(디폴트 값이 필요한 생성자) : AnotherAnswerType

}

 

더보기

Enum Class_ 디폴트 값 만들기

//Record.kt
data class Record(
    val trial: Int,
    val target: Int,
    val record: Int,
    val isCorrect: AnswerType = AnswerType.WRONG
)

...

//MainActivity.kt
    private fun checkAnswerAndShowToast() {
        val spartaText = binding.spartaTextView.text.toString()
        val randomText = binding.textViewRandom.text.toString()
        val record = Record(
            trial = myRecords.size + 1,
            target = randomText.toInt(),
            record = spartaText.toInt(),
        )
        if (spartaText == randomText) {
            Toast.makeText(this, "Correct!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                record.copy(isCorrect = AnswerType.CORRECT)
            )
        } else {
            Toast.makeText(this, "Wrong!", Toast.LENGTH_SHORT).show()
            myRecords.add(
                record
            )
        }
    }