프래그먼트는 간단하게 말해 앱의 사용자 인터페이스에서 재사용 가능한 부분을 말한다.
Activity과 마찬가지로 프래그먼트는 수명 주기가 있고 사용자 입력에 응답할 수 있다.
재사용성과 모듈성을 강조
단일 activity에서 여러 프래그먼트를 동시에 호스팅할 수도 있다.
각 프래그먼트는 별도의 자체 수명 주기를 관리
프래그먼트에는 5가지 Lifecycle.State가 있다.
INITIALIZED
: 프래그먼트의 새 인스턴스가 인스턴스화 되었습니다.
CREATED
: 첫 번째 프래그먼트 수명 주기 메서드가 호출됩니다. 이 상태에서 프래그먼트와 연결된 뷰도 만들어집니다.
STARTED
: 프래그먼트가 화면에 표시되지만 '포커스'가 없으므로 사용자 입력에 응답할 수 없습니다.
RESUMED
: 프래그먼트가 표시되고 포커스가 있습니다.
DESTROYED
: 프래그먼트 객체의 인스턴스화가 취소되었습니다.
Activity처럼 Fragment 수명 주기 이벤트에 응답하기 위해 재정의할 수 있는 메서드는 다음과 같다.
onCreate()
: 인스턴스화되어 CREATED 상태지만 뷰는 아직 만들어지지 않았다. Activity와 달리 레이아웃을 확장할 수 없고 뷰를 바인딩할 수 없음을 주의하자!
onCreateView()
: 레이아웃을 확장한다.
onViewCreated()
: 뷰가 만들어진 후 호출된다. 일반적으로 여기서 findViewById()
를 호출하여 특정 뷰를 속성에 바인딩한다.
onStart()
: STARTED 상태로 전환
onResume()
: RESUMED 상태로 전환되었고 포커스되어 사용자 입력에 응답할 수 있다.
onPause()
: STARTED 상태로 다시 전환되어 UI가 사용자에게 표시된다.
onStop()
: CREATED 상태로 전환된다. 객체가 인스턴스화되었지만 더 이상 화면에 표시되지 않는다.
onDestroyView()
: DESTROYED 상태로 전환되기 직전에 호출. 뷰는 메모리에서 이미 삭제되었지만 프래그먼트 객체는 남아있다.
onDestroy()
: DESTROYED 상태로 전환
이전 시간에 완성한 Words app을 Fragment를 사용하도록 수정한다.
File > New > Fragment > Fragment (Blank)
에서 클래스와 레이아웃 파일을 생성할 수 있다. LetterListFragment.kt
와 WordListFragment.kt
를 생성한다.
기본으로 작성되어 있는 코드가 많은데 모두 삭제하고 클래스 선언 부분만 남겨둔다.
이렇게!
package com.example.wordsapp
import androidx.fragment.app.Fragment
class LetterListFragment : Fragment() {
}
알파벳 RecyclerView를 표시하는 activity_main.xml
내용을 fragment_letter_list.xml
로 옮긴다.
단어 item RecyclerView를 표시하는 activity_detail.xml
내용을 fragment_word_list.xml
에 옮긴다.
주의! tools:context
에서 연결되어있는 activity를 각 fragment로 수정한다.
class LetterListFragment : Fragment() {
private var _binding: FragmentLetterListBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
. . .
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLetterListBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
. . .
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
. . .
}
. . .
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
뷰 바인딩을 구현하려면 null을 허용하는 FragmentLetterListBinding
이 필요하다.
✔ null 허용?
onCreateView()
가 호출될 때까지 레이아웃을 확장할 수 없기 때문. LetterListFragment
의 인스턴스가 만들어지는 시점은 onCreate()
가 시작될 때!
private var _binding: FragmentLetterListBinding? = null
null을 허용하므로 _binding
에 접근할 때마나 null 안전을 위해 ?
를 포함해야한다. _binding?.someView
액세스할 때 값이 null이 아님을 확신하는 경우 유형 이름에 !!
를 추가해 ?
연산자 없이 다른 속성처럼 액세스할 수 있다.
참고: !!를 사용하여 변수를 null을 허용하는 것으로 만들 때 변수가 null이 아님을 아는(_binding이 onCreateView()에서 할당된 후 값을 보유하는 것을 아는 것처럼) 위치 한두 군데에서만 사용하는 것이 좋습니다. 이런 식으로 null을 허용하는 값에 액세스하는 것은 위험하며 비정상 종료가 발생할 수 있으므로 최소한으로 사용합니다.
private val binding get() = _binding!!
여기서 get()
은 이 속성이 'get-only'임을 나타낸다. 즉, 값을 가져올 수 있지만 할당되고 나면 다른 것에 할당할 수 없다.
참고: 속성 이름 앞에 밑줄이 있는 것을 자주 볼 수 있습니다. 일반적으로 속성에 직접 액세스하지 못하도록 하기 위함입니다.
onCreateView() - 레이아웃 확장
onCreateView()
메서드를 생성하고 View?{} 내부에 레이아웃 확장 코드를 작성한다. onCreateView()
에서 레이아웃이 확장된다는 것을 배웠다. 뷰를 확장하고, _binding
값을 설정한 다음 루트 뷰를 반환한다.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLetterListBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
onViewCreated() - 바인딩
클래스 상단에 리싸이클러뷰 변수를 만들고, onViewCreated
에서 xml의 recycler_view
와 바인딩한다.
private lateinit var recyclerView: RecyclerView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
chooseLayout()
}
onDestroyView()
뷰가 더 이상 없으므로 _binding
을 null
로 재설정한다.
onCreate()
ctrl+O
로 onCreate()
를 찾아서 생성한다.
setHasOptionMenu()
를 호출한다.
onCreateOptionMenu()
activity에서는 menuInflater
라는 전역 속성이 있었는데 fragment에서는 없다. 대신 inflater
를 사용한다.
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.layout_menu, menu)
val layoutButton = menu.findItem(R.id.action_switch_layout)
setIcon(layoutButton)
}
chooseLayout(), setIcon(), onOptionsItemSelected()
메인액티비티에서 코드를 옮겨온다.
✔ 단! activity와 달리 fragment는 Context
가 아니다. 즉 this
대한 context
속성을 제공해야한다. this
를 context
로 변경.
다음 글에서 Nav Graph 생성하고, arguments를 추가하는 부분 참고...!
이전에 activity?.intent
를 참조하여 letter
extra에 액세스했는데 이 방식은 권장되는 방법은 아니다. (프래그먼트가 다른 레이아웃에 삽입될 수 있고 큰 앱에서는 프래그먼트가 속하는 활동을 추측하기가 훨씬 더 어렵기 때문. 또한 nav_graph를 사용하여 탐색을 실행하고 안전 인수를 사용하면 인텐트가 없으므로 인텐트 extras에 액세스하려고 해도 효과가 없다.)
WordListFragment
클래스 상단에 letterId
속성을 만든다.private lateinit var letterId: String
onCreate()
에서 arguments
를 작성한다. override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
letterId = it.getString(LETTER).toString()
}
}
// 이 코드를
recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())
// 이렇게
recyclerView.adapter = WordAdapter(letterId, requireContext())
class WordListFragment : Fragment() {
private var _binding: FragmentWordListBinding? = null
private val binding get() = _binding!!
private lateinit var letterId: String
companion object{
const val LETTER = "letter"
const val SEARCH_PREFIX = "https://www.google.com/search?q="
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
letterId = it.getString(LETTER).toString()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWordListBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())
recyclerView.addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}