몇 달 전에 저는 제가 만든 맞춤형 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를 참조하십시오.
- 뭔가 안 되면 세미콜론을 잘못 꽂았나 봐요.
'프로그래밍' 카테고리의 다른 글
OSMnx: OpenStreetMap에서 데이터를 가장 빠르게 가져오는 방법 (0) | 2022.01.12 |
---|---|
이 Python 라이브러리로 웹 스크래핑 속도를 10배 높이십시오. (0) | 2022.01.12 |
이진 검색 - 더미용 (0) | 2022.01.11 |
주간 요약 #56 (0) | 2022.01.04 |
게임에서 차량 정비 기술을 구현하는 방법 - Part 5 (0) | 2022.01.04 |
댓글