기존에 Button 터치에 대한 이벤트를 처리한다고 하자.
1. findViewById
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val startButton = findViewById<Button>(R.id.btn_start)
startButton.setOnClickListener {
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
}
}
위의 코드는 매우 간단하지만 만약 Button이 100개 라면 findViewById를 100번 할 것인가? 그것은 매우 비효율적인 코드를 남길 것이다.
-> 그렇게 findViewById를 쓰지 않고도 ImageView 컴포넌트를 가져올 수 있는 방법이 Kotlin Extensions이다.
2. Kotlin Extensions
- 사용 전 세팅 : build.gradle.kts(Module)의 플러그인에 추가한다.
plugins {
id("kotlin-android-extensions")
}
// 2. 컴포넌트의 id값만 써도 자동으로 컴포넌트를 가져올 수 있다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_start.setOnClickListener {
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
}
}
매우 편하다고 생각할 수 있지만, Google에서 사용하지 말라고 권고한다.
그 이유는 치명적인 오류를 발생시킬 수 있기 때문이다.
어느 상황에서 오류가 날 수 있냐면, second 액티비티의 UI인 xml에 정의된 id값을 실수로 다른 액티비티에서 접근한다면 런타임 오류(null object reference)가 발생하는 치명적인 case가 존재한다.
그리고 추가로 RecyclerView 같은 컴포넌트를 쓸 때, 성능 이슈가 발생할 수 있다는 문제도 지적된다.
이러한 문제점을 지적하면서 Google은 ViewBinding과 DataBinging 사용을 지향했다.
3. ViewBinding
- findViewById와의 차이점 :
1. Null-Safe : 유효하지 않은 뷰 ID로 인해 null pointer Exception이 발생하지 않습니다.
2. Type-Safe : 반환 타입이 일치하지 않을 때 발생하는 Exception이 발생하지 않습니다.
-> val startButton = findViewById<Button>(R.id.btn_start)에서 btn_start 컴포넌트가 Button이 아닌 ImageView일 때 나는 오류나는 경우를 말함
- DataBinding과 비교했을 때의 장단점 :
1. 장점 : 더 빠른 컴파일 시간, 사용 편의성(특별히 태그된 XML 레이아웃 파일이 필요하지 않음)
2. 단점(제한사항) : 동적 UI 콘텐츠에 사용할 수 없음, 양방향 데이터 결합을 지원하지 않음
- 사용 전 세팅 : build.gradle.kts(Module)의 android 안에 buildFeatures를 추가한다.
android {
buildFeatures {
viewBinding = true
}
}
3-1. in Activity
// 파일 타입과 이름에 따라 Binding 타입이 정의됨
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 여기서 부터 다름
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.ivStart.setOnClickListener {
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
}
}
3-2. in Fragment
// 기존 findViewById
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_question, container, false)
val title: TextView = view.findViewById(R.id.tv_question_title)
title.text = getString(questionTitle[questionType])
val questionTextViews = listOf<TextView>(
view.findViewById(R.id.tv_question_1),
view.findViewById(R.id.tv_question_2),
)
return view
}
// ViewBinding 적용 결과
private var _binding : FragmentQuestionBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentQuestionBinding.inflate(inflater, container, false)
val view = binding.root
binding.tvQuestionTitle.text = getString(questionTitle[questionType])
val questionTextViews = listOf<TextView>(
binding.tvQuestion1,
binding.tvQuestion2,
)
return view
}
diff : https://github.com/rlaxodud214/NBC-MBTI/commit/9b1bb8f75d9c28d7e72674cf12edee2af19c1608
4. DataBinding
- 사용 전 세팅1 : build.gradle.kts(Module)의 android 안에 buildFeatures를 추가한다.
android {
buildFeatures {
dataBinding = true
}
}
- 사용 전 세팅2 : 사용하려는 xml파일의 컴포넌트 전체를 <layout>으로 한 번 감싸줘야 한다. => 최상위 컴포넌트가 layout이 되도록 해야함
4-1. in Activity
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.ivStart.setOnClickListener {
val intent = Intent(this, TestActivity::class.java)
startActivity(intent)
}
}
DataBinding을 (class의 데이터로 XML에 데이터를 셋업하는 로직을) 입문 과제에 적용해봤다.
1. xml에 data 태그로 사용할 변수 선언(user)
2. 액티비티에서 binding.user에 데이터 채우기(연동)
3. xml에서 user 사용 -> android:text="@{user.password}"
// 변경점 없음
data class UserData(
val name: String,
val email: String,
val id: String,
val password: String,
) : Serializable
<layout>
<data> // dataBinding
<variable
name="user" // 원하는 대로 명명
type="com.example.introduce.domain.UserData" /> // 위 UserData class의 경로
</data>
~~~~~~~~~~~~~~~~~~~~
<EditText
android:id="@+id/et_id"
android:textSize="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.id}" // dataBinding
android:hint="@string/hint_id"/>
<EditText
android:id="@+id/et_pw"
android:textSize="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.password}" // dataBinding
android:hint="@string/hint_pw"
android:inputType="textPassword" />
~~~~~~~~~~
</layout>
위처럼 xml 코드를 수정해준다. data 태그의 user 프로퍼티를 액티비티에서 값 셋업
주의사항! - 지금은 user.id와 user.password가 String 타입이여서 오류가 없지만, 만약 Int 타입을 넣어야 하는 경우 아래와 같이 형변환이 필요함
android:text="@{Integet.toString(user.age)}"
android:text="@{user.age.toString()}"
액티비티 코드에서 아래와 같이 user 프로퍼티의 값 초기화
// 데이터 바인딩 적용된 코드
userData = result.data?.getSerializableExtra("userData") as UserData
if (this::userData.isInitialized) {
binding.user = userData // 데이터 셋업
}
// 데이터 바인딩 적용 전 코드
userData = result.data?.getSerializableExtra("userData") as UserData
if (this::userData.isInitialized) {
userData.run {
editTextID.setText(id)
editTextPW.setText(password)
}
}
바인딩 객체의 user(아까 xml에서 data의 name으로 정의한 거) 프로퍼티에 UserData타입인 userData를 넣어주면 된다!!
diff : https://github.com/rlaxodud214/NBC-Introduce/commit/8d32a527a86dcf1cdfc8b378e26029f8f6863f90
느낀점 : 신세계다,,, 확실히 액티비티의 UI 셋업 코드를 많이 줄일 수 있을 것 같음
-> 다음 실습 : RecyclerView + Adapter에 DataBinding 적용하기
4-2. in Fragment
// 기존 findViewById
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_question, container, false)
val title: TextView = view.findViewById(R.id.tv_question_title)
title.text = getString(questionTitle[questionType])
val questionTextViews = listOf<TextView>(
view.findViewById(R.id.tv_question_1),
view.findViewById(R.id.tv_question_2),
)
return view
}
// DataBinding 적용 결과
private var _binding : FragmentQuestionBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DataBindingUtil.inflate(inflater, R.layout.fragment_question, container, false)
val view = binding.root
binding.tvQuestionTitle.text = getString(questionTitle[questionType])
val questionTextViews = listOf<TextView>(
binding.tvQuestion1,
binding.tvQuestion2,
)
return view
}
- Activity에서 FrameLayout(R.id.frameArea)에 Fragment 추가하는 방법
val manager = supportFragmentManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val transcation = manager.beginTransaction()
val fragment = QuestionFragment()
transcation.replace(R.id.frameArea, fragment)
transcation.addToBackStack(null)
transcation.commit()
}
'Android' 카테고리의 다른 글
앱 개발 입문 과제 해설 후기 (1) | 2024.03.29 |
---|---|
안드로이드 Button 디자인(shape, corners, stroke, gradient) (3) | 2024.03.29 |
[docs] 앱 아키텍처 가이드 읽어보기 (1) | 2024.03.22 |
호출한 액티비티에서 결과 가져오기, registerForActivityResult (0) | 2024.03.20 |
안드로이드 앱개발 입문 1~4주차 학습 (0) | 2024.03.18 |