
프래그먼트는 간단하게 말해 앱의 사용자 인터페이스에서 재사용 가능한 부분을 말한다.
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
}
}