연습장/실습
Basic 6주차_ 1. Enum Class, Sealed Class 사용
아이른
2024. 5. 1. 20:26
- Enum Class, Sealed Class 이론
![]() |
![]() |
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
)
}
}