본문 바로가기

책 요약하기/이것이 안드로이드다

#5-3. 프래그먼트 2021-03-12

3. 액티비티의 조각 프래그먼트 다루기

화면을 분할해서 독립적인 코드로 구성할 수 있게 도와주는 것이 프래그먼트(Fragment) 입니다. 가령 프래그먼트는 서로 다른 크기의 화면을 가진 기기(태블릿, 스마트폰 등)에서 하나의 액티비티로 서로 다른 레이아웃을 구성할 수 있도록 설계되었습니다. 목록 프래그먼트와 상세 프래그먼트가 있을 때 태블릿과 같은 큰 화면에서는 두 프래그먼트를 한 화면에 표시하고, 스마트폰처럼 작은 화면에서는 먼저 목록 프래그먼트만 표시한 후 목록을 클랙하면 상세가 나타나는 구조입니다.

 

프래그먼트를 활용한 구조

  1. 한 번에 1개의 프래그먼트가 화면에 나타나는 형태는 프래그먼트 여러 개를 미리 만들어두고 탭 메뉴나 스와이프(Swipe)로 화면 간 이동을 할 때 사용됩니다.
  2. 한 번에 여러 개의 프래그먼트가 동시에 화면에 나타나는 형태로 테블릿과 같은 대형 화면을 가진 디바이스에서 메뉴와 뷰를 함께 나타내거나 여러 개의 섹션을 모듈화한 후 한 화면에 나타낼 때 사용합니다.
화면(뷰)이 하나만 필요할 때는 프래그먼트를 사용하지 않습니다.
프래그먼트는 2개 이상의 화면을 빠르게 이동한다던지 탭으로 구성된 화면의 자연스러운 움직임을 구현할 때 주로 사용됩니다. 따라서 1개의 액티비티에 1개의 뷰만 필요한 구조라면 프래그먼트를 사용하지 않는 것이 좋습니다.

프래그먼트 또한 하나의 모듈로써 동작하기 때문에 생성에 따른 자원이 낭비되고, 액티비티와 별개의 생명 주기를 갖고 있어서 상황에 따라 2개의 생명 주기를 관리하는 코드를 액티비티와 프래그먼트 양쪽에 작성해야 할 수도 있습니다.

 

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

목록 프래그먼트 만들기

1. java디렉터리 밑에 있는 패키지명을 선택하여 마우스 우클릭하여 [New] - [Fragment] - [Fragment(Blank)] Fragment name을 ListFragment로 하겠습니다.

2. ListFragment.kt은 onCreateView()는 리사이클러뷰의 onCreateHolder()메서드처럼 동작합니다. 액티비티가 프래그먼트를 요청하면 onCreateView() 메서드를 통해 뷰를 만들어서 보여줍니다. inflate 메서드는 리사이클러뷰에서와 동일하게 동작합니다. 다음은 프래그먼트 생성 후 ListFragment.kt의 class 부분 기본 코드입니다.

// ListFragment.kt의 class 부분

class ListFragment : Fragment() {
    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의 파라미터
inflater: 레이아웃 파일을 로드하기 위한 레이아웃 인플레이터를 기본으로 제공.
container: 프래그먼트 레이아웃이 배치되는 부모 레이아웃(액티비티의 레이아웃)
savedInstanceState: 상태값 저장을 위한 보조 도구, 액티비티의 onCreate의 파라미터와 동일하게 동작.
트랜잭션이란?
여러 개의 의존성이 있는 동작을 한 번에 실행할 때 중간에 하나라도 잘못되면 모든 동작을 복구하는 하나의 작업 단위입니다.
예로 들어 네트워크로 연결된 2개의 은행 시스템을 두고 전송하는 은행에서 100만 원을 전송했는데, 수신받는 은행에서 100만 원을 수신하지 못했다면 전송하는 은행에서 취소해야 합니다. 이때 전송과 수신하는 은행을 하나의 트랜잭션으로 묶어서 어느 한쪽에 문제가 있으면 트랜잭션 내부에서 처리된 모든 작업을 취소하게 됩니다.
프래그먼트를 화면에 삽입하는 메서드
프래그먼트를 화면에 삽입하기 위해서는 아래와 같은 메서드를 사용할 수 있습니다.
.add(레이아웃, 프래그먼트): 프래그먼트를 레이아웃에 추가합니다.
.replace(레이아웃, 프래그먼트): 레이아웃에 삽입되어 있는 프래그먼트와 교체합니다.
.remove(프래그먼트): 지정한 프래그먼트를 제거합니다.

 

[activity_main.xml, fragment_list.xml]

(따로 코드로 작성하지 않고 디자인 편집으로 텍스트 뷰와 프레임 레이아웃을 추가하고 프레임 레이아웃의 id를 frameLayout으로 설정하고 fragment_list.xml파일 역시 디자인 편집으로 텍스트뷰와 버튼(id: btnNext text: NEXT)을 추가했습니다.)

 

[MainActivity.kt]

 

레이아웃에서 프래그먼트 추가하기

fragment 컨테이너를 사용하면 소스 코드를 거치지 않고 레이아웃 파일에서도 위젯처럼 프래그먼트를 추가할 수 있습니다. 하나의 프래그먼트를 화면 전환 없이 사용하면 소스 코드에서 추가하는 것보다 레이아웃에서 추가하는 것이 더 효율적입니다.
레이아웃 파일에서 프래그먼트를 추가하기 위해서는 메인 액티비티의 레이아웃에 추가했던 'FrameLayout'을 'Fragment' 로 변경해야 합니다.

<!-- FrameLayout
// 중략
</FrameLayout> --!>
XML 태그 살펴보기
XMl 파일을 정의하는 첫 줄<?xml version="1.0" encoding="utf-8"?>을 제외하고 보통 XML 태그는 쌍으로 구성됩니다. 예외적으로 태그 내부에 다른 태그를 입력할 필요가 없을 경우는 홀 태기 형태로 사용합니다.

시작 태그 : <태그 이름> 괄호 안에 태그 이름을 입력합니다.
종료 태그 : </태그 이름> 괄호 안에 태그 이름을 입력하고 앞에 /를 붙입니다.
홀 태그 : <태그 이름/> 괄호 안에 태그 이름을 입력하고 끝에 /를 붙입니다.

 

3. 다시 디자인 모드로 변경하여 frameLayout -> fragment로 변경

(Fragment의 id를 FragmentLayout로 설정)


4. MainActivity.kt 파일의 setFragment() 메서드 주석 처리

(프레임 레이아웃을 사용하여 코틀린 파일로 동작 시킬때와 프래그먼트를 XML에 사용했을 때 동일하게 동작)

 

3.2 프래그먼트 화면 전환

새로운 Detail 프래그먼트를 하나 만들고, 앞에서 만든 List 프래그먼트 안의 [Next] 버튼이 클릭되면 Detail 프래그먼트 화면으로 전환하는 과정으로 예제를 만들어보겠습니다.

1. DetailFragment 프래그먼트를 생성합니다.

 

2. fragment_detail.xml 파일을 열고 FrameLayout -> ConstranintLayout으로 변경 (id : detailFragment)

 

3. id = btnBack인 버튼 1개와 텍스트를 추가

 

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

앞서 만든 fragment_list.xml의 [NEXT]버튼 클릭시 해당 Detail 프래그먼트로 이동하고 다시 Detail 프래그먼트의 [BACK] 버튼을 클릭하면 목록 프래그먼트로 돌아가는 코드를 작성합니다.

4. MainActivity.kt에 goDetail() 메서드 작성합니다.

addToBackStack으로 프래그먼트 트랜잭션을 백스택에 담을 수 있습니다.
addToBackStack을 사용하면 프래그먼트를 삽입하기 위해 사용되는 트랜잭션을 마치 하나의 액티비티처럼 백스택에 담아둘 수 있습니다. 
주의할 점은 개별 프래그먼트가 스택에 담기는 것이 아니라 트랜잭션 전체가 담기기 때문에 add나 replace에 상관없이 해당 트랜잭션 전체가 제거됩니다.

 

5. MainActivity.kt에 [BACK]버튼 클릭시 기존의 프래그먼트로 돌아가는 코드를 작성합니다. (goBack() 메서드 작성)

상세 프래그먼트에서 목록 프래그먼트로 돌아가는 코든느 트랜잭션 없이 뒤로가기로 간단하게 처리할 수 있으므로 메서드의 이름을 goBack() 으로 작성합니다. onBackPressed()는 뒤로가기가 필요할 때 액티비티에서 사용 가능한 기본 메서드 입니다.

 

6. ListFragment.kt코드 수정 [NEXT] 버튼의 클릭리스너를 작성합니다. 프래그먼트의 버튼으로 사용자의 클릭이 전달되면 클릭리스너 코드 블록 안에서 메인 액티비티의 goDetail()메서드를 호출하는 형태입니다. 일반적으로 프래그먼트의 생명 주기 메서드 중에 onAttach()를 통해 코드를 전달받는 것이 가장 일반적인 방법입니다.

인터페이스를 사용하지 않고 액티비티를 직접 변수에 담아 사용합니다.
프래그먼트를 만들면 자동으로 생성되는 기본 코드에서는 인터페이스를 통해 의존성을 제거하는 코드로 작성되어 있지만, 처음 공부할 때는 이런 코드가 오히려 이해하는 데 방해가 될 수 있어서 액티비티를 그대로 사용하는 것을 권장합니다.

 

(ListFragment클래스에 mainActivity 프로퍼티를 추가하고, onAttach()메서드를 추가하고, onCreateView()메서드를 다음과 같이 수정합니다.)

[실행 시]

[NEXT] 버튼을 클릭하면 Detail 프래그먼트가 화면에 겹쳐서 보입니다. 프래그먼트는 하나의 레이아웃에 한 층씩 쌓이는 형태라서 기본 배경색을 설정하지 않으면 화면이 중첩된 채로 그려집니다.

 

7. fragment_detail.xml의 최상위 컴포넌트(여기서는 ConstraintLayout)의 background속성을 변경하고, (목록 프래그먼트의 클릭 속성이 있으면 예기치 못한 이벤트가 발생 할 수 있기 때문에) 컨스트레인트 레이아웃의 clickable 속성을  'true'로 변경합니다.

 

8. 마지막으로 DetailFragment.kt의 [BACK] 버튼 리스너를 등록하는 코드를 작성합니다.

 

3.3 프래그먼트 값 전달하기

프래그먼트로 값을 전달하는 방법에는 크게 두 가지가 있습니다. 하나는 프래그먼트 생성 시에 값을 전달하는 것이고 또 하난느 이미 생성되어 있는 프래그먼트에 값을 전달하는 것입니다.

 

프래그 먼트 생성시 값 전달하기

안드로이드에서는 프래그먼트를 생성하면사 값을 전달하는 방법으로 arguments를 제공합니다. arguments는 프래그먼트의 기본 프로퍼티이기 때문에 선언 없이 사용할 수 있습니다. arguments를 통해 전달된 값은 생성된 프래그먼트에서 arguments로 꺼낼 수 있습니다.

 

// 액티비티 -> 프래그먼트 값 전달

// 프래그먼트 생성
var listFragment: Fragment = ListFragment()

// Bundle 생성 후 값을 저장 (Intent와 유사)
var bundle = Bundle()
bundle.putString("key1", "List Fragment")
bundle.putString("key2", 20210314)

// 프래그먼트의 arguments에 bundle저장
listFragment.arguments = bundle

// 프래그먼트 매니저를 통해서 프래그먼트를 삽입하면 값이 전달
var transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.frameLayout, listFragment)
transaction.commit()


override fun onCreateView(inflate: LayoutInflater, container: ViewGroup?,
			savedInstanceState: Bundle?): View? {
    // 값 받음
    val title = arguments?.getString("key1")
    val value = arguments?.getInt("key2")
}

 

프래그먼트에서 프래그먼트로 값 전달

프래그먼트끼리 값을 주고받기 위해서는 프래그먼트를 생성한 액티비티에 값을 전달할 메서드를 미리 생성합니다.

// Fragment A

fun throwValue() {
	activity.passValue(값)
}



// 액티비티
fun passValue(파라미터) {
	fragmentB.receiveValue(파라미터)
}



// Fragment B
fun receiveValue(파라미터) {
	// do something
}

 

3.4 프래그먼트의 생명 주기 관리

생성 주기 메서드 (액티비티가 화면에 계속 나타나고 있는 상태에서는 onAttach() 메서드부터 onResume()까지의 메서드가 모두 한번에 호출됩니다.)

생성 주기 메서드 내용
onAttach() 프래그먼트 매니저를 통해 액티비티에 프래그먼트가 추가되고 commit 되는 순간 호출됩니다. 액티비티 소스 코드에서 var fragment = Fragment() 형태로 생성자를 호출하는 순간에는 호출되지 않습니다.
파라미터로 전달되는 Context를 저장해 놓고 사용하거나 또는 Context로부터 상위 액티비티를 꺼내서 사용합니다. 객체지향의 설계구조로 인해 onAttach()를 통해 넘어오는 Context에서만 상위 액티비티를 꺼낼 수 있습니다.
API레벨 23 이전에서는 onAttach() 메서드의 파라미터로 액티비티를 사용할 수 있었지만 23이상 부터는 Context만 받도록 변경되었습니다.
onCreate() 프래그먼트가 생성됨과 동시에 호출됩니다. 사용자가 인터페이스인 뷰와 관련된 것을 제외한 프래그먼트 자원(주로 변수)을 초기화할때 사용합니다.
onCreateView(0 사용자 인터페이스와 관련된 뷰를 초기화하기 위해 사용합니다.
onStart() 액티비티의 startActivity로 새로운 액티비티를 호출하는 것처럼 프래그먼트가 새로 add되거나 화면에서 사라졌다가 다시 나타나면 onCreateView()는 호출되지 않고 onStart()만 호출됩니다. 주로 화면 생성 후에 화면에 입력될 값을 초기화하는 용도로 사용합니다.
onResume() onStart()와 같은 용도로 사용됩니다. 다른 점은 소멸 주기 메서드가 onPause() 상태에서 멈췄을 때 (현재 프래그먼트의 일부가 가려지지 않았을 때)는 onStart()를 거치지 않고 onResume()이 바로 호출된다는 점입니다.

 

소멸 주기 메서드(현재 프래그먼트 위로 새로운 프래그먼트가 add되거나 현재 프래그먼트를 제거하면 소멸 주기와 관련된 메서드들이 순차적으로 호출됩니다.)

소멸 주기 메서드 내용
onPause() 현재 프래그먼트가 화면에서 사라지면 호출됩니다. 주로 동영상 플레이어를 일시정지한다든가 현재 작업을 잠시 멈추는 용도로 사용됩니다.
onStop() onPause()와 다른 점은 현재 프래그먼트가 화면에 일부분이라도 보이면 onStop()은 호출되지 않습니다. 예를 들어 add되는 새로운 프래그먼트가 반투명하면 현재 프래그먼트의 생명 주기 메서드는 onPause()까지만 호출됩니다. 동영상 플레이어를 예로 든다면 일시정지가 아닌 정지를 하는 용도로 사용됩니다.
onDestroyView() 뷰의 초기화를 해제하는 용도로 사용됩니다. 이 메서드가 호출된 후에 생성 주기 메서드인 onCreateView()에서 인플레이터로 생성한 View가 모두 소멸됩니다.
onDestroy() 액티비티에는 아직 남아있지만 프래그먼트 자체는 소멸됩니다. 프래그먼트에 연결된 모든 자원을 해제하는 용도로 사용됩니다.
onDetach() 액티비티에서 연결이 해제됩니다.

 

 

- 미니 퀴즈 5-3 -

1. 프래그먼트에서 부모 액티비티를 전달해주는 생명 주기 메서드는 무엇인가요?

답 : onAttach(context: Context) 메서드

 

2. 프래그먼트에서 버튼을 사용할 경우 어떤 생명 주기 메서드에서 클릭리스너를 사용해야 하나요?

답 : onCreateView(inflater: LayoutInfater, container: ViewGroup?, savedInstanceState: Bundle?) : View?

 

3. 하나의 액티비티에서 목록과 상세 2개의 프래그먼트를 사용할 때, 목록에서 상세로 화면 전환을 하면 2개의 프래그먼트가 겹치게 됩니다. 이때 사용자가 클릭을 하면 상세 화면을 뚫고 목록 화면에 이벤트가 전달되는데 이를 막기 위해서 어떤 속성값을 어떻게 바꿔야 하나요?

답 : 상세 화면의 레이아웃 백그라운드 속성을 입력하고, 레이아웃 클릭 속성을 true로 해줍니다.

 

4. 화면을 분할해서 독립적인 코드로 구성할 수 있게 도와주는 것은 무엇입니까?

답 : 프래그먼트