[Android] 5(3). 프래그먼트 (프래그먼트 생성과 전환)

7 minute read


프래그먼트에 관한 포스팅은 3개로 나누어 진행합니다.

첫번째 포스팅: 프래그먼트 생성과 전환

두번째 포스팅: 프래그먼트로 값 전달하기

세번째 포스팅: 프래그먼트의 생명주기 관리


프래그먼트 (프래그먼트 생성과 전환)

프래그먼트는 액티비티가 너무 복잡해지지 않도록 화면을 각각 분할해서 독립적인 코드로 구성하여 2개 이상의 화면을 빠르게 이동하거나 탭으로 구성된 화면의 자연스러운 움직임을 구현할 때 주로 사용됩니다.

또한 프래그먼트는 서로 다른 크기의 화면을 가진 기기(태블릿과 스마트폰 등)에서 하나의 액티비티로 서로 다른 레이아웃을 구성할 수 있도록 설계되었습니다. 예를 들어 태블릿에서는 목록 프래그먼트와 상세 프래그먼트를 동시에 보여준다면, 스마트폰에서는 목록 프래그먼트만 먼저 보여주고 목록을 클릭하면 상세 프래그먼트가 나타나는 구조입니다.

이렇게 프래그먼트를 사용하면 하나의 액티비티로 조건에 따라 서로 다른 화면 구성을 만들 수 있습니다.


프래그먼트를 만들어 액티비티에 추가하기


목록 프래그먼트 만들기

1. 프래그먼트 생성

[java] - 패키지명 우클릭 - [New] - [Fragment] - [Fragment (Blank)] 를 통해 프래그먼트 소스파일과 xml 파일을 만들 수 있습니다. (여기서는 이름을 ListFragment라고 하겠습니다)


2. 프래그먼트 kt 파일: 필요없는 부분 지우기

ListFragment 클래스 안에는 다양한 변수들과 메서드들이 있는데, 프래그먼트의 생성과 전환에 있어 필요없는 부분들은 지우고 시작합니다.

package kr.co.hanbit.fragment

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kr.co.hanbit.fragment.databinding.FragmentListBinding

class ListFragment : Fragment() {

    // 액티비티가 프래그먼트를 요청하면 onCreateView() 메서드를 통해 뷰를 만들어서 보여줌(리사이클러뷰의 onCreateViewHolder 메서드와 유사)
    // 파라미터 1: 레이아웃 파일을 로드하기 위한 레이아웃 인플레이터를 기본 제공
    // 파라미터 2: 프래그먼트 레이아웃이 배치되는 부모 레이아웃 (액티비티의 레이아웃)
    // 상태값 저장을 위한 보조 도구. 액티비티의 onCreate의 파라미터와 동일.
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        // 리사이클러뷰에서와 동일하게 동작
        return inflater.inflate(R.layout.fragment_list, container, false)
    }
}

onCreateView 메서드는 리사이클러 뷰의 onCreateViewHolder 메서드와 비슷하게 동작하는데, 액티비티가 프래그먼트를 요청하면 onCreateView 메서드를 통해 뷰를 만들어서 보여줍니다.

onCreateView의 파라미터

  • inflater: 레이아웃 파일을 로드하기 위한 인플레이터를 기본으로 제공합니다.
  • container: 프래그먼트 레이아웃이 배치되는 부모 레이아웃 (액티비티의 레이아웃)
  • savedInstanceState: 상태 값 저장을 위한 보조 도구. 액티비티의 onCreate의 파라미터와 동일.

inflate 메서드는 리사이클러 뷰의 inflate 뷰와 동일합니다. inflate 메서드는 나중에 binding을 사용하는 코드로 변경할 것입니다.


3. 프래그먼트 xml 파일: 프래그먼트 레이아웃 작성

fragment_list.xml 파일에서 최상단 레이아웃을 ConstraintLayout으로 변경합니다.

추후에 프래그먼트 화면 전환을 위해 프래그먼트 상단에 버튼을 하나 가져다 놓고 id는 btnNext, text는 Next로 지정합니다. 또한 액티비티와의 영역을 구분하기 위해 background를 ‘#0000ff’ 로 지정합니다.



액티비티에 프래그먼트 추가하기

프래그먼트를 모두 구성했으면 액티비티와 연결합니다. **프래그먼트는 기본적으로 하나의 뷰이기 때문에 액티비티 안에 뷰를 삽입할 수 있는 레이아웃을 준비해야 합니다. **

프래그먼트를 삽입하기 위한 전용 레이아웃으로 [Layout]의 FrameLayout과 [Container]의 FragmentContainerView가 있는데, 아래와 같이 사용합니다.

  • FrameLayout: 화면 전환이 필요할 때
  • FragmentContainerView: 화면 전환 없이 프래그먼트 하나만 화면에 표시할 때

여기서는 FrameLayout을 사용합니다.


4. 액티비티에 프레임 레이아웃 가져다 놓기

activity_main.xml 파일에 기본으로 있는 텍스트뷰의 text 속성을 Activity로 바꾸고 화면 상단에 가져다 놓습니다. 컨스트레인트는 위와 양옆을 연결하고 위쪽 마진으로 16dp를 설정합니다.

그 다음으로 이제 프레임 레이아웃을 화면에 가져다 놓습니다. 컨스트레인트를 네방향 모두 연결(위쪽 방향은 텍스트뷰의 밑에 연결)하고 마찬가지로 위쪽 마진으로 16dp를 설정합니다. layout_width와 layout_height는 0dp (match constraint)로 설정합니다.


5. 레이아웃과 프래그먼트 연결하기

액티비티에서 레이아웃과 프래그먼트를 연결하는 코드를 작성합니다.

MainActivity.kt 파일에서 setFragment 메서드를 작성합니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setFragment()
    }
    // 액티비티에 프래그먼트를 삽입하는 메서드
    fun setFragment(){
      
    }
}


액티비티에 프래그먼트를 삽입하기 위해서는 프래그먼트 매니저를 통해 삽입할 레이아웃의 id와 프래그먼트를 지정합니다.

프래그먼트를 삽입하는 과정은 하나의 트랜잭션으로 관리되기 때문에 트랜잭션 매니저를 통해 begin transaction -> add fragment -> commit transaction 순으로 처리됩니다.

트랜잭션: 여러 개의 의존성이 있는 동작을 한 번에 실행할 때 어느 하나라도 잘못되면 모든 동작을 복구하는 하나의 작업 단위


프래그먼트를 레이아웃과 연결하는 과정은 다음 과정으로 이루어집니다.

  1. 우선 앞에서 생성한 ListFragment 인스턴스를 생성합니다.

  2. 액티비티가 가지고 있는 프래그먼트 매니저를 통해 트랜잭션 인스턴스를 생성합니다.

  3. 트랜잭션의 add 메서드로 레이아웃과 프래그먼트를 연결합니다.

  4. commit 메서드로 모든 작업이 정상적으로 완료되었음을 알려주면 작업이 반영됩니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setFragment()
    }
    // 레이아웃과 프래그먼트를 연결하는 메서드
    fun setFragment(){
        val listFragment: ListFragment = ListFragment()
        // 액티비티가 가지고 있는 프래그먼트 매니저를 통해 트랜잭션을 시작하고, 시작한 트랜잭션울 변수에 저장해둡니다.
        val transaction = supportFragmentManager.beginTransaction()
        // 트랜잭션의 add() 메서드로 frameLayout을 id로 가지고 있는 레이아웃에 앞에서 생성한 listFragment를 삽입합니다.
        transaction.add(R.id.frameLayout, listFragment)
        // commit() 메서드로 모든 작업이 정상적으로 처리되었음을 트랜잭션에 알려주면 작업이 반영됩니다.
        transaction.commit()
    }
}


이제 에뮬레이터를 실행하면 다음의 결과를 볼 수 있습니다.

image-20210803185744963



프래그먼트 화면 전환하기


이제 앞의 과정에 이어서 프래그먼트 간 화면 전환을 수행해봅니다.


상세 프래그먼트 만들기

6. 새로운 프래그먼트 만들기

먼저 앞에서 만든 목록 프래그먼트의 버튼을 누르면 전환될 새로운 상세 프래그먼트를 생성합니다.

마찬가지로 [java] - 클래스명 우클릭 - [New] - [Fragment] - [Fragment (Blank)] 으로 새로운 프래그먼트를 생성합니다. (여기서는 프래그먼트명을 DetailFragment로 하겠습니다.)

이후 다음 과정을 진행합니다.

  • fragment_detail.xml 파일에서 최상단 레이아웃을 ‘ConstraintLayout’으로 변경합니다.
  • 프래그먼트 화면 전환을 위해 프래그먼트 상단에 버튼을 하나 가져다 놓고 id는 btnBack, text는 Back으로 지정합니다.
  • 액티비티와의 영역을 구분하기 위해 background를 ‘#ff0000’ 로 지정합니다.
  • ❗ **기존의 프래그먼트 이후에 새롭게 삽입되는 프래그먼트는 clickable 속성을 true로 설정해야 합니다. **
    • 프래그먼트는 기존의 프래그먼트를 대체하는 것이 아닌, 같은 레이아웃 위에 겹쳐서 올라가는 개념이기 때문에, clickable 속성을 설정하여 뒤쪽의 프래그먼트가 원치 않게 클릭되지 않도록 해야 합니다.


메인 액티비티에 두 프래그먼트 연결하기

7. 화면 전환 메서드 생성

먼저 ListFragment에서 DetailFragment로 전환되는 메서드를 생성합니다.

goDetail 메서드를 setFragment 메서드 아래에 작성하여 goDetail 메서드가 호출되면 DetailFragment가 메인 액티비티의 frameLayout에 삽입되도록 합니다.

goDetail 함수는 앞의 setFragment 함수와 상당히 유사하지만, 한 가지 다른 점이 있습니다.

// DetailFragment를 생성해서 메인 액티비티의 frameLayout에 삽입 -> 화면 전환
// 버튼은 프래그먼트에 있지만, 프래그먼트를 메인 액티비티에서 생성하고 프래그먼트를 담는 레이아웃도 메인 액티비티에 있으므로
// 화면 전환 코드는 메인 액티비티에 작성함.
fun goDetail(){
    // 프래그먼트 인스턴스 저장
    val detailFragment = DetailFragment()
    // 프래그먼트를 액티비티에 삽입
    val transaction = supportFragmentManager.beginTransaction()
    transaction.add(R.id.frameLayout, detailFragment)
    // addToBackStack을 사용하지 않은 채로 뒤로가기를 하면 액티비티가 종료된다.
    // addToBackStack을 사용하면 프래그먼트를 백스택에 담아둘 수 있다. 따라서 스마트폰의 뒤로가기 버튼으로
    // 트랜잭션 전체를 마치 액티비티처럼 제거할 수 있게 된다. 다만, 개별 프래그먼트가 아닌 트랜잭션 전체가
    // 담기기 때문에 add나 replace와 상관없이 해당 트랜잭션 전체가 제거된다.
    transaction.addToBackStack("detail")
    transaction.commit()
}

바로 트랜잭션의 add 메서드와 commit 메서드 사이에 addToBackStack 메서드를 추가하는 것입니다. 위의 코드에서 설명했듯이, 이 메서드를 추가함으로써 뒤로가기를 눌러서 트랜잭션을 종료(액티비티로 귀환)시킬 수 있습니다.


다음으로 DetailFragment에서 ListFragment로 전환되는 goBack 메서드를 정의합니다. 이 메서드는 간단히 onBackPressed 메서드를 호출하는 것으로 구현할 수 있습니다.

// 뒤로가기 함수
fun goBack(){
    // 뒤로가기가 필요할 때 액티비티에서 사용할 수 있는 기본 메서드
    onBackPressed()
}

단, 이 메서드가 올바르게 동작하려면 반드시 addToBackStack 메서드가 먼저 호출되어야 합니다.


8. 프래그먼트에 버튼 리스너 생성

프래그먼트 생성, 레이아웃과 프래그먼트 연결(액티비티에 삽입), 화면 전환 메서드 구현을 모두 마쳤습니다.

이제 마지막으로 버튼을 누르면 적절한 메서드를 호출하여 화면 전환이 일어나도록 클릭 리스너를 구현합니다. 메인 액티비티에 있는 메서드를 사용하기 위해 메인 액티비티를 받아야 하고, 프래그먼트의 버튼을 사용하기 위해 프래그먼트를 바인딩해야 합니다.


먼저 MainActivity.kt 에 작성된 메서드를 호출해야 하기 때문에 액티비티를 전달받는 코드를 먼저 작성합니다.

ListFragment.kt

class ListFragment : Fragment() {
    // 메인 액티비티에 작성된 goDetail 메서드를 호출해야 하므로 MainActivity를 먼저 전달받아야 한다.
    // MainActivity를 담아둘 멤버 변수 선언
    var mainActivity: MainActivity? = null
    ...
}

DetailFragment.kt

class DetailFragment : Fragment() {
    // 메인 액티비티 받아오기 (위와 동일한 효과)
    lateinit var mainActivity: MainActivity


다음으로 코드를 전달받는 onAttach 메서드를 구현합니다. onCreateView 메서드 아래에 마우스 포인터를 놓고 [Ctrl + O]를 눌러서 onAttach 메서드를 선택합니다.

onAttach 메서드를 통해 넘어오는 Context는 부모 전체 액티비티가 담겨있는데, context의 타입이 MainActivity인 것을 확인하고 캐스팅하여 mainActivity 변수에 담아둡니다.

ListFragment.kt

override fun onAttach(context: Context){
    super.onAttach(context)
    // 컨텍스트를 메인 액티비티로 캐스팅
    if (context is MainActivity) mainActivity = context
}

DetailFragment.kt

override fun onAttach(context: Context) {
        super.onAttach(context)
        // 컨텍스트를 메인 액티비티로 캐스팅 (위와 동일한 효과)
        mainActivity = context as MainActivity
    }


이제 마지막으로 프래그먼트의 버튼을 사용할 수 있도록 onCreateView 메서드를 수정합니다.

ListFragment.kt

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    // 리사이클러뷰에서와 동일하게 동작
    /*원본 코드: inflater로 생성한 뷰를 바로 리턴*/
    // return inflater.inflate(R.layout.fragment_list, container, false)
    /*수정 코드: 바인딩으로 생성한 후 레이아웃에 있는 btnNext 버튼에 리스너를 등록한 후에 binding.root 리턴*/
    val binding = FragmentListBinding.inflate(inflater, container, false)
    binding.btnNext.setOnClickListener { mainActivity?.goDetail() }
    return binding.root // 바인딩이 가지고 있는 root view를 반환
    }

DetailFragment.kt

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    // return inflater.inflate(R.layout.fragment_detail, container, false)

    // 바인딩 만들고 리스너 설정하고 뷰를 반환
    val binding = FragmentDetailBinding.inflate(inflater, container, false)
    binding.btnBack.setOnClickListener { mainActivity?.goBack() }
    return binding.root // 바인딩이 가지고 있는 root view를 반환
    }


9. 에뮬레이터 실행해서 결과 확인하기

이제 모든 준비가 끝났습니다. 에뮬레이터를 실행하여 프래그먼트가 전환되는 것을 확인합니다.

image-20210804110947581



정리


  • 프래그먼트는 액티비티와 독립적인 코드로 구성하여 2개 이상의 화면을 빠르게 이도하거나 탭으로 구성된 화면의 자연스러운 움직임을 구현할 때 주로 사용합니다.
  • 프래그먼트의 생성은 다음 과정을 따릅니다.
  • 프래그먼트 생성 ➡ xml 파일 작성
  • 액티비티에 프래그먼트 추가는 다음 과정을 따릅니다.
    • 액티비티에 프레임 레이아웃 가져다 놓기 ➡ 레이아웃과 프래그먼트 연결 (트랜잭션 사용)
  • 프래그먼트 간 화면 전환은 다음 과정을 따릅니다.
    • 다른 프래그먼트 생성 (clickable: true) ➡ 화면 전환 메서드 생성(새로운 프래그먼트 띄우기, 뒤로가기) ➡ 프래그먼트에서 버튼 리스너 생성(메인 액티비티 선언 ➡ onAttach 메서드 구현 ➡ 리스너에서 화면 전환 메서드 호출)

Categories:

Updated:

Leave a comment