목표
View Binding을 적용한다
2022.01.24 - [Android] - Android Fragment 설정하기
2022.01.24 - [Android] - Android Fragment Navigation with Action
2022.01.25 - [Android] - Android Fragment with Arguments
앞서 진행한 posts의 연속입니다. 마지막 포스트에있는 아래 소스 다운받고 보시면 더 편하실거에요.
https://github.com/theyoung/fragmentsetup/tree/2de0436018dde923c210601237c121df6909aa4c
findViewById의 단점
앞서 만든 TestFragment.kt 파일을 확인해 보자
class TestFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_test, container, false)
view.findViewById<Button>(R.id.button).setOnClickListener {
val str = view.findViewById<EditText>(R.id.editTextTextPersonName).text.toString() ?: "hello"
val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
view.findNavController().navigate(action)
}
return view
}
}
R.layout.fragment_test라고 하는 fragment에서 root view를 갖어 왔다.
이 root view에서 onClick Event를 listen할 button을 ID로 갖어와서 Button으로 Class Casting후 사용하게 된다.
여기서 크게 2가지 문제점이 발생하게 된다.
- Not Null Safe : 만약 R.id.button이라는 ID가 없다면 setOnclickListener가 Null Point Exception이 발생하게 된다
view.findViewById<Button>(R.id.button).setOnClickListener{....}
- Not Type Safe : Casting Class가 Button이 아니라면? 개발자 실수에 의해 오류가 발생하게 됩니다.
view.findViewById<Button>
viewBinding 설정하기
app 하위에 있는 build.gradle에 buildfeatures를 추가해 줍니다.
android {
...
buildFeatures {
viewBinding true
}
}
build.gradle을 수정 후 sync > clean Project > RebuildProject를 진행해 줍니다.
이와 같이 3가지를 진행하고 나면 Project Perspective에서 Activity 및 Fragment binding이 XML 명과 비슷하게 build 된것을 확인 할 수 있습니다.
이것이 확인이 안되면 view bindind이 안된다고 생각하시면 됩니다.
Fragment에 Binding 설정하기
TestFragment.kt에 있는 binding 설정을 변경해 보자.
앞에서 보았던 원본 소스는 아래와 같습니다.
class TestFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_test, container, false)
view.findViewById<Button>(R.id.button).setOnClickListener {
val str = view.findViewById<EditText>(R.id.editTextTextPersonName).text.toString() ?: "hello"
val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
view.findNavController().navigate(action)
}
return view
}
}
이 소스를 preBuilt된 binding소스를 이용해서 변경해 볼 예정입니다.
우선 TestFragement와 연결된 xml인 R.layout.fragment_test에서 파생된 build 파일인 FragmentTestBinding을 연결 시켜야합니다.
class TestFragment : Fragment() {
var _binding: FragmentTestBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? { ....
우선
var _binding: FragmentTestBinding? = null
이와같이 작성한 이유는 이후에 onDestroyView를 통해서 null처리가 필요하기 때문이다.
그런데 문제는 binding하는 코드 자체에 null일경우 null exception이 가능한 코드가 될 가능성이 있다.
_binding = FragmentTestBinding.inflate(inflater,container,false)
_binding.button.setOnClickListener {
위에 코드를 보면 _binding에서 바로 button이라고 하는 view 객체를 접근하는 것을 볼 수 있다.
앞서 null able로 _binding이 선언되었기 때문에 잠재적으로 null exception을 발생할 여지가 있다.
그래서 임시 방편으로 null check를 넣어 줄순 있지만, 좋지 않은 방법이다.
_binding = FragmentTestBinding.inflate(inflater,container,false)
_binding?.button?.setOnClickListener {
그래서
var _binding: FragmentTestBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentTestBinding.inflate(inflater,container,false)
binding.button.setOnClickListener {
val str = binding.editTextTextPersonName.text.toString() ?: "hello"
val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
binding.root.findNavController().navigate(action)
}
return binding.root
}
상기와 같이 binding get() 을 사용해서 null이 없음을 보여주게 됩니다.
binding.button.setOnClickListener {
val str = binding.editTextTextPersonName.text.toString() ?: "hello"
val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
binding.root.findNavController().navigate(action)
실제 binding한 코드를 보면 findviewbyid와는 다르게 button을 직접 access하는 것을 볼수 있습니다. 이를 통해서 특정 view의 객체에 더빠른 접근과 typecating으로 부터 오는 잠재적 오류를 사전에 막을 수 있습니다.
가장 중요한 부분이 남았는데요. 바로 onDestroyView의 사용입니다.
해당 시점에 _binding = null 을 사용해서 객체를 해제해주는 것을 볼 수 있습니다.
override fun onDestroyView() {
super.onDestroyView()
Log.d(this.javaClass.name,"finish view")
_binding = null
}
fragment는 activity의 lifecycle을 활용하지만 정확히는 activity의 한파트로써 독립적인 라이프 사이클을 갖게 됩니다.
https://www.geeksforgeeks.org/difference-between-a-fragment-and-an-activity-in-android/
그래서 activity와 다르게 onDestroyView와 onDestroy 둘다 사용이 가능합니다.
하지만 onDestroy는 Class finish이후 불특정한 시점에 불리게 됨으로 Leak을 발생시킬 가능성이 있습니다.
그래서 화면에서 View가 사라지는 순간 _binding을 null화 함으로써 GC시 clear가능상 상태로 만들어 줘야 합니다.
만약에 _binding = null을 처리해주지 않으면 아래와 같이 Leak의 대상이 되는 것을 확인 할 수 있습니다.
위의 내용가지 완료 되면 아래와 같이 정상 작동 되는 것을 확인 할 수 있습니다.
전제 코드는 아래와 같습니다.
package com.example.fragmentsetup
import android.os.Bundle
import android.util.Log
import android.util.Log.*
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import androidx.navigation.findNavController
import com.example.fragmentsetup.databinding.FragmentTestBinding
import java.util.logging.Level.INFO
import java.util.logging.Logger
/**
* A simple [Fragment] subclass.
* Use the [TestFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class TestFragment : Fragment() {
var _binding: FragmentTestBinding? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentTestBinding.inflate(inflater,container,false)
binding.button.setOnClickListener {
val str = binding.editTextTextPersonName.text.toString() ?: "hello"
val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
binding.root.findNavController().navigate(action)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
Log.d(this.javaClass.name,"finish view")
_binding = null
}
}
기능상으로는 별차이 없겠죠?
소스는 아래에 있습니다.
https://github.com/theyoung/fragmentsetup/tree/63ade4463beda013c96a7a85f7b48ce757f4ebc5
GitHub - theyoung/fragmentsetup
Contribute to theyoung/fragmentsetup development by creating an account on GitHub.
github.com
ViewModel을 활용해서 ListView를 표현해 보겠습니다.
'Android' 카테고리의 다른 글
Android BaseAdapter vs RecyclerAdapter 작동 원리 (RecyclerView 개발) (0) | 2022.02.12 |
---|---|
Android ViewModel & ListView 사용하기 (0) | 2022.02.05 |
Android Fragment with Arguments (0) | 2022.01.25 |
Android Fragment Navigation with Action (0) | 2022.01.24 |
Android Fragment 설정하기 (0) | 2022.01.24 |