깨알 개념/Android

[Android] Data Binding - View에 객체 전달

interfacer_han 2024. 1. 12. 14:57

#1 객체 전달의 필요성

#1-1 이전 글

 

Data Binding - 기초

#1 데이터 바인딩 사용 전 #1-1 예시 앱 위과 같은 간단한 앱이 있다. Button을 누르면, EditText의 text가 바로 위에 있는 TextView의 text에 대입된다. 이 앱의 코드는 다음과 같다. #1-2 activity_main.xml #1-3 Main

kenel.tistory.com

이전 글에서 이어진다. 이전 글에선, 데이터 바인딩을 통해 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-6 결론 (객체 전달의 이점)

데이터 바인딩 라이브러리를 통해서, data class 등의 객체(Model)를 View에 직접(directly) 보낼 수 있다. 이 방식으로 Activity와 View(xml) 간의 결합도를 느슨하게 만들었다. Activity는은 View에 어떤 데이터를 보낼지만 고민하고, 그 이상으로 신경쓰지 않는다. View는 Activity로부터 받아들인 객체를 오로지 어떻게 표시할 지만을 기술한다. 이러한 느슨한 결합도는 데이터 바인딩 사용으로 얻을 수 있는 큰 이점이다.
 

#3 요약

객체를 View에 보내는 이유는 '분업'을 하기 위함이다.

 

#4 완성된 앱

https://github.com/Kanmanemone/android-practice/tree/master/data-binding/ObjectToView