#1 객체 전달의 필요성
#1-1 이전 글
이전 글에서 이어진다. 이전 글에선, 데이터 바인딩을 통해 View의 레퍼런스를 일괄적으로 가져와 참조할 수 있었다. 이번에는 반대로 View에게 객체를 보낸다. 아래의 예시 앱을 보자.
#1-2 예시 앱
이 앱은 어떤 책 클래스의 객체를 받아 화면에 표시하는 앱이다. 화면 아래에 있는 버튼들을 누르면 화면 위 쪽의 TextView들의 text 속성을 변경한다. 이전 글의 내용에 따라 코드를 작성했으며, 그 코드는 다음과 같다.
#1-3 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/book_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@+id/book_author"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/book_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@id/book_year"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/book_name" />
<TextView
android:id="@+id/book_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/book_author" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />
<Button
android:id="@+id/button_book_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="월든"
app:layout_constraintBottom_toTopOf="@id/button_book_3"
app:layout_constraintEnd_toStartOf="@id/button_book_2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<Button
android:id="@+id/button_book_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="어린 왕자"
app:layout_constraintBottom_toTopOf="@id/button_book_4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/button_book_1"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<Button
android:id="@+id/button_book_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="도쿄의 디테일"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_book_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_book_1" />
<Button
android:id="@+id/button_book_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="플립 싱킹"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_book_3"
app:layout_constraintTop_toBottomOf="@+id/button_book_2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
#1-4 Book.kt
// package com.example.withobjectpractice
data class Book (
val name: String,
val author: String,
val year: Int
)
#1-5 MainActivity.kt
// package com.example.withobjectpractice
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.databinding.DataBindingUtil
import com.example.withobjectpractice.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
onShowBookInfoButtonClick(binding.buttonBook1, getWalden())
onShowBookInfoButtonClick(binding.buttonBook2, getTheLittlePrince())
onShowBookInfoButtonClick(binding.buttonBook3, getDetailsOfTokyo())
onShowBookInfoButtonClick(binding.buttonBook4, getFlipThinking())
}
private fun onShowBookInfoButtonClick(button: Button, book: Book) {
button.setOnClickListener{
binding.bookName.text = "이름: " + book.name
binding.bookAuthor.text = "저자: " + book.author
binding.bookYear.text = "출판년도: " + book.year.toString() + "년"
}
}
private fun getWalden(): Book {
return Book("월든", "David Thoreau", 1854)
}
private fun getTheLittlePrince(): Book {
return Book("어린 왕자", "Saint-Exupéry", 1943)
}
private fun getDetailsOfTokyo(): Book {
return Book("도쿄의 디테일", "생각노트", 2018)
}
private fun getFlipThinking(): Book {
return Book("플립 싱킹", "Berthold Gunster", 2023)
}
}
책 객체를 받아서 이름, 저자, 출판년도로 이루어진 프로퍼티를 하나하나 View에 대입한다. 이 코드는 findViewById()를 사용하는 것보다는 낫다. 하지만, 앱의 기능이 많아지고 Activity의 코드 길이도 길어지면, 이와 같이 View에 세부 사항에 관여하는 코드는 굉장히 거슬리게 될 것이다. 이 때, 세부 사항을 Activity에서 처리하지 않고, Book 객체 자체를 View에 전달해서 그 View보고 알아서 그 객체를 다루라고 한다면 Activity(Cotroller)는 더 가벼워질 것이다. 즉 객체를 전달한다는 말은, Cotroller는 데이터만 전달하고 그 데이터가 어떻게 보일지는 관여하지 않게 만들겠다는 소리다.
지금부터 #1의 코드가 객체를 전달하게끔 리팩토링 해본다.
#2 객체를 View에 보내게 수정하기
#2-1 activity_main.xml에서 <variable> 태그 추가 및 활용
<?xml version="1.0" encoding="utf-8"?>
<layout ... >
<data>
<variable
name="book"
type="com.example.withobjectpractice.Book" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<TextView
...
android:text="@{`이름: ` + book.name}"
... />
<TextView
...
android:text="@{`저자: ` + book.author}"
... />
<TextView
...
android:text="@{`출판년도: ` + book.author + `년`}"
... />
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<data> 태그를 xml에 넣는다. 그 속에 <variable> 태그를 넣는다. variable 태그는 type과 name의 2가지 속성이 있다. type에는 data class의 정확한 위치를 적고, name에는 type의 긴 이름을 대신할 별명을 지정한다. 이제 Data binding 클래스가 자동으로, ActivityMainBinding 클래스에 book이라는 이름의 프로퍼티를 추가한다. ActivityMainBinding 클래스의 활용은 MainActivity.kt의 몫이다. MainActivity.kt가 어떤 Book 객체를 View단으로 보내줄 지는 View 입장에서 알 길이 없으나, 그것이 어떻게 보일지는 통제할 수 있다. 예를 들어, <TextView>의 속성 android:text에 "@{book.author}"와 같이 book 프로퍼티를 참조하는 값을 넣는 것과 같이 말이다.
#2-2 MainActivity.kt 수정
// package com.example.withobjectpractice
...
class MainActivity : AppCompatActivity() {
...
private fun onShowBookInfoButtonClick(button: Button, book: Book) {
button.setOnClickListener{
binding.book = book
}
}
...
}
ActivityMainBinding의 프로퍼티에 book이 추가되었으므로, 해당 프로퍼티를 활용하게끔 코드를 수정한다. 이후의 작업 즉 이 객체를 어떻게 잘 표시할 것인가?는 activity_main.xml (View)에게 맡긴다. (#2-1 참조)
#2-3 변경사항 작동 확인, 아쉬운 점
제대로 작동한다. 하지만, 아무 버튼도 클릭되지 않은 처음 화면이 거슬린다. <variable>에 아무런 객체가 전달되지 않았기 때문에, null을 반환하는 것이다. 객체가 전달되지 않은 상황에서도 자연스럽게 화면을 표시하기 위해서 다음과 같이 코드를 변경한다.
#2-4 null값을 고려한 XML 코드
<?xml version="1.0" encoding="utf-8"?>
<layout ... >
<data>
<variable ... >
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<TextView
...
android:text="@{book != null ? `이름: ` + book.name : `책 정보 없음`}"
... />
<TextView
...
android:text="@{book != null ? `저자: ` + book.author : ``}"
... />
<TextView
...
android:text="@{book != null ? `출판년도: ` + book.year + `년` : ``}"
... />
...
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
book에 대해 null 체크를 하고, 객체가 null일 때와 아닐 때를 분기시켜 각각 표시할 문자열을 써 넣는다. 이렇게 변경하고 앱을 실행시키면 다음과 같다.
#2-5 결론 (객체 전달의 이점)
데이터 바인딩 라이브러리를 통해서, data class 등의 객체(Model)를 View에 직접(directly) 보낼 수 있다. 이 방식으로 Activity와 View(xml) 간의 결합도를 느슨하게 만들었다. Activity는은 View에 어떤 데이터를 보낼지만 고민하고, 그 이상으로 신경쓰지 않는다. View는 Activity로부터 받아들인 객체를 오로지 어떻게 표시할 지만을 기술한다. 이러한 느슨한 결합도는 데이터 바인딩 사용으로 얻을 수 있는 큰 이점이다.
#3 요약
객체를 View에 보내는 이유는 '분업'을 하기 위함이다.
#4 완성된 앱
'깨알 개념 > Android' 카테고리의 다른 글
[Android] LiveData - 기초 (0) | 2024.01.16 |
---|---|
[Android] ViewModel - 뷰 모델에 인자(Argument) 전달 (ViewModelFactory) (0) | 2024.01.15 |
[Android] ViewModel - 기초 (0) | 2024.01.13 |
[Android] Data Binding - 기초 (0) | 2024.01.11 |
AndroidX (구 Support Library) (0) | 2023.12.13 |