[Android/Flutter 교육] 30일차

MSU·2024년 2월 7일

Android-Flutter

목록 보기
31/85
post-thumbnail

Android Project1

리사이클러뷰 새로고침

매번 메인 액티비티로 돌아올때 onResume에서 리사이클러뷰를 새로고침해줄 수 있음
하지만 onCreate에 어댑터 설정을 해놨을때 onResume에서도 어댑터 설정이 동작하므로 액티비티를 맨 처음 동작 시 어댑터 설정이 두번 중복으로 일어나게 됨

onResume에서 리사이클러 새로고침 코드를 한번만 작성하든가
런처별로 새로고침 코드를 매번 작성하든가 방법은 선택하면 됨

동물 객체 액티비티 간 전달

※ RMI 기술 참고

객체지향프로그래밍언어에서 일반적으로 어떤 객체를 사용하고싶을때 객체의 주소를 전달받아 사용함
그러나 안드로이드에서 intent를 통해 객체의 주소값을 직접 전달할 수는 없음

A 액티비티에서 B 액티비티로 이동할때 객체의 주소값을 전달해버리면
B 액티비티를 보여주는 시점에서 A 액티비티를 종료해버리는 경우 생성한 객체 또한 같이 소멸할 수 있기 때문에 B 액티비티에서는 전달받은 객체의 주소값을 사용할 수가 없다 따라서 이러한 위험성을 원천 차단하고 있음

Parcelable 인터페이스를 구현하면 해당 객체가 가지고 있는 값을 담은 Parcel객체를 전달 가능
Parcel객체를 전달받아 이동한 액티비티에서 새로운 객체를 생성해서 사용함

Lion객체를 생성하고 변수에 담아서 프로퍼티를 설정하고 animalList에 객체의 주소값을 담음

이번 프로젝트에서는 항목의 순서값만 전달하면 등록,수정,삭제 모두 해결이 가능하지만 Parcelable도 사용할 것임

리사이클러뷰 항목 클릭 이벤트

리사이클러뷰 항목 클릭 이벤트 리스너는 ViewHolder클래스의 init블럭에 작성해도 되고

        // ViewHolder
        inner class ViewHolderMain(rowMainBinding: RowMainBinding) : RecyclerView.ViewHolder(rowMainBinding.root){
            val rowMainBinding: RowMainBinding

            init {
                this.rowMainBinding = rowMainBinding

                this.rowMainBinding.root.layoutParams = ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                )

                // 항목을 누르면 ShowActivity를 실행
                this.rowMainBinding.root.setOnClickListener {
                    val showIntent = Intent(this@MainActivity, ShowActivity::class.java)
                    showIntent.putExtra("position",adapterPosition)
                    showActivityLauncher.launch(showIntent)
                }

            }
        }

onBindViewHolder 메서드 안에서 작성해도 상관없음

        override fun onBindViewHolder(holder: ViewHolderMain, position: Int) {

            // 항목을 누르면 ShowActivity를 실행
            holder.rowMainBinding.root.setOnClickListener {
                val showIntent = Intent(this@MainActivity, ShowActivity::class.java)
                showIntent.putExtra("position",position)
                showActivityLauncher.launch(showIntent)
            }
        }

특정한 조건을 만족하면 스마트 캐스팅

동물 타입별 프로퍼티를 가져올 때 명시적 형변환 대신 스마트 캐스팅을 이용할 수 있다.

  • 명시적 형변환을 사용할 경우
                when(animal.type){
                    // 사자
                    AnimalType.ANIMAL_TYPE_LION -> {
                        // 사자면 Lion 타입으로 형변환한다.
                        val lion = animal as Lion
                        append("털의 개수 : ${lion.hairCount}개\n")
                        append("성별 : ${lion.gender.str}\n")
                    }
                    // 호랑이
                    AnimalType.ANIMAL_TYPE_TIGER -> {
                        // 호랑이면 Tiger 타입으로 형변환한다.
                        val tiger = animal as Tiger
                        append("줄무늬 개수 : ${tiger.lineCount}개\n")
                        append("몸무게 : ${tiger.weight}kg\n")
                    }
                    // 기린
                    AnimalType.ANIMAL_TYPE_GIRAFFE -> {
                        // 기린이면 Giraffe 타입으로 형변환한다.
                        val giraffe = animal as Giraffe
                        append("목의 길이 : ${giraffe.neckLength}cm\n")
                        append("달리는 속도 : 시속 ${giraffe.runSpeed}km\n")
                    }
                }
  • 스마트 캐스팅을 이용할 경우
				// 사자
                if(animal is Lion){
                    append("털의 개수 : ${animal.hairCount}개\n")
                    append("성별 : ${animal.gender.str}\n")
                }
                // 호랑이
                else if(animal is Tiger){
                    append("줄무늬 개수 : ${animal.lineCount}개\n")
                    append("몸무게 : ${animal.weight}kg\n")
                }
                // 기린
                else if(animal is Giraffe){
                    append("목의 길이 : ${animal.neckLength}cm\n")
                    append("달리는 속도 : 시속 ${animal.runSpeed}km\n")
                }

text input에 setText값

무조건 String만 넣어야 하므로 int인 경우에는 문자열로 변경해줘야 한다. 안그러면 오류가 발생한다.

textFieldModifyAge.setText("${animal.age}")

slider뷰의 value 프로퍼티는 Float 타입이다

slider뷰의 value값을 넣어줄때는 Float값으로 넣어줘야 한다.

sliderModifyWeight.value = animal.weight.toFloat()

객체 수정

메인액티비티에서 가져온 position으로 객체에 접근한다.

val animal = Util.animalList[position]

접근한 객체의 프로퍼티를 직접 수정하면 된다.

    // 수정 처리
    fun modifyData(){
        // 위치 값을 가져온다.
        val position = intent.getIntExtra("position",0)
        // position번째 객체를 가져온다.
        val animal = Util.animalList[position]

        activityModifyBinding.apply {
            // 공통
            animal.name = textFieldModifyName.text.toString()
            animal.age = textFieldModifyAge.text.toString().toInt()
            // 클래스 타입별로 분기
            // 사자
            if(animal is Lion){
                animal.hairCount = textFieldModifyHairCount.text.toString().toInt()
                animal.gender = when(buttonGroupModifyGender.checkedButtonId){
                    R.id.buttonModifyGender1 -> LION_GENDER.LION_GENDER1
                    R.id.buttonModifyGender2 -> LION_GENDER.LION_GENDER2
                    else -> LION_GENDER.LION_GENDER1
                }
            }
            // 호랑이
            else if(animal is Tiger){
                animal.lineCount = textFieldModifyLineCount.text.toString().toInt()
                animal.weight = sliderModifyWeight.value.toInt()
            }
            // 기린
            else if(animal is Giraffe){
                animal.neckLength = textFieldModifyNeckLength.text.toString().toInt()
                animal.runSpeed = textFieldModifyRunSpeed.text.toString().toInt()
            }
        }
    }

수정 후 다시 info화면으로 돌아갈때도 onResume 설정

class ShowActivity : AppCompatActivity() {


    override fun onResume() {
        super.onResume()
        // 다른곳에 갔다 왔을 경우 출력 내용을 다시 구성해준다.
        setView2()
    }
}

dialog 선택 항목 설정

레이아웃 파일 작성 필요 없이 코드로 구현 가능하다.

  • setItems

  • setMultiChoiceItems

  • setSingleChoiceItems

필터 설정

필터 타입을 enum클래스로 정리

// 필터 타입
enum class FilterType(var num:Int, var str:String){
    FILTER_TYPE_ALL(0,"전체"),
    FILTER_TYPE_LION(0,"사자"),
    FILTER_TYPE_TIGER(0,"호랑이"),
    FILTER_TYPE_GIRAFFE(0,"기린")
}

필터 사용을 위한 리스트 2개와 필터타입 셋팅

// MainActivity.kt
class MainActivity : AppCompatActivity() {

    // RecycerView를 구성하기 위한 리스트
    val recyclerViewList = mutableListOf<Animal>()
    // 현재 항목을 구성하기 위해 사용한 객체가 Util.animalList의 몇번째 객체인지를 담을 리스트
    val recyclerViewIndexList = mutableListOf<Int>()
    // 현재 선택되어 있는 필터 타입
    var filterType = FilterType.FILTER_TYPE_ALL
}

필터 초기값 셋팅

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)
        
        setLauncher()
        setToolbar()
        setView()
        setEvent()
    }

리사이클러뷰 어댑터 셋팅도 변경

        override fun getItemCount(): Int {
            return recyclerViewList.size
        }
        
        override fun onBindViewHolder(holder: ViewHolderMain, position: Int) {
            // position번째 객체를 추출
            val animal = recyclerViewList[position]
            
            ...
        }

onResume에서 필터 타입에 맞는 데이터 담아주기

    override fun onResume() {
        super.onResume()
        activityMainBinding.apply {
            // 필터 타입에 맞는 데이터를 담는다.
            setRecyclerViewList()
            
            // 리사이클러 뷰 갱신
            recyclerView.adapter?.notifyDataSetChanged()
        }
    }

setItems

다이얼로그 항목을 클릭 시 선택한 항목에 대한 필터 값을 설정해주기

   // 필터 다이얼로그를 띄우는 메서드
    fun showFilterDialog(){
        val dialogBuilder = MaterialAlertDialogBuilder(this@MainActivity)
        dialogBuilder.setTitle("필터 선택")
        
        // 항목
        val itemArray = arrayOf("전체","사자","호랑이","기린")
        dialogBuilder.setItems(itemArray){ dialogInterface: DialogInterface, i: Int ->
            // 리스너의 두 번째 매개변수(i)에는 사용자가 선택한 다이얼로그의 항목의 순서값이 전달된다.
            // 선택한 항목에 대한 필터 값을 설정해준다.
            filterType = when(i){
                0 -> FilterType.FILTER_TYPE_ALL
                1 -> FilterType.FILTER_TYPE_LION
                2 -> FilterType.FILTER_TYPE_TIGER
                3 -> FilterType.FILTER_TYPE_GIRAFFE
                else -> FilterType.FILTER_TYPE_ALL
            }
            // 데이터를 새로 담는다.
            setRecyclerViewList()
            // 리사이클러뷰를 갱신한다.
            activityMainBinding.recyclerView.adapter?.notifyDataSetChanged()
        }


        dialogBuilder.setNegativeButton("취소",null)
        dialogBuilder.show()
    }

리사이클러뷰 항목 선택시 intent에 담아줄 position을 원래의 animalList에서 몇번째였는지를 담아준다

            // 항목을 누르면 ShowActivity를 실행
            holder.rowMainBinding.root.setOnClickListener {
                val showIntent = Intent(this@MainActivity, ShowActivity::class.java)
                
                // showIntent.putExtra("position",position)
                // 사용자가 선택한 항목을 구성하기 위해 사용한 객체가
                // Util.animalList 리스트에 몇번째에 있는 값인지를 담아준다.
                showIntent.putExtra("position",recyclerViewIndexList[position])
                
                showActivityLauncher.launch(showIntent)
            }

다이얼로그 설정
검색 필터에 따라 리스트에 데이터를 담아주는 메서드

    // 검색 필터에 따라 리스트에 데이터를 담아주는 메서드
    fun setRecyclerViewList(){
        // 기본 다이얼로그용
        setRecyclerViewListBasic()
    }

    // 기본 다이얼로그용
    fun setRecyclerViewListBasic(){
        // 리스트 초기화
        recyclerViewList.clear()
        recyclerViewIndexList.clear()

        // 필터에 따라 분기한다.
        when(filterType){
            // 전체
            FilterType.FILTER_TYPE_ALL -> {
                // 모든 객체를 담아준다.
                var index = 0
                Util.animalList.forEach {
                    recyclerViewList.add(it)
                    recyclerViewIndexList.add(index)
                    index++
                }
            }
            // 사자
            FilterType.FILTER_TYPE_LION -> {
                // 사자만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_LION){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
            // 호랑이
            FilterType.FILTER_TYPE_TIGER -> {
                // 호랑이만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_TIGER){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
            // 기린
            FilterType.FILTER_TYPE_GIRAFFE -> {
                // 기린만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_GIRAFFE){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
        }
    }

setMultiChoiceItems


    // 현재 선택되어 있는 필터 타입 - MultiChoice
    var filterTypeMulti = booleanArrayOf(true, true, true)


    fun showFilterDialogMultiChoice(){
        val dialogBuilder = MaterialAlertDialogBuilder(this@MainActivity)
        dialogBuilder.setTitle("필터 선택")

        // 항목, 전체 선택이 가능하므로 전체 항목은 없애준다.
        val itemArray = arrayOf("사자","호랑이","기린")

        // 두 번째 : 체크상태가 변경된 항목의 순서값
        // 세 번째 : 체크 상태
        dialogBuilder.setMultiChoiceItems(itemArray,filterTypeMulti){ dialogInterface: DialogInterface, i: Int, b: Boolean ->
            // 체크가 변경된 항목 번째의 값을 변경한다.
            filterTypeMulti[i] = b
        }

        dialogBuilder.setNegativeButton("취소",null)
        dialogBuilder.setPositiveButton("확인"){ dialogInterface: DialogInterface, i: Int ->
            // 데이터를 새로 담는다.
            setRecyclerViewList()
            // 리사이클러뷰를 갱신한다.
            activityMainBinding.recyclerView.adapter?.notifyDataSetChanged()
        }
        dialogBuilder.show()
    }
    
	// 검색 필터에 따라 리스트에 데이터를 담아주는 메서드
    fun setRecyclerViewList(){
        // 기본 다이얼로그용
        // setRecyclerViewListBasic()
        // MultiChoice 다이얼로그용
        setRecyclerViewListMulti()
    }
    
    // MultiChoice 다이얼로그용
    fun setRecyclerViewListMulti() {
        // 리스트 초기화
        recyclerViewList.clear()
        recyclerViewIndexList.clear()

        // animalList에 담긴 객체의 수 만큼 반복한다.
        var index = 0
        Util.animalList.forEach {
            // 동물 타입이 사자이고 사자 필터가 true라면 담아준다.
            if(it.type == AnimalType.ANIMAL_TYPE_LION && filterTypeMulti[0] == true){
                recyclerViewList.add(it)
                recyclerViewIndexList.add(index)
            }
            // 동물 타입이 호랑이이고 호랑이 필터가 true라면 담아준다.
            if(it.type == AnimalType.ANIMAL_TYPE_TIGER && filterTypeMulti[1] == true){
                recyclerViewList.add(it)
                recyclerViewIndexList.add(index)
            }
            // 동물 타입이 기린이고 기린 필터가 true라면 담아준다.
            if(it.type == AnimalType.ANIMAL_TYPE_GIRAFFE && filterTypeMulti[2] == true){
                recyclerViewList.add(it)
                recyclerViewIndexList.add(index)
            }
            index++
        }
    }

setSingleChoiceItems


                setOnMenuItemClickListener {
                    when(it.itemId){
                        // 필터 메뉴
                        R.id.menu_item_main_filter -> {
                            // 필터 선택을 위한 다이얼로그를 띄운다.
                            // 기본 다이얼로그
                            // showFilterDialog()
                            // MultiChoice 다이얼로그
                            // showFilterDialogMultiChoice()
                            // SingleChoice 다이얼로그
                            showFilterDialogSingleChoice()
                        }
                    }
                    true
                }


    // 필터 다이얼로그를 띄우는 메서드 - singleChoice
    fun showFilterDialogSingleChoice(){
        val dialogBuilder = MaterialAlertDialogBuilder(this@MainActivity)
        dialogBuilder.setTitle("필터 선택")

        // 항목
        val itemArray = arrayOf("전체","사자","호랑이","기린")

        dialogBuilder.setSingleChoiceItems(itemArray, filterType.num){ dialogInterface: DialogInterface, i: Int ->
            // 리스너의 두 번째 매개변수(i)에는 사용자가 선택한 다이얼로그의 항목의 순서값이 전달된다.
            // 선택한 항목에 대한 필터 값을 설정해준다.
            filterType = when(i){
                0 -> FilterType.FILTER_TYPE_ALL
                1 -> FilterType.FILTER_TYPE_LION
                2 -> FilterType.FILTER_TYPE_TIGER
                3 -> FilterType.FILTER_TYPE_GIRAFFE
                else -> FilterType.FILTER_TYPE_ALL
            }
        }

        dialogBuilder.setNegativeButton("취소",null)
        dialogBuilder.setPositiveButton("확인"){ dialogInterface: DialogInterface, i: Int ->
            // 데이터를 새로 담는다.
            setRecyclerViewList()
            // 리사이클러뷰를 갱신한다.
            activityMainBinding.recyclerView.adapter?.notifyDataSetChanged()
        }
        dialogBuilder.show()
    }
    
    
    // 검색 필터에 따라 리스트에 데이터를 담아주는 메서드
    fun setRecyclerViewList(){
        // 기본 다이얼로그, SingleChoice 용
        setRecyclerViewListBasic()
        // MultiChoice 다이얼로그용
        // setRecyclerViewListMulti()
    }
    
    // 기본 다이얼로그용
    fun setRecyclerViewListBasic(){
        // 리스트 초기화
        recyclerViewList.clear()
        recyclerViewIndexList.clear()

        // 필터에 따라 분기한다.
        when(filterType){
            // 전체
            FilterType.FILTER_TYPE_ALL -> {
                // 모든 객체를 담아준다.
                var index = 0
                Util.animalList.forEach {
                    recyclerViewList.add(it)
                    recyclerViewIndexList.add(index)
                    index++
                }
            }
            // 사자
            FilterType.FILTER_TYPE_LION -> {
                // 사자만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_LION){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
            // 호랑이
            FilterType.FILTER_TYPE_TIGER -> {
                // 호랑이만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_TIGER){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
            // 기린
            FilterType.FILTER_TYPE_GIRAFFE -> {
                // 기린만 담아준다.
                var index = 0
                Util.animalList.forEach {
                    if(it.type==AnimalType.ANIMAL_TYPE_GIRAFFE){
                        recyclerViewList.add(it)
                        recyclerViewIndexList.add(index)
                    }
                    index++
                }
            }
        }
    }
    

setItems 메서드는 항목을 클릭하면 다이얼로그가 닫히지만
singleChoice와 multiChoice 메서드는 항목을 클릭해도 다이얼로그가 닫히지 않는다
따라서 확인 버튼을 눌렀을 때 데이터를 새로 담고 리사이클러뷰를 갱신해주는 작업을 해준다.

Parcelable 구현할 경우

부모 클래스인 Animal이 아닌 자식클래스에만 Parcelable 인터페이스를 구현했다.

writeToParcel 메서드 오버라이딩 시 부모클래스 값이 빠져있다.

class Lion() : Animal(), Parcelable {

    // 털의 개수
    var hairCount = 0
    // 성별
    var gender = LION_GENDER.LION_GENDER1

    constructor(parcel: Parcel) : this() {
        hairCount = parcel.readInt()
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(hairCount)
    }

따라서 부모클래스에 parcel객체에 부모클래스의 데이터를 담아줄 메서드를 추가해준다.

open class Animal {
    // 동물 타입
    var type = AnimalType.ANIMAL_TYPE_LION
    // 이름
    var name = ""
    // 나이
    var age = 0

    // parcel 객체에 데이터를 담는다.
    fun addToParcel(parcel:Parcel){
        parcel.writeValue(type)
        parcel.writeString(name)
        parcel.writeInt(age)
    }

    // parcel로부터 데이터를 추출하여 담아준다.
    fun getFromParcel(parcel: Parcel){
        type = parcel.readValue(AnimalType::class.java.classLoader) as AnimalType
        name = parcel.readString()!!
        age = parcel.readInt()
    }
}

각 동물 클래스에 해당 메서드를 추가해준다.
사자의 경우 gender값도 직접 추가해줘야 한다.

class Lion() : Animal(), Parcelable {

    // 털의 개수
    var hairCount = 0
    // 성별
    var gender = LION_GENDER.LION_GENDER1

    constructor(parcel: Parcel) : this() {
        getFromParcel(parcel)
        hairCount = parcel.readInt()
        gender = parcel.readValue(LION_GENDER::class.java.classLoader) as LION_GENDER
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        addToParcel(parcel)
        parcel.writeInt(hairCount)
        parcel.writeValue(gender)
    }
    
    
class Tiger() : Animal(), Parcelable {

    // 줄무늬 개수
    var lineCount = 0
    // 몸무게
    var weight = 0

    constructor(parcel: Parcel) : this() {
        getFromParcel(parcel)
        lineCount = parcel.readInt()
        weight = parcel.readInt()
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        addToParcel(parcel)
        parcel.writeInt(lineCount)
        parcel.writeInt(weight)
    }
    

class Giraffe() : Animal(), Parcelable {

    // 목의 길이
    var neckLength = 0
    // 달리는 속도
    var runSpeed = 0

    constructor(parcel: Parcel) : this() {
        getFromParcel(parcel)
        neckLength = parcel.readInt()
        runSpeed = parcel.readInt()
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        addToParcel(parcel)
        parcel.writeInt(neckLength)
        parcel.writeInt(runSpeed)
    }

이대로는 리스트에 담긴 객체값의 주소를 담은 변수의 타입이 Animal이고 Parcelable이 아니기 때문에 intent에 담을수가 없다
이부분을 해결할 방법은 강사님이 고민해보시고 내일 이어서 할 예정




※ 출처 : 멋쟁이사자 앱스쿨 2기, 소프트캠퍼스 
profile
안드로이드공부

0개의 댓글