[Android] 5(2). 컨테이너 (목록 만들기)

7 minute read


컨테이너 (목록 만들기)

위젯이나 다른 레이아웃에 데이터를 동적으로, 반복적으로 표현할 때는 컨테이너를 사용합니다.

대표적인 컨테이너로 목록을 화면에 출력할 때 사용하는 ‘리사이클러 뷰’가 있습니다.

image-20210801223738686

리사이클러 뷰의 코드는 조금 어렵기 때문에 리사이클러 뷰의 축소 버전인 ‘스피너’를 먼저 살펴봅니다.

컨테이너는 레이아웃과는 다르게 내부 요소의 위치를 결정할 수 있는 속성이 없으므로 컨테이너를 사용할 때는 다른 레이아웃을 컨테이너 안에 삽입해서 사용합니다.


스피너


  • 스피너는 여러 개의 목록 중에서 하나를 선택할 수 있는 선택 도구입니다.
  • 스피너는 어댑터라는 연결 도구를 사용해 화면에 보여주는 스피너를 연결합니다. 여러 개의 데이터가 어댑터에 입력되면 1개의 데이터 당 1개의 아이템 레이아웃이 생성되어 화면에 목록 형태로 나타납니다.

스피너로 보는 어댑터의 동작 구조

1. 스피너에 입력될 가상의 데이터 작성

var data = listOf("-- 선택하세요 --", "1월", "2월", "3월", "4월", "5월", "6월")


2. 데이터와 스피너를 연결해줄 어댑터 생성

어댑터의 데이터 타입은 제네릭으로 지정해줍니다.

var adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)

ArrayAdapter의 파라미터

  • 파라미터 1: 컨텍스트 객체
  • 파라미터 2: 스피너의 목록 하나하나가 그려질 레이아웃. 위에서 지정한 레이아웃은 안드로이드 기본 제공 레이아웃.
  • 파라미터 3: 연결할 데이터


3. 어댑터를 스피너에 연결

binding.spinner.adapter = adapter

스피너의 어댑터 프로퍼티에 연결할 어댑터 인스턴스 저장. \


4. 스피너 리스너 연결

binding.spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
 	... 
}


5. 리스너 메서드 구현

스피너 리스너 스코프 내에서 [Ctrl + I]로 구현할 메서드 2개를 불러옵니다.

binding.spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
    override fun onItemSelected(
        parent: AdapterView<*>?,
        view: View?,
        position: Int, // 선택된 아이템 순서
        id: Long
    ) {
        // 대부분 세번째 파라미터인 position만 사용
        // 데이터는 스피너에서 가져오는 것이 아니라 전달한 데이터에서 직접 가져옴
        binding.result.text = data.get(position)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {

    }

onItemSelected 메서드의 경우 대부분 세번째 파라미터인 position만 사용하며, 데이터를 가져올 때는 스피너에서 가져오는 것이 아닌 연결한 데이터에서 직접 가져옵니다.


전체 코드

package kr.co.hanbit.containerspinner

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import kr.co.hanbit.containerspinner.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}

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

        // 스피너에 입력될 가상의 데이터 작성
        var data = listOf("-- 선택하세요 --", "1월", "2월", "3월", "4월", "5월", "6월")

        // 데이터와 스피너를 연결해줄 어댑터 생성
        // 데이터 타입은 제네릭으로 지정.
        // 파라미터: 컨텍스트, 스피너의 목록 하나하나가 그려질 레이아웃, 데이터
        var adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)

        // 어댑터를 스피너에 연결. 스피너의 어댑터 프로퍼티에 연결할 어댑터 인스턴스 저장.
        binding.spinner.adapter = adapter

        // 스피너 리스너 연결
        binding.spinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener{
            override fun onItemSelected(
                parent: AdapterView<*>?,
                view: View?,
                position: Int, // 선택된 아이템 순서
                id: Long
            ) {
                // 대부분 세번째 파라미터인 position만 사용
                // 데이터는 스피너에서 가져오는 것이 아니라 전달한 데이터에서 직접 가져옴
                binding.result.text = data.get(position)
            }

            override fun onNothingSelected(parent: AdapterView<*>?) {

            }
        }

    }
}



리사이클러 뷰


리사이클러 뷰는 스퍼너의 확장된 형태로, 간단한 코드만으로 목록의 형태를 바꿀 수 있습니다.

컨테이너들의 목록 모양은 데이터와 레이아웃을 연결해주는 어댑터에서 어떤 아이템 레이아웃을 사용하느냐에 따라 결정됩니다.

이제 리사이클러 뷰의 사용 과정에 대해 살펴봅시다.


화면 구상하기

**1. activity_main.xml 파일에 [Containers] 카테고리에서 RecyclerView를 가져다 놓습니다. **

2. 아이템 레이아웃 생성하기

리사이클러 뷰의 아이템처럼 여러 개의 정보를 하나의 아이템에 보여줘야 할 때는 아이템 레이아웃을 레이아웃 파일로 직접 생성하여 사용합니다.

[app] - [res] - [layout] 디렉터리에서 [New] - [Layout Resource File]을 선택합니다.

‘File name’에는 ‘item_recycler’, ‘Root element’에는 ‘LineraLayout’ 과 같이 입력하여 파일을 생성합니다.

리니어 레이아웃의 속성 중 layout_height는 50dp, orientation은 horizontal, gravity는 center_vertical 로 지정합니다. 이 속성값들은 전형적인 값들이지 개발자의 의도에 따라 얼마든지 바꿀 수 있습니다.

레이아웃 안에 하나의 아이템에서 보여주고 싶은 정보의 개수만큼 텍스트뷰(또는 다른 위젯)를 담은 후 각 텍스트뷰의 layout_width 를0dp로 설정하고 layout_weight을 설정하여 각 정보를 보여줄 비율을 결정합니다.

image-20210802120335656


데이터 정의하기

3. 아이템 레이아웃에 담길 데이터 클래스 정의

이제 아이템 레이아웃에 담길 정보들을 하나로 묶어 관리할 데이터 클래스를 정의합니다.

[java] - 기본 패키지명 우클릭 - [New] - [Kotlin Class/File]을 클릭하고 입력 필드에는 클래스명을 입력한 후 Data Class를 생성합니다. (여기서는 클래스명을 Memo라고 하겠습니다.)

생성한 클래스에 아이템 당 보여줄 정보의 수만큼 파라미터를 정의합니다.

data class Memo(var no: Int, var title: String, var timeStamp: Long){

}

✋ 날짜나 시간 정보를 보여주는 데이터를 정의할 때는 변수는 timeStamp로, 데이터 타입은 Long으로 한 이후에 뒤에서 날짜 형식으로 변환해줍니다.


어댑터와 뷰홀더 정의하기

리사이클러 뷰는 RecyclerView.Adapter 클래스를 상속하여 만든 후 별도의 메서드 어댑터를 사용해서 데이터를 연결합니다.

리사이클러뷰어댑터는 개별 데이터에 대응하는 뷰홀더 클래스를 제네릭으로 지정해야 합니다. 따라서 **뷰홀더 클래스를 먼저 만들고 나서 어댑터 클래스를 만드는 것이 더 편합니다. **

class 커스텀어댑터: RecyclerView.Adapter<여기에 사용할 뷰홀더 지정>{
  
}

뷰 홀더 클래스도 이미 만들어져 있는 RecyclerView.ViewHolder 클래스를 상속받아서 만듭니다. 뷰 홀더는 현재 화면에 보여지는 개수만큼만 아이템 레이아웃이 생성되고 목록이 스크롤 될 경우 사라지는 아이템 위젯이 보이는 아이템 위젯을 대체하여 데이터만 바꿔주기 때문에 앱의 효율이 향상됩니다.

ViewHolder 클래스의 생성자에는 다음에 만들 어댑터의 아이템 레이아웃을 넘겨줘야 하므로 Holder 클래스를 생성할 때 생성자에게서 레이아웃의 바인딩을 넘겨받아야 합니다.

class 커스텀뷰홀더(바인딩): RecyclerView.ViewHolder(바인딩.root)


4. 어댑터 클래스와 뷰홀더 클래스 선언하기

이번에는 [java] - 기본 패키지명 우클릭 - [New] - [Kotlin Class/File]을 클릭하고 입력 필드에는 클래스명을 입력한 후 Class를 생성합니다. (여기서는 클래스명을 CustomAdapter라고 하겠습니다.)

CustomAdapter 클래스 아래에 먼저 뷰홀더 클래스를 하나 정의합니다. (여기서는 클래스명을 Holder라고 하겠습니다. )

class CustomAdapter: RecyclerView.Adapter<Holder>{
  
}

class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
  
}

✋ binding은 Holder 클래스 안에서 전역변수로 사용해야 하기 때문에 val 키워드를 붙여줍니다.

바인딩 생성은 어댑터에서

뷰 홀더가 사용하는 바인딩은 어댑터에서 생성한 후에 넘겨줍니다. 앞에서 생성한 아이템 레이아웃 파일명이 ‘item_recycler’이기 때문에 안드로이드에서 생성해주는 바인딩의 이름은 ‘ItemRecyclerBinding’이 됩니다.


코드가 위의 모양처럼 완성되었으면 이제 리사이클러뷰어댑터의 인터페이스를 구현합니다.


5. 어댑터 클래스 정의하기

CustomAdapter 스코프 내에서 [Ctrl + I]를 눌러 3개의 메서드들을 모두 구현합니다.

  • 어댑터에서 사용할 데이터 목록 변수 선언
var listData = mutableListOf<Memo>()

앞에서 만든 데이터 클래스를 제네릭으로 지정해주어 리스트를 생성합니다. 데이터는 액티비티에서 직접 호출하여 값을 넣습니다.

어댑터에서 사용할 데이터의 데이터 타입과 액티비티에서 전달해줄 데이터의 데이터 타입은 일치해야 합니다.

  • getItemCount( ) 메서드
// 목록에 보여줄 아이템 개수
override fun getItemCount(): Int {
   return listData.size
}

리사이클러 뷰에서 사용할 데이터의 총 개수를 리턴하는 메서드입니다.

  • onCreateViewHolder( ) 메서드
// 스마트폰의 한 화면에 그려지는 아이템 개수만큼 아이템 레이아웃 생성
// 안드로이드는 뷰홀더(Holder) 클래스를 메모리에 저장했다가 요청이 있을 때마다 메서드를 실행하여 꺼내서 사용한다.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
    // 어댑터에서 사용하는 바인딩
    // 파라미터: LayoutInflater.from(parent.content), parent, false
    val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context),
                                                parent, false)
    return Holder(binding)
}

아이템 레이아웃을 바인딩하고, 생성한 binding 변수를 전달하면서 Holder 클래스의 생성자를 호출하여 하나의 아이템이 담기는 홀더를 생성합니다. 스마트폰의 한 화면에 보이는 개수만큼 이 함수를 호출하여 각각의 홀더를 생성합니다.

inflate(inflater, parent, attachToRoot) 파라미터의 의미

  • inflater: 바인딩을 생성할 때 사용하는 인플레이터입니다. 액티비티에서와는 다르게 LayoutInflater.from(parent.context) 를 지정해주면 됩니다.
  • parent: 생성되는 바인딩이 속하는 부모 뷰(레이아웃). parent로 지정하면 됩니다.
  • attachToRoot: true일 경우 attach해야 하는 대상으로 root를 지정하고 아래에 붙입니다. false일 경우 뷰의 최상위 레이아웃의 속성을 기본으로 레이아웃이 적용됩니다. 리사이클러뷰에 있어서는 항상 false로 지정합니다.


  • onBindViewHolder( ) 메서드
// 생성된 아이템 레이아웃에 값 입력 후 목록에 출력 (생성된 뷰홀더를 출력)
override fun onBindViewHolder(holder: Holder, position: Int) {
    val memo = listData.get(position)
    holder.setMemo(memo) // 바로 이어서 구현합니다. 
}

홀더에 값을 할당하고 목록에 출력합니다.


6. 뷰 홀더 클래스 정의하기

뷰 홀더 클래스에는 각 아이템마다 데이터를 세팅할 메서드가 필요합니다.

class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
    // 아이템에 데이터를 세팅하는 메서드 구현
    fun setMemo(memo: Memo){
        binding.textNo.text = "${memo.no}"

        binding.textTitle.text = memo.title

        // java.text의 SimpleDataFormat을 사용하여 날짜형식으로 변환
        var sdf = SimpleDateFormat("yyyy/MM/dd")
        // 날짜 형식을 사용하고 싶을 때는 Long 형태의 데이터 타입을
        // SimpleDataFormat 인스턴스의 format 메서드로 변환한다.
        var formattedDate = sdf.format(memo.timeStamp)
        binding.textDate.text = formattedDate
    }
}

SimpleDataFormat

날짜 형식을 우리가 원하는 문자열 형태로 변환해주는 도구입니다.

형식 의미 형식 의미
yyyy 연도 4자리 h 시간
MM m
d s


액티비티에서 어댑터 사용하기

이제 지금까지 생성한 레이아웃과 소스 코드를 모두 연결합니다.

7. 액티비티에 데이터 전달 메서드 만들기

이제 리사이클러 뷰의 각 아이템에 데이터를 로드할 메서드를 정의합니다. 반환 값은 데이터 리스트입니다.

메서드의 위치는 클래스 내부입니다.

    // 목록의 아이템 클래스의 리스트를 반환하는 함수
    fun loadData(): MutableList<Memo>{
        // 리턴할 MutableList 컬렉션
        val data: MutableList<Memo> = mutableListOf()
        // 100개의 가상 데이터 만들기
        for (no in 1..100){
            val title = "이것이 안드로이드다 ${no}"
            val date = System.currentTimeMillis()
            // Memo 인스턴스 생성 후 반환값에 값 추가
            var memo = Memo(no, title, date)
            data.add(memo)
        }

        return data
    }

8. 사용할 데이터를 어댑터의 listData 변수에 저장

val data: MutableList<Memo> = loadData()

var adapter = CustomAdapter()
adapter.listData = data

앞에서 만들어둔 데이터 로드 메서드를 사용하여 데이터를 가져온 후 어댑터의 데이터에 저장합니다.


9. 리사이클러 뷰에 어댑터와 레이아웃 매니저 연결

binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)


**10. 리사이클러뷰가 완성됩니다. **

image-20210802130519503



레이아웃 매니저의 종류

리사이클러 뷰에서 사용할 수 있는 레이아웃 매니저의 종류는 세 가지입니다.

image-20210802142404662

  1. LinearLayoutManager

    • 세로 스크롤: 기본으로 세로 스크롤을 하며 한 줄로 목록을 생성합니다. 생성자에는 컨텍스트 1개만 전달합니다.

        LinearLayoutManager(this)
      
    • 가로 스크롤: 열의 개수를 지정해서 개수만큼 그리드 형태로 목록을 생성합니다. 리니어 레이아웃 매니저의 두번째 파라미터에 가로 스크롤 옵션을 설정합니다.

        LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
      
  2. GridLayoutManager

    • 데이터의 사이즈에 따라 그리드의 크기가 결정됩니다. 두번째 파라미터에 한 줄에 몇 개의 아이템을 표시할 것인지 개수를 설정합니다.

        GridLayoutManager(this, 3)
      
  3. StaggeredGridLayoutManager

    • 세로 스크롤: 컨텍스트를 사용하지 않으므로 this를 넘기지 않습니다. 첫번째 파라미터에는 한 줄에 표시되는 아이템의 개수, 두번째 파라미터에는 세로 방향을 설정합니다.

      ```kotlin StaggeredFridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)

    • 가로 스크롤: 두번째 파라미터에 가로 방향을 설정합니다.

        StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.HORIZONTAL)
      


목록 클릭 이벤트 처리

홀더가 가지고 있는 아이템뷰에 클릭리스터를 달고, 리스너 블록에 실행할 코드만 추가하면 됩니다.

여기서는 간단하게 아이템 선택 시 제목을 화면에 토스트로 띄우는 코드를 작성해봅니다.

class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
    // init 초기화 블록
    init{
        binding.root.setOnClickListener { 
            Toast.makeText(binding.root.context, "클릭된 아이템 = ${binding.textTitle.text}", 
                            Toast.LENGTH_SHORT).show()       
        }
    }
    ... 
}

목록 -> 상세로 화면 이동이 일어날 경우는 클릭리스너 안에서 startActivity를 호출하는 형태로 처리할 수 있습니다.



정리


  • 컨테이너 위젯을 사용할 때에는 어댑터라는 연결 도구를 사용합니다.

  • 스피너는 여러 개의 목록 중에서 하나를 선택할 수 있는 선택 도구입니다.

    • 스피너에 입력될 데이터 작성 ➡ 데이터와 스피너를 연결해줄 어댑터 생성 ➡ 어댑터를 스피너에 연결 ➡ 스피너 리스터 연결 ➡ 리스터 메서드 구현
  • 리사이클러 뷰는 스피너의 확장된 형태로, 목록의 형태를 다양하게 바꿀 수 있는 컨테이너 위젯입니다.

    • 아이템 레이아웃 생성 ➡ 아이템 레이아웃에 담길 데이터 클래스 정의 ➡ 어댑터와 뷰홀더 선언 및 정의 ➡ 액티비티에서 데이터 생성 후 어댑터 데이터에 저장 ➡ 리사이클러 뷰에 어댑터와 레이아웃 매니저 연결
  • 레이아웃 매니저에는 LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager가 있습니다.

Categories:

Updated:

Leave a comment