[kotlin] 뷰바인딩,어댑터,ListView,GridView,CustomView,RecyclerView

남윤희·2023년 8월 22일
2

kotlin

목록 보기
1/25

뷰바인딩

뷰바인딩은 findViewById 대신에 쓰는 코드이고 대부분 findViewById대신 뷰바인딩을 쓴다고 한다.

NullSafe

  • 뷰 바인딩은 뷰의 Direct References 즉 직접 참조를 생성하므로 유효하지 않은 뷰ID로 인해 null포인터 예외(NPE)가 발생 할 위험이 없다.
  • 즉, 레이아웃에 아직 생성되지 않은 뷰의 참조를 얻어(null상태)해당 뷰의 속성을 사용하려 할 때 발생하는 NPE를 방지 한다는 것이다.
  • 레이아웃의 일부 구성에만 뷰가 있는 경우 결합 클래스에서 참조를 포함하는 필드가@Nullable로 표시된다.

2) Type safety

  • 각 바인딩 클래스에 있는 필드의 유형이 XML파일에서 참조하는 뷰와 일치한다.
  • 즉,클래스 변환 예외가 발생할 위험이 없다.
  • 쉽게말해 타입을 가지고 있기 때문에 imageView.text같이 타입이 다른 경우 발생하는 오류를 방지 할 수 있다.

위와 같은 이유로 뷰바인딩을 주로 쓴다.

프로젝트 첫 시작시 몇가지 설정만 해 놓으면 복잡한 코드 없이 간단히 id를 찾고 일을 수행하는게 편해진다.

먼저 gradle에서

//안드로이드 스튜디오 4.0이상에서는
android{
buildFeatures {
viewBinding true
}
}

//그 이하에서는
viewBinding{
enabled = true 
}

를 입력해 설정 해 주고

Activity에서

package com.example.myapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }
}

이렇게 설정 해 두면

binding.button.text = "안뇽"
        binding.button.setBackgroundColor(Color.BLACK)

이렇게 findViewById로 일일이 찾을필요없이 점 하나로 아이디에 접근이된다.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.myButton.setOnClickListener{
            binding.myTextView.text = "바인딩이 잘 되네요!"
        }
    }
}

이런식으로 setOnClickListener와 같은 다양한 일을 수행하는 것도 가능하다.

어댑터 뷰(Adapter View)

어댑터뷰

리스트뷰

그리드뷰

커스텀뷰

RecyclerView

어댑터뷰는

코드와 데이터 원본 중간역할이다.

어댑터 종류

1)BaseAdapter

  • 어댑터 클래스의 공통 구현
  • 사용자 정의 어댑터 구현 시 적용
  • 데이터 원본이 배열인 경우에 ArrayAdapter객체를 사용
    -- 필요 자료
  • context: 현재 컨텍스트
  • resource: 항목으로 표시될 텍스트 뷰의 리소스 ID

    레이아웃은 그냥 리스트뷰 하나만 넣어놓고
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val items = arrayOf<String?>("아이템1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6",  "item7", "아이템8")

        //어댑터 준비 (배열 객체 이용, simple_list_item_1 리소스 사용
        val adapter = ArrayAdapter(this, R.layout.simple_list_item_1, items)

        //어댑터 연결
        binding.listView.adapter = adapter
    }
}

이렇게 간단히 테스트로 값을 넣어보면

데이터를 잘 적용이 된 것을 볼 수 있다.

그리드 뷰(GridView)


Gridview는 사진이나 두줄 이상의 스크롤 가능한 그리드에 항목을 표시한다.

--layout

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gridview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnWidth="100dp"
    android:numColumns="auto_fit"
    android:verticalSpacing="10dp"
    android:horizontalSpacing="10dp"
    android:stretchMode="columnWidth"
    android:gravity="center"
    //
    android:columnWidth=“100dp”** : 그리드 항목 하나의 폭을 100dp로 설정
    android:numColumns=“auto_fit”**: 열의 폭과 화면 폭을 바탕으로 자동 계산
    android:verticalSpacing**: 항목 간의 간격 설정
    android:stretchMode=“columnWidth”**: 열 내부의 여백을 폭에 맞게 채움
    />

kt파일

package com.example.myapplication

import android.R
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.Toast
import com.example.myapplication.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val items = arrayOf<String?>("item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6", "item7", "item8", "item5", "item6",  "item7", "item8")

        //어댑터 준비 (배열 객체 이용, simple_list_item_1 리소스 사용
        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)

        // 어댑터를 GridView 객체에 연결
        binding.gridView.adapter = adapter

    }
}


마찬가지로 잘 출력이 된다.

커스텀뷰(Custom View)

위와같이 이미지와 문자열이나 이미지를포함하는 위젯을 항목에 넣을 수 있다.

  • 커스텀 항목뷰 설정 절차
  1. 커스텀 항목을 위한 XML 레이아웃 정의
    res/layout 하위에 item.xml 이라는 새로운 xml 파일을 생성해 만들고자 하는 레이아웃을 만든다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    >
    <ImageView
        android:id="@+id/iconItem"
        android:layout_width="@dimen/icon_size"
        android:layout_height="@dimen/icon_size"
        android:scaleType="centerCrop"
        android:padding="@dimen/icon_padding"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:src="@drawable/sample_0"
        />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="2">
        <TextView
            android:id="@+id/textItem1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:textSize="@dimen/list_item_text_size1"
            android:padding="@dimen/list_item_padding"
            android:hint="Name"
            />
        <TextView
            android:id="@+id/textItem2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:textSize="@dimen/list_item_text_size2"
            android:padding="@dimen/list_item_padding"
            android:hint="Age"
            />
    </LinearLayout>

</LinearLayout>
  1. 항목 관련 데이터 클래스 정의
    데이터를 정의한 MyItem 클래스를 생성한다.
data class MyItem(val aIcon:Int, val aName:String, val aAge:String) {}

data클래스는
equals(), hashCode(), toString(), copy(), componentN() 5가지 유용한 함수들을 내부적으로 자동으로 생성한다.

  1. 어댑터 클래스 정의
    앞서 정의한 MyItem 타입의 객체들을 ArrayList로 관리하는 MyAdapter 클래스 BaseAdapter를 만든다.
    MyAdapter 클래스는 getCount(), getItem(), getItemID(), getView() method를 재 정의해야 함.
package com.android.customitemview

class MyAdapter(val mContext: Context, val mItems: MutableList<MyItem>) : BaseAdapter() {

    // MyAdapter 클래스가 관리하는 항목의 총 개수를 반환
    override fun getCount(): Int {
        return mItems.size
    }

    // MyAdapter 클래스가 관리하는 항목의 중에서 position 위치의 항목을 반환
    override fun getItem(position: Int): Any {
        return mItems[position]
    }

    // 항목 id를 항목의 위치로 간주함
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // position 위치의 항목에 해당되는 항목뷰를 반환하는 것이 목적임
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        var convertView = convertView
        if (convertView == null) convertView = LayoutInflater.from(parent?.context).inflate(R.layout.item, parent, false)

        val item : MyItem = mItems[position]

        // convertView 변수로 참조되는 항목 뷰 객체내에 포함된 객체를 id를 통해 얻어옴
        val iconImageView = convertView?.findViewById<View>(R.id.iconItem) as ImageView
        val tv_name = convertView.findViewById<View>(R.id.textItem1) as TextView
        val tv_age = convertView.findViewById<View>(R.id.textItem2) as TextView

        // 어댑터가 관리하는 항목 데이터 중에서 position 위치의 항목의 객체를 헤딩 힝목에 설정
        iconImageView.setImageResource(item.aIcon)
        tv_name.text = item.aName
        tv_age.text = item.aAge

        return convertView
    }
}
  1. 메인화면 레이아웃에 ListView 위젯 정의
    메인 화면 레이아웃에 ListView위젯을 추가
    XML레이아웃 파일에 정의된 ListView위젯을 Kotlin코드에서 참조하기 위하여 id속성을정의한다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       />
</LinearLayout>
  1. 어댑터를 생성하고 어댑터뷰 객체에 연결

MainActivity.kt 파일에서 어댑터 객체에서 관리할 항목 데이터의 ArrayList 객체를 준비하고
MyAdapter 객체를 생성하고 초기화 한다.
생성된 MyAdapter 객체를 어댑터뷰인 리스트뷰에 연결한다.

package com.android.customitemview

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val dataList = mutableListOf<MyItem>()
        dataList.add(MyItem(R.drawable.sample_0, "Bella", "1"))
        dataList.add(MyItem(R.drawable.sample_1, "Charlie", "2"))
        dataList.add(MyItem(R.drawable.sample_2, "Daisy", "1.5"))
        dataList.add(MyItem(R.drawable.sample_3, "Duke", "1"))
        dataList.add(MyItem(R.drawable.sample_4, "Max", "2"))
        dataList.add(MyItem(R.drawable.sample_5, "Happy", "4"))
        dataList.add(MyItem(R.drawable.sample_6, "Luna", "3"))
        dataList.add(MyItem(R.drawable.sample_7, "Bob", "2"))


        // 어댑터 생성 및 연결
        binding.listView.adapter = MyAdapter(this, dataList)

        // 항목 클릭 이벤트 처리
        binding.listView.setOnItemClickListener{ parent, view, position, id ->
            val name: String = (binding.listView.adapter.getItem(position) as MyItem).aName
            Toast.makeText(this," $name 선택!", Toast.LENGTH_SHORT).show()
        }
    }
}


실행화면

✨RecyclerView

가장 중요하다는 RecyclerView는 View를 계속 생성하는 것이 아닌 재활용해서 사용하는 View이다.
그렇기 때문에 다른 View와 달리 Adapter뿐 아니라 저장할 수 있는 ViewHolder가 필요하다.
동일한 예제에서 메인 레이아웃과 Adapter.kt 파일, Main.kt파일만 수정하면 된다.
레이아웃은 ListView위젯을 RecyclerView위젯으로 변경한다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       />
</LinearLayout>

어댑터 클래스는

class MyAdapter(val mItems: MutableList<MyItem>) : RecyclerView.Adapter<MyAdapter.Holder>() {

    interface ItemClick {
        fun onClick(view : View, position : Int)
    }

    var itemClick : ItemClick? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        holder.itemView.setOnClickListener {  //클릭이벤트추가부분
            itemClick?.onClick(it, position)
        }
        holder.iconImageView.setImageResource(mItems[position].aIcon)
        holder.name.text = mItems[position].aName
        holder.age.text = mItems[position].aAge
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

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

    inner class Holder(val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
        val iconImageView = binding.iconItem
        val name = binding.textItem1
        val age = binding.textItem2
    }
}

메인은

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 데이터 원본 준비
        val dataList = mutableListOf<MyItem>()
        dataList.add(MyItem(R.drawable.sample_0, "Bella", "1"))
        dataList.add(MyItem(R.drawable.sample_1, "Charlie", "2"))
        dataList.add(MyItem(R.drawable.sample_2, "Daisy", "1.5"))
        dataList.add(MyItem(R.drawable.sample_3, "Duke", "1"))
        dataList.add(MyItem(R.drawable.sample_4, "Max", "2"))
        dataList.add(MyItem(R.drawable.sample_5, "Happy", "4"))
        dataList.add(MyItem(R.drawable.sample_6, "Luna", "3"))
        dataList.add(MyItem(R.drawable.sample_7, "Bob", "2"))

        binding.recyclerView.adapter = MyAdapter(dataList)

        val adapter = MyAdapter(dataList)
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)

        adapter.itemClick = object : MyAdapter.ItemClick {
            override fun onClick(view: View, position: Int) {
                val name: String = dataList[position].aName
                Toast.makeText(this@MainActivity," $name 선택!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}


생긴건 거의 동일하지만, 기능은 꽤 다른 리사이클뷰가 완성이됐다.

interface는 setOnClickListner처럼 내가 커스텀해서 쓸 수 있는것
내가 interface 어쩌고 하고 뒤에 기능을 쓰면
어쩌고는 앞으로 이기능을 쓰는거야 라는것
object는 원래 상속받는 class를 써야하는데 임의로 object라는 클래스에 넣어놓을게 라는뜻
예시 -

        adapter.itemClick = object : MyAdapter.ItemClick {
            override fun onClick(view: View, position: Int) {
               val name: String = dataList[position].aName
                Toast.makeText(this@MainActivity, "선택!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}
class Test : MyAdapter.ItemClick{
    override fun onClick(view: View, position: Int) {
        TODO("Not yet implemented")
    }

}
//

itemClick : ItemClick? = null 은 무얼 의미하는지
ItemClick타입의 변수를 만들었고 일단 ? = null 할당하겠다.
nullable

parent: ViewGroup, viewType: Int
매개변수prrent와 ViewGroup은 클래스지만 자료View상위

inflate(LayoutInflater.from(parent.context), parent, false)

override fun getItemId(position: Int): Long {
return position.toLong()
getItemId가 Long값을 받기로 약속

inflate - xml이랑 연결돼있는 객체. 바인딩 생성 시 inflate bind함수와 inflate

object 인터페이스에 대한 익명의 이름을 지정. 원래 클래스를 상속받아야 하는데 object

getItemId 해당값의 고유값을 알고 싶을 때

profile
안드로이드 주니어 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 22일

고생 많으셨어요
앞으로 2주가 될지, 3주가 될지 모르겠지만
잘 부탁드립니다 형님

답글 달기