본문 바로가기
프로그래밍

Android 일반 TableView - 데이터 표시를 위한 전체 사용자 지정 가능 라이브러리

by it-view 2022. 1. 12.
반응형

몇 달 전에 저는 제가 만든 맞춤형 AccordionLayout 구성 요소를 설명하면서 어떻게 복사하는 것을 멈추고 재사용 가능한 구성 요소를 사랑하게 되었는지에 대해 말씀드리고 있었습니다. 오늘은 유용한 라이브러리를 하나 더 살펴봄으로써 재사용 가능한 구성요소에 대한 주제를 다시 살펴보려고 합니다.

제가 마이클 볼튼의 이야기를 들려드릴게요. 마이클은 이니텍이라는 회사에서 소프트웨어 개발자로 일하고 있습니다. 그는 지난 몇 년 동안 안드로이드 애플리케이션을 개발해왔고, 최근에는 애플리케이션에 관리 옵션을 추가하기로 결정했습니다. 당연히, 그들은 마이클에게 그 일을 맡겼습니다.

간단한 요청이었습니다. 직원과 해당 고객의 목록을 가져오면 데이터를 제시해야 합니다. 예제 CSV 파일에는 ID, 이름 및 성의 세 가지 정보가 포함되어 있습니다.

또한 고객 목록은 ID, 회사 이름, 전화 번호, 이 회사에 할당된 직원의 ID 및 국가 코드와 같이 매우 간단해 보였습니다.

 

분명히 마이클은 이 간단한 작업에 RecyclerView를 사용하기로 결정했다. 그래서 그는 모델을 만들고 뷰로 묶었다.홀더*:

data class Employee (val id: Int, val firstName: String, val lastName: String)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    >
          <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/employee_id" />
              <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"           
        android:layout_weight="1"
        android:id="@+id/first_name" />

              <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"        
        android:layout_weight="1"
        android:id="@+id/last_name" />

          </LinearLayout>
class EmployeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
      private val id: TextView = itemView.findViewById(R.id.employee_id)
      private val firstName: TextView = itemView.findViewById(R.id.first_name)
      private val lastName: TextView = itemView.findViewById(R.id.last_name)

      fun bindView(employee: Employee){
                id.text = employee.id.toString()
                firstName.text = employee.firstName
                lastName.text = employee.lastName
                itemView.setOnClickListener{
                              //todo open the clients view
                }
      }
}

아직까진 좋아. 이제 마이클은 고객 목록에 대해서도 똑같이 해야 했습니다. 약간 반복적이긴 하지만 그래도 큰 문제는 아니야. 그래서 그는 새로운 모델을 만들고 뷰를 재사용할 수 있을 것이라고 생각했습니다.Holder. 그러나 Client 모델에 표시해야 하는 추가 필드가 있습니다. 문제가 있습니다.

data class Client (val id: Int, val companyName: String, val phone: String, val assignedTo: Int, countryCode: Int)

마이클은 갈림길에 서 있었다. 새 어댑터 및 뷰를 생성합니까?이러한 목적을 위한 홀더 또는 일부 구성 요소를 재사용하려고 시도합니까? 그는 이미 가지고 있던 것을 조금씩 개조하여 높은 길을 걷는다. 따라서 새 TextView를 XML에 추가하여 클라이언트의 국가 코드를 표시하는 데 사용되지만 직원에게는 숨겨지고 필드가 사용되는 새로운 방식을 나타내기 위해 이름을 리팩터링합니다. 썩 좋진 않지만, 쓸모가 있어요.

 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    >
          <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"      
        android:layout_weight="1"
        android:id="@+id/column1" />
              <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/column2" />

              <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"    
        android:layout_weight="1"
        android:id="@+id/column3" />
              <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/column4" />
          </LinearLayout>
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
      private val column1: TextView = itemView.findViewById(R.id.column1)
      private val column2: TextView = itemView.findViewById(R.id.column2)
      private val column3: TextView = itemView.findViewById(R.id.column3)
      private val column4: TextView = itemView.findViewById(R.id.column4)
      fun bindView(employee: Employee){
                column1.text = employee.id.toString()
                column2.text = employee.firstName
                column3.text = employee.lastName
                column4.visibility = View.GONE
                itemView.setOnClickListener{
                              //todo open the clients view
                }
      }
      fun bindView(client: Client){
                column1.text = client.id.toString()
                column2.text = client.companyName
                column3.text = employee.phone
                column4.text = employee.countryCode
                column4.visibility = View.VISIBLE
      }
}

그래서 이 구현이 포함된 새로운 버전이 출시되었고, 상사들은 이제 직원들의 성과에 대한 통찰력을 갖게 되어 기쁩니다. 하지만 그들은 더 많이 원한다. 그래서 그들은 마이클이 처리해야 할 데이터에 더 많은 정보를 추가해요. 그들은 이제 직원이 정규직인지 아닌지를 확인하고 앱에서 직접 상태를 관리하려고 합니다. 이니텍의 상사들은 실적(이 시나리오에서는 더 많은 고객을 의미한다)에만 관심을 두기 때문에 실적이 충분하지 않다고 느낄 경우 회사에서 직원을 해고할 수 있는 옵션도 원합니다.

마이클은 이 요청이 마음에 들지 않지만, 직원 명단에서 제외되지 않으려면 그렇게 해야 합니다. 하지만 그는 처음 구현한 것으로 돌아가면 이 요청을 더 싫어할 것입니다. 새 열? 전체 시간 상태에 대한 옵션을 전환하시겠습니까? 단추 제거? 이 모든 것이 이전 구현과 어떤 면에서 부합합니까? 그는 더 잘 알았어야 했다. 숙련된 소프트웨어 엔지니어라면 누구나 고객이 변경을 요청한다는 것을 알아야 한다. 그리고 자주. 마이클은 어떻게 해야 하나요? 일을 더 복잡하게 만들어서 그가 이미 가지고 있는 코드를 이용하려고 노력하세요? 아니면 마이클이 상사의 요청을 이행할 수 있는 더 나은 방법을 찾거나요?

우리 모두는 정답을 알고 있지만, 우리 중 너무나 많은 사람들이 더 쉬워 보이는 방법을 선택한다. 그러나 마이클이 지금 코드를 중지하고 리팩터링하지 않는다면, 그는 요청이 바뀌었을 때 (다시) 더 큰 문제에 직면하게 될 것이다. 그럼 이 코드를 재사용할 수 있게 하려면 어떻게 해야 할까요? 다음 요청마다 어떻게 처리해야 할까요? 다음 요청이 무엇일지 정확히 알 수는 없지만, 그래도 계획을 세울 수는 있습니다. 코드 조각을 서로 다른 구성 요소로 분리하여 확장할 수 있으므로 많은 문제를 줄일 수 있습니다.

마이클에게 필요한 것은 데이터 프레젠테이션을 쉽게 변경할 수 있는 완전한 사용자 정의, 확장 및 독립 모듈입니다. 이제 그는 이것을 처음부터 개발 할 수 있지만, 좋은 소식은, 제가 이미 개발했다는 것입니다. Generic TableView는 사용자가 미리 정의된 구성 요소를 사용하여 데이터를 다용도로 표시하거나, 단순히 데이터를 확장하여 필요에 맞게 사용자 정의할 수 있도록 개발되었습니다.

 

모든 항목은 테이블에 표시하려는 모든 클래스로 확장되는 GenericListElement로 시작합니다. 클래스는 필수 매개 변수 1개와 옵션 매개 변수 3개를 사용합니다.

ColumnMap은 GenericView 구현의 맵이며 이 열을 표시할지 여부를 나타내는 부울 값입니다. 이 매개 변수는 필수입니다. GenericView는 인스턴스화하기 위해 구현되어야 하는 추상 클래스입니다. 제공되는 몇 가지 구현이 있지만 언제든지 고유한 사용자 정의 보기를 생성하여 사용할 수 있습니다.

이 유형은 제공할 수 있지만 필수 사항은 아닌 선택적 매개 변수입니다. 이것은 행의 작업 유형 또는 보기에 대한 작업을 수행하는 데 일반적으로 사용되는 맨 오른쪽 요소를 나타냅니다. 몇 가지 미리 정의된 RowType(ButtonRow)이 있습니다.유형, ChevronRow유형, 포지티브 네거티브 행유형 및 기본값인 GenericRowType). 이 매개 변수를 제공하지 않으면 행에 작업이 부착되지 않습니다. 액션TextRes 및 작업또한 IconRes는 선택 사항이며, 해당되는 경우 작업 텍스트와 그리기 가능한 리소스를 설정하는 데 사용됩니다.

이 모든 것은 GenericListAdapter 및 해당 대체 GenericPagedListAdapter와 함께 제공됩니다. 어댑터는 서로 다른 수신기 또는 UI 표시 옵션에 대해 몇 가지 매개 변수를 사용합니다. 자세한 내용은 설명서를 참조하십시오.

자, 마이클의 문제에 대해서, 이것이 그 상황에 어떻게 적용되나요? 이 라이브러리를 사용하여 Michael은 상황에 따라 다른 수의 열을 지정하고 각 열에 다른 유형을 사용할 수 있습니다. 예를 들어, 전체 시간/파트 시간 열에 CheckBox를 사용하거나 action 열에 버튼을 추가합니다. 그러면 어떻게 작동하는지 살펴보겠습니다.

 

일반 RecyclerView 대신 사용자 지정 HeaderRecyclerView를 사용하여 다음과 같은 열의 이름을 가진 헤더를 추가하겠습니다.

<com.deluxe1.generic_tableview.view.HeaderRecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    android:id="@+id/recycler"
    />
      ```

      그런 다음 이 RecycleerView에 사용할 어댑터를 생성하겠습니다.

      ```js
      adapter = GenericListAdapter(
          maxDataColumns = 3,
              showHeader = true,
                  onItemSelectedListener = this,
                      onRowActionsListener = this,
                          onRowClickListener = this,
                              actionTypeDetector = MyActionTypeDetector(),
                                  highlightColorResId = R.color.teal_a700,
                                      alternateColoring = true
                                      )
                                      binding.recycler.setAdapter(adapter)
                                      ```

                                      여기서 무슨 일이 일어나는지 살펴보겠습니다. 최대 데이터 열 수(`액션` 열이 포함되지 않음)를 3개(이름, 성 및 전체 시간 상태, ID를 표시하지 않고 클라이언트 파일과 연결하는 데만 사용). 그런 다음 열 이름을 가진 헤더를 보여주고 싶다는 주석을 달았습니다. 온아이템SelectedListener, onRowActionsListener 및 onRowClickListener는 해당 작업을 활성화하는 모든 수신기이므로 사용자의 장시간 누르기를 처리할 수 있으므로 행을 클릭하고 행 작업 열을 클릭합니다.

                                      <div class="content-ad"></div>

                                      highlightColorResId는 onItem인 경우 선택에 사용되는 색상 리소스를 나타냅니다.SelectedListener가 제공되며, 대체 색상이 true로 설정된 경우, 모든 두 번째 행은 더 나은 시각적 구별을 위해 더 어둡게/밝게 변형됩니다.

                                      자, 이것은 중요합니다: 행동.TypeDetector. 사용자 지정 작업과 함께 사용자 지정 보기를 사용하려면 사용자 지정 작업을 제공해야 합니다.사용자 지정 보기를 해당 보기와 매핑할 TypeDetector홀더입니다. 이것은 이 라이브러리를 완전히 확장할 수 있게 하는 것입니다. 액션을 최대한 많이 추가할 수 있기 때문입니다.원하는 타입은 어댑터에게 알려주기만 하면 됩니다. 예는 다음과 같습니다.

                                      ```js
                                      class MyActionTypeDetector : ActionTypeDetector() {

                                          override fun getActionTypeForInt(value: Int): ActionType =
                                                  when (value) {
                                                              MyActionType.getIntValue() -> MyActionType
                                                                          CustomButtonActionType.getIntValue() -> CustomButtonActionType
                                                                                      else -> super.getActionTypeForInt(value)
                                                                                              }
                                                                                              }
                                                                                              ```

                                                                                              이것으로 나는 두 가지 액션을 추가한다.유형, MyAction유형 및 사용자 지정 단추액션이 어댑터와 함께 사용할 수 있는 유형입니다. 액션유형을 차례로 어댑터에 어떤 보기를 표시사용할 홀더 - 여기서 사용자 정의 레이아웃을 정의합니다.

                                                                                              ```js
                                                                                              object CustomButtonActionType : ActionType() {
                                                                                                  override fun <T : GenericListElement> getViewHolder(
                                                                                                          binding: GenericViewHolderBinding,
                                                                                                                  onRowActionsListener: OnRowActionsListener<T>?,
                                                                                                                          maxColumns: Int
                                                                                                                              ): GenericViewHolder<T> = CustomButtonViewHolder(binding, maxColumns, onRowActionsListener)
                                                                                                                              }
                                                                                                                              ```

                                                                                                                              <div class="content-ad"></div>

                                                                                                                              사용자 지정 버튼액션유형이 CustomButtonView를 사용함을 나타냅니다.GenericView의 구현체인 Holder작업에 대한 재료버튼을 반환하는 홀더(추가 사용자 지정 포함):

                                                                                                                              ```js
                                                                                                                              class CustomButtonViewHolder<T : GenericListElement>(binding : GenericViewHolderBinding,
                                                                                                                                                                             maxColumns : Int,
                                                                                                                                                                                                                            private val onRowActionsListener: OnRowActionsListener<T>?) :
                                                                                                                                                                                                                                GenericViewHolder<T>(binding, maxColumns) {

                                                                                                                                                                                                                                    override fun getView(element: T): View {
                                                                                                                                                                                                                                            return MaterialButton(
                                                                                                                                                                                                                                                        ContextThemeWrapper(
                                                                                                                                                                                                                                                                        binding.container.context,
                                                                                                                                                                                                                                                                                        R.style.button
                                                                                                                                                                                                                                                                                                    )
                                                                                                                                                                                                                                                                                                            ).apply {
                                                                                                                                                                                                                                                                                                                       //todo customize the button here
                                                                                                                                                                                                                                                                                                                               }
                                                                                                                                                                                                                                                                                                                                   }
                                                                                                                                                                                                                                                                                                                                   }
                                                                                                                                                                                                                                                                                                                                   ```

                                                                                                                                                                                                                                                                                                                                   좀 복잡하게 들리지만, 일단 요령만 익히면 쉬워요. 이것이 도서관을 확장하고 커스터마이징 할 수 있는 가장 좋은 방법이라고 생각했습니다. 어떻게 해야 할지 더 좋은 생각이 있으면 댓글로 듣고 싶어요.

                                                                                                                                                                                                                                                                                                                                   알았어, 다음으로 넘어가자. 그래서 우리는 어댑터를 준비하고 세팅했습니다. 활동은 어댑터에 제공한 수신기를 구현합니다.

                                                                                                                                                                                                                                                                                                                                   샘플 어플리케이션의 경우 처음에 설명한 것과 일치하는 두 개의 파일에서 몇 가지 추가 속성이 있는 데이터를 가져옵니다. 이러한 파일은 샘플 앱의 /raw 폴더에서 볼 수 있습니다. 먼저 GenericListElement를 확장할 모델을 만들고 어댑터에 사용할 속성과 표시 방법을 지정해야 합니다. 아주 간단합니다. 편집 가능한 이름, 성 및 전체 시간 속성을 표시하려고 하지만 ID는 표시하지 않습니다. 그리고 우리는 `액션`이 `제거` 액션이 있는 버튼이었으면 합니다. 이것은 다음과 같습니다.

                                                                                                                                                                                                                                                                                                                                   <div class="content-ad"></div>

                                                                                                                                                                                                                                                                                                                                   ```js
                                                                                                                                                                                                                                                                                                                                   data class Employee (val id: Int, val firstName: String, val lastName: String, val fullTime: Boolean)
                                                                                                                                                                                                                                                                                                                                       : GenericListElement(
                                                                                                                                                                                                                                                                                                                                           mapOf(
                                                                                                                                                                                                                                                                                                                                                   CustomTextView(R.string.id, id.toString()) to false,
                                                                                                                                                                                                                                                                                                                                                           CustomTextView(R.string.first_name, firstName, 1f) to true,
                                                                                                                                                                                                                                                                                                                                                                   CustomTextView(R.string.last_name, lastName, 1.2f) to true,
                                                                                                                                                                                                                                                                                                                                                                           CustomBooleanView(R.string.full_time, value = fullTime, true) to true
                                                                                                                                                                                                                                                                                                                                                                               ), type =  CustomButtonActionType, R.string.remove
                                                                                                                                                                                                                                                                                                                                                                               )
                                                                                                                                                                                                                                                                                                                                                                               ```

                                                                                                                                                                                                                                                                                                                                                                               그런 다음 이러한 파일을 구문 분석하고 데이터를 어댑터에 전달합니다.

                                                                                                                                                                                                                                                                                                                                                                               ```js
                                                                                                                                                                                                                                                                                                                                                                               override fun onResume() {
                                                                                                                                                                                                                                                                                                                                                                                   super.onResume()
                                                                                                                                                                                                                                                                                                                                                                                       adapter.setAdapterData(getEmployees())
                                                                                                                                                                                                                                                                                                                                                                                       }

                                                                                                                                                                                                                                                                                                                                                                                       private fun getEmployees(): List<Employee> {
                                                                                                                                                                                                                                                                                                                                                                                           val employees = arrayListOf<Employee>()
                                                                                                                                                                                                                                                                                                                                                                                               val ins: InputStream = resources.openRawResource(
                                                                                                                                                                                                                                                                                                                                                                                                       resources.getIdentifier(
                                                                                                                                                                                                                                                                                                                                                                                                                   "employees",
                                                                                                                                                                                                                                                                                                                                                                                                                               "raw", packageName
                                                                                                                                                                                                                                                                                                                                                                                                                                       )
                                                                                                                                                                                                                                                                                                                                                                                                                                           )
                                                                                                                                                                                                                                                                                                                                                                                                                                               ins.bufferedReader().forEachLine { line ->
                                                                                                                                                                                                                                                                                                                                                                                                                                                       val lineValues = line.split(",").map { it.trim() }
                                                                                                                                                                                                                                                                                                                                                                                                                                                               employees.add(Employee(lineValues[0].toInt(), lineValues[1],lineValues[2], lineValues[3].toBooleanStrict()))
                                                                                                                                                                                                                                                                                                                                                                                                                                                                   }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                       return employees
                                                                                                                                                                                                                                                                                                                                                                                                                                                                       }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                       ```

                                                                                                                                                                                                                                                                                                                                                                                                                                                                       데이터가 표시되고 행은 선택 및 클릭이 가능하며 다른 작업을 위한 별도의 단추가 있습니다(이 경우 제거). 이러한 각 작업이 수행될 때 수행할 작업만 정의하면 됩니다.

                                                                                                                                                                                                                                                                                                                                                                                                                                                                       ```js
                                                                                                                                                                                                                                                                                                                                                                                                                                                                       override fun onRowClicked(row: Employee) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                           startActivity(Intent(this, ClientActivity::class.java).apply {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   putExtra("EMPLOYEE_ID", row.id)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           putExtra("EMPLOYEE_NAME", "${row.firstName} ${row.lastName}")
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               })
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               }

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               override fun onItemSelected(item: Employee, isSelected: Boolean, totalSelected: Int) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   if (totalSelected > 0) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           menuItem?.isVisible = true
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   title = "$totalSelected Selected"
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       } else {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               menuItem?.isVisible = false
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       resetActionBar()
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           }

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           override fun onAction(element: Employee, action: RowAction) {
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               adapter.removeItem(element)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ```

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               <div class="content-ad"></div>

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               그게 바로 이거죠. 우리의 활동에서 몇 줄의 코드와 함께, 데이터는 우리가 원하는 대로 제시됩니다. 당신은 내 GitHub에서 전체 코드를 찾을 수 있으며, 라이브러리의 내부 논리를 설명하는 추가 문서가 있다. 클라이언트 목록에 대한 두 번째 요청도 그곳에서 실행되는데, 여기서 했던 것과 거의 비슷하지만 코드를 약간 수정하면 원하는 동작을 얻을 수 있습니다. 좋은 점은 모든 데이터 세트에 대해 이 작업을 수행할 수 있으며, 다른 모델만 있으면 됩니다(커스터마이징을 원할 경우 다른 뷰도 필요). 나머지는 그대로입니다.

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               - 코드의 일부분은 의도적으로 생략된 것으로 인정되고, 읽기가 길고, 크기가 작아진다.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               - 직원들의 이름과 회사 그리고 그들의 숫자가 구성되어 있습니다. 모카루가 만들어냈다.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               - 마이클은 실제 사람도 아니고 이니텍도 실제 회사가 아니다. 모든 유사점은 순전히 우연의 일치이다.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               - 또한 이 블로그 포스트에 표시된 코드 스니펫은 컴파일할 수 없습니다. 완전한 기능의 샘플은 GitHub repo를 참조하십시오.
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               - 뭔가 안 되면 세미콜론을 잘못 꽂았나 봐요.

댓글