Unit 3: Navigation (3)

quokka·2021년 11월 12일
0

Android Basics in Kotlin

목록 보기
12/25
post-thumbnail

Fragment

  • 프래그먼트는 간단하게 말해 앱의 사용자 인터페이스에서 재사용 가능한 부분을 말한다.

  • Activity과 마찬가지로 프래그먼트는 수명 주기가 있고 사용자 입력에 응답할 수 있다.

  • 재사용성모듈성을 강조

  • 단일 activity에서 여러 프래그먼트를 동시에 호스팅할 수도 있다.

  • 각 프래그먼트는 별도의 자체 수명 주기를 관리

Fragment LifeCycle

프래그먼트에는 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 (Activity → Fragment)

이전 시간에 완성한 Words app을 Fragment를 사용하도록 수정한다.

Fragment 생성

Fragment와 연결된 xml 함께 생성

File > New > Fragment > Fragment (Blank)에서 클래스와 레이아웃 파일을 생성할 수 있다. LetterListFragment.ktWordListFragment.kt를 생성한다.

클래스 선언만 남기기

기본으로 작성되어 있는 코드가 많은데 모두 삭제하고 클래스 선언 부분만 남겨둔다.
이렇게!

package com.example.wordsapp
import androidx.fragment.app.Fragment
class LetterListFragment : Fragment() {

}

xml 옮기기

알파벳 RecyclerView를 표시하는 activity_main.xml 내용을 fragment_letter_list.xml로 옮긴다.
단어 item RecyclerView를 표시하는 activity_detail.xml 내용을 fragment_word_list.xml에 옮긴다.
주의! tools:context에서 연결되어있는 activity를 각 fragment로 수정한다.

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()
뷰가 더 이상 없으므로 _bindingnull로 재설정한다.

Fragment Menu

onCreate()

ctrl+OonCreate()를 찾아서 생성한다.

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 속성을 제공해야한다. thiscontext로 변경.

Fragment 인수 가져오기

다음 글에서 Nav Graph 생성하고, arguments를 추가하는 부분 참고...!

이전에 activity?.intent를 참조하여 letter extra에 액세스했는데 이 방식은 권장되는 방법은 아니다. (프래그먼트가 다른 레이아웃에 삽입될 수 있고 큰 앱에서는 프래그먼트가 속하는 활동을 추측하기가 훨씬 더 어렵기 때문. 또한 nav_graph를 사용하여 탐색을 실행하고 안전 인수를 사용하면 인텐트가 없으므로 인텐트 extras에 액세스하려고 해도 효과가 없다.)

  1. WordListFragment 클래스 상단에 letterId 속성을 만든다.
private lateinit var letterId: String
  1. onCreate()에서 arguments를 작성한다.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}
  1. recyclerView adapter를 설정하는 코드를 수정한다.
// 이 코드를
recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

// 이렇게
recyclerView.adapter = WordAdapter(letterId, requireContext())

 

DetailActivity의 내용도 WordListFragment로 비슷한 과정으로 옮긴다

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
    }
}

0개의 댓글