/* 본 게시물은 '이것이 안드로이드다 with Kotlin | 고돈호 지음' 의 내용을 토대로 작성되었습니다. */
#Fragment
1. Fragment 란?
안드로이드의 액티비티는 화면을 표현하기 위한 기본 단위이다. 액티비티를 구성하다 보면 화면이 너무 복잡하거나 또는 코드의 양이 너무 많아졌거나 하는 이유로 화면의 부위별로 따로 동작시키고 싶을 때가 있다. 그럴 때 각각의 화면을 분할해서 독립적인 코드로 구성할 수 있게 도와주는 것이 프래그먼트이다.
프래그먼트는 서로 다른 크기의 화면을 가진 기기에서 하나의 액티비티로 서로 다른 레이아웃을 구성할 수 있도록 설계되었다. 목록 프래그먼트와 상세 프래그먼트가 있을 때 태블릿과 같은 큰 화면에서는 두 프래그먼트를 한 화면에 표시하고, 스마트폰처럼 작은 화면에서는 먼저 목록 프래그먼트만 표시한 후 목록을 클릭하면 상세가 나타나는 구조이다.
이렇게 프래그먼트를 사용하면 하나의 액티비티로 조건에 따라 서로 다른 화면 구성을 만들 수 있다.
구글의 설계 의도는 앞의 구조처럼 사용하는 것이지만, 실제 개발할 때에는 태블릿 환경을 고려하기 보다는 다음과 같은 구조로 더 많이 사용된다.
- 한 화면에 프래그먼트 1개 : 한 번에 1개의 프래그먼트가 화면에 나타나는 형태로 프래그먼트 여러 개를 미리 만들어두고 탭 메뉴나 스와이프(swipe)로 화면 간 이동을 할 때 사용한다.
- 한 화면에 프래그먼트 여러 개 : 한 번에 여러 개의 프래그먼트가 동시에 화면에 나타나는 형태로 태블릿과 같은 대형 화면을 가진 디바이스에서 메뉴와 뷰를 함께 나타내거나 여러 개의 섹션을 모듈화한 후 한 화면에 나타낼 때 사용한다.
화면(뷰)이 하나만 필요할 때는 프래그먼트를 사용하지 않는다.
프래그먼트는 2개 이상의 화면을 빠르게 이동한다던지 탭으로 구성된 화면의 자연스러운 움직임을 구현할 때 주로 사용된다. 따라서 1개의 액티비티에 1개의 뷰만 필요한 구조라면 프래그먼트를 사용하지 않는 것이 바람직하다.
프래그먼트 또한 하나의 모듈로써 동작하기 때문에 생성에 따른 자원이 낭비되고, 액티비티와 별개의 생명 주기를 갖고 있어서 상황에 따라 2개의 생명 주기를 관리하는 코드를 액티비티와 프래그먼트 양쪽에 작성해야 할 수도 있다.
2. 프래그먼트를 만들어 액티비티에 추가 / 액티비티에서 프래그먼트로 데이터 전송
프래그먼트는 단독으로 사용되지 않고 액티비티의 일부로 사용된다. 이번에는 프래그먼트를 액티비티에 추가하는 방법과 액티비티에서 프래그먼트로 데이터를 전송하는 방법을 알아보자. 이번 예제에서는 번들을 이용해 목록을 보여주는 프래그먼트를 만들어보겠다.
1. MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val listFragment: ListFragment = ListFragment() //ListFragment() 객체를 listFragment 변수에 넣는다.
//액티비티 -> Fragment 데이터 보내기
val bundle = Bundle() //Intent에 값을 담을 때와 동일한 구조이다.
bundle.putString("key1", "ListFragment")
bundle.putInt("key2", 20210604)
listFragment.arguments = bundle
setFragment()
}
fun setFragment() {
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.frameLayout, listFragment)
transaction.commit()
}
}
}
- listFragment : ListFragment() 객체를 가지고 있는 프로퍼티이다.
- bundle : 번들은 액티비티에서 Fragment로 데이터를 보낼 때 사용한다. 액티비티 -> 액티비티로 데이터를 보낼 때 사용하는 Intent와 동일한 구조이다.
- listFragment.arguments : Fragment.arguments에 번들 값을 저장한다.
- setFragment() : transaction.add을 이용해 ListFragment을 MainActivityUI.FrameLayout에 추가하는 함수이다.
트랜잭션이란?
여러 개의 의존성이 있는 동작을 한 번에 실행할 때 중간에 하나라도 잘못되면 모든 동작을 복구하는 하나의 작업 단위이다.
예를 들어 은행에서 송금한다고 가정해보자. 네트워크로 연결된 2개의 은행 시스템을 두고 전송하는 은행에서 100만 원을 전송했는데, 수신받는 은행에서 100만 원을 수신하지 못했다면 전송하는 은행에서 취소해야 한다.
이 때 전송과 수신하는 은행을 하나의 트랜잭션으로 묶어서 어느 한 쪽에서 문제가 있으면 트랜잭션 내부에서 처리된 모든 작업을 취소하게 된다.
프래그먼트를 화면에 삽입하는 메서드
· add(레이아웃, 프래그먼트) : 프래그먼트를 레이아웃에 추가한다.
· replace(레이아웃, 프래그먼트) : 레이아웃에 삽입되어 있는 프래그먼트와 교체한다.
· remove(프래그먼트) : 지정한 프래그먼트를 제거한다.
2. ListFragment
class ListFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_list, container, false)
val title = arguments?.getString("key1")
val value = "${arguments?.getInt("key2")}"
view.keyText1?.text = title
view.keyText2?.text = value
return view
}
}
- onCreateView() : 액티비티가 프래그먼트를 요청할 때 뷰를 만들어서 보내는 콜백함수이다. 이 콜백 함수는 리사이클러뷰의 onCreateViewHolder() 메서드처럼 동작한다.
- inflate() : 이 메서드는 리사이클러뷰에서와 동일하게 동작한다. fragment_list를 컨테이너 형식으로 만들어준다.
- title / value : 액티비티에서 번들로 보낸 값을 arguments로 접근해서 데이터를 받는다.
onCreateView의 파라미터
· inflater : 레이아웃 파일을 로드하기 위한 레이아웃 인플레이터를 기본으로 제공한다.
· container : 프래그먼트 레이아웃이 배치되는 부모 레이아웃(액티비티 레이아웃이다)이다.
· savedInstanseState : 상태값을 저장을 위한 보조 도구. 액티비티의 onCreate의 파라미터와 동일하게 동작한다.
3. 프래그먼트 화면 전환
새로운 Detail 프래그먼트를 하나 만들고, 앞에서 만든 List 프래그먼트 안의 [Next] 버튼이 클릭되면 Detail 프래그먼트 화면으로 전환하는 과정을 알아보자.
1. ListFragment
class ListFragment : Fragment() {
var mainActivity: MainActivity? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_list, container, false)
view.btnNext.setOnClickListener { mainActivity?.goDetail() }
//..
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity = context as MainActivity
}
}
- fragment_list.btnNext를 클릭시 MainActivity.goDetail() 함수를 실행한다.
- onAttach() : MainActivity의 content 값을 가져오는 함수이다. MainActivity.goDetail()을 실행하려면 MainActivity의 context가 필요하다.
2. MainActivity.goDetail()
fun goDetail() {
val detailFragment = DetailFragment()
val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.frameLayout, detailFragment)
transaction.addToBackStack("detail")
transaction.commit()
}
fun goBack() {
onBackPressed()
}
- transaction.addToBackStack() : 스마트폰의 뒤로가기 버튼을 사용할 수 있다.
- onBackPressed() : 액티비티 기본 메서드로 뒤로가기가 필요할 때 액티비티에서 사용할 수 있는 기본 메서드이다.
3. DetailFragment
class DetailFragment : Fragment() {
var mainActivity: MainActivity? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_detail, container, false)
view.btnBack.setOnClickListener { mainActivity?.goBack() }
return view
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity = context as MainActivity
}
}
※ 주의할 점
1. ListFragment에서 Next 버튼을 클릭 시 DetailFragment를 실행하게 되는데 ListFragment는 종료된 상태가 아니다. 기본 배경색을 설정하지 않으면 프래그먼트는 하나의 레이아웃에 한 층씩 쌓이는 형태라서 화면이 중첩된 채로 그려진다.
-> activity_detail 에 background를 설정한다.
2. 프래그먼트가 중첩되었을 때 아래쪽 프래그먼트와 버튼과 같은 클릭가능한 요소가 있을 경우 위쪽 프래그먼트를 통과해서 클릭이 될 수 있다. 그래서 예상치 못한 이벤트가 발생할 수 있는데 이를 방지하기 위해서 컴포너트 트리의 컨스턴트레인트 레이아웃의 clickable 속성을 체크해서 'true'로 변경한다.
3. 프래그먼트로 값 전달하기
위에서 Bundle로 액티비티에서 Fragment가 생성될 때 데이터를 주는 방법을 소개했다. 그 외에 방법은 없을까?
그리고 이미 생성되어 화면에 보이는 프래그먼트에 값을 전달하는 방법이나 프래그먼트로 값을 전달하는 방법은 없을까? 당연히 있다.
1. 액티비티에서 프래그먼트로 데이터 전달하는 방법
관련글
[액티비티에서 프래그먼트로 데이터 전달하는 방법] : https://jwsoft91.tistory.com/216
[안드로이드 코틀린 : Activity에서 Fragment에 데이터 보내기] : https://juahnpop.tistory.com/225
1. onAttach()에서 Context 파라미터로 Activity 객체 생성하여 전달받는 방법.
2. Bundle을 사용하는 방법
3. viewModel()을 공유하는 방법
4. ViewBinding을 이용한 방법 -> Bundle 사용 ( kotlin-android-extensions 지원 중단으로 이 방법 추천)
2. 생성되어 화면에 보이는 프래그먼트에 값 전달하기
액티비티에서 이미 생성되어 화면에 보이는 프래그먼트로 값을 전달하기 위해서는 프래그먼트에 메서드를 정의하고 직접 호출하면 된다. 예를 들어보자.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//...
//fragment의 값을 설정할 때 사용
fragment.setValue(값)
}
}
class fragment : Fragment() {
//...
fun setValue(값) {
...
}
}
3. 프래그먼트에서 프래그먼트로 값 전달하기
관련글
[프래그먼트 간 데이터 전달]URL : https://developer.android.com/training/basics/fragments/pass-data-between?hl=ko
FragmentManager가 프래그먼트 결과의 중앙 저장소 역할을 한다. 프래그먼트가 서로 직접 참조할 필요 없이 프래그먼트 결과를 설정하고 결과를 수신하여 개별 프래그먼트 간에 통신할 수 있다.
// <FragmentManager로 데이터를 보내는 프래그먼트>
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setResult("requestKey", bundleOf("bundleKey" to result))
}
// <FragmentManager에서 데이터를 받는 프래그먼트>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setResultListener("requestKey") { key, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result...
}
}
4. Fragment의 생명 주기 관리
프래그먼트는 액티비티와 마찬가지로 화면에 보이는 것을 기준으로 생명 주기 메서드를 가지는데, 생성에 관련된 5개와 소멸에 관련된 5개를 가지고 있다.
생명 주기 메서드
생성과 관련된 5개의 생명 주기 메서드가 있찌만 프래그먼트를 포함하고 있는 액티비티가 화면에 계속 나타나고 있는 상태에서는 onAttach()부터 onResume()까지의 메서드가 모두 한 번에 호출된다. 개발자는 상황에 따라 필요한 생명 주기 메서드에 코드를 넣어 사용할 수 있다.
①onAttach()
프래그먼트 매니저를 통해 액티비티에 프래그먼트가 추가되고 commit 되는 순간 호출된다. 액티비티 소스 코드에서 var fragment = Fragment() 형태로 생성자를 호출하는 순간에는 호출되지 않는다.
파라미터로 전달되는 Context를 저장해 놓고 사용하거나 또는 Context로부터 상위 액티비티를 꺼내서 사용한다. 객체지향의 설계구조로 인해 onAttach()를 통해 넘어오는 Context에서만 상위 액티비티를 꺼낼 수 있다.
②onCreate()
프래그먼트가 생성됨과 동시에 호출된다. 사용자 인터페이스인 뷰와 관련된 것을 제외한 프래그먼트 자원(주로 변수)를 초기화할 때 생성된다.
③onCreateView()
사용자 인터페이스와 관련된 뷰를 초기화하기 위해 사용된다.
④onStart()
액티비티의 startActivity로 새로운 액티비티를 호출하는 것처럼 프래그먼트가 새로 add되거나 화면에서 사라졌다가 다시 나타나면 onCreateView()는 호출되지 않고 onStart()만 호출된다. 주로 화면 생성 후에 화면에 입력될 값을 초기화하는 용도로 사용된다.
⑤onResume()
onStart()와 같은 용도로 사용된다. 다른 점은 소멸 주기 메서드가 onPause() 상태에서 멈췄을 떄(현재 프래그먼트의 일부가 가려지지 않았을 때)는 onStart()를 거치지 않고 onResume()이 바로 호출된다.
소멸 주기 메서드
현재 프래그먼트 위로 새로운 프래그먼트가 add 되거나 현재 프래그먼트를 제거하면 소멸 주기와 관련된 메서드들이 순차적으로 호출된다.
①onPause
현재 프래그먼트가 화면에서 사라지면 호출된다. 주로 동영상 플레이어를 일시정지한다든가 현재 작업을 잠시 멈추는 용도로 사용된다.
②onStop()
onPause()와 다른 점은 현재 프래그먼트가 화면에 일부분이라도 보이면 onStop()은 호출되지 않는다. 예를 들어 add되거나 새로운 프래그먼트가 반투명하면 현재 프래그먼트의 생명 주기 메서드는 onPause()까지만 호출된다. 동영상 플레이어를 예로 든다면 일시정지가 아닌 정지를 하는 용도로 사용된다.
③onDestoryView()
뷰의 초기화를 해제하는 용도로 사용된다. 이 메서드가 호출된 후에 생성 주기 메서드인 onCreateView()에서 인플레이터로 생성한 View가 모두 소멸된다.
④onDestory()
액티비티에는 아직 남아있지만 프래그먼트 자체는 소멸된다. 프래그먼트에 연결된 모든 자원을 해제하는 용도로 사용된다.
⑤onDetach()
액티비티에서 연결이 해제된다.