기존에 사용하던 잘못된 프래그먼트 관련 사용법을 고쳐보자!!
이번공부를 하며 바보같은 시간을 너무많이 보냈다. 문다빈이 매일 말하는 이창환은 바보야가 진짜 맞는말이다.
어쨋든뭐 평생 징징거릴수도 없고 열심히 정리라도 해야겠다.
오늘의 주제는 프래그먼트이다.
너무 식상할수도 있다 왜 일상적으로 쓰던 프래그먼트가 나올까?
이사건은 솝트 30기 1주차 코드리뷰에서 시작되었다.
문서 라고 나와있는 글의 링크이다
https://developer.android.com/guide/fragments/transactions#add-remove
안드로이드 개발자의 흔한 자살행위 10가지(명주의 의역) 라는 글의 링크이다.
첫번쨰 글인 공식문서를 들어가보면 프래그먼트에서 인스턴스를 직접생성해서 사용하지말고
Class를 이용한 연산을 통해 항상 동일한 매커니즘을 통해 프래그먼트를 받아와서 매번 같은 프래그먼트를 받을수있게 보증된 상황을 만들라고 나와있다.
그래서 기존에 내가 쓰던방식인
private fun initTransactionEvent(){
val signInFragment = SignInFragment()
supportFragmentManager.beginTransaction().add(R.id.sign_container,signInFragment).commit()
}
요렇게 인스턴스를 미리 만들어놓고 그것을 넣어주는 방식이 아닌
private fun initTransactionEvent() {
supportFragmentManager.beginTransaction().add<FollowerFragment>(R.id.container_home)
.commit()
}
이렇게 인스턴스를 만들지 않고 Class를 이용한 방법을 사용하는것이 좋다는것을 들을수 있었다.
근데 왜 이렇게 사용하는것이 좋은가는 또 다른이야기니까 그부분은 안드로이드 개발자의 흔한 자살행위 10가지(명주의 의역)에 나와있었다.
글에 2번내용을 보면
프래그먼트 사용할때 인스턴스를 참조하는것은 참 안좋은 행위라고 나와있다.
내가 기존에 사용하고 있던 방식이 굉장히 큰 문제를 가지고있었다
private fun initTransactionEvent(){
val signInFragment = SignInFragment()
supportFragmentManager.beginTransaction().add(R.id.sign_container,signInFragment).commit()
}
이런식으로 인스턴스를 직접 만들어서 사용한다면
프래그먼트는 기존에 이미 생성되어있는 객체가 있는지 확인을 하지 않고 매번 초기화 될것이다.
이문제를 해결하기 위해 tag를 붙이고 supportFragmentManager.findFragmentByTag() 를 통해 기존에 프래그먼트가 존재하는지 확인하고 쓸수는있다.
val fragment = supportFragmentManager
.findFragmentByTag("myFragment")
?: MyFragment().also { fragment ->
addFragment(fragment, "myFragment")
}
this.myFragment = fragment
이런식으로 프래그먼트를 생성할때 태그를통해 기존에있는지 확인하고 아니라면 새로 생성하는 코드를 거쳐서 항상 같은 프래그먼트를 제공하도록 해야한다.
이렇게 findFragmentByTag() 를 고쳐서 항상 있는 값인지 검사를 해주지 않는다면
프래그먼트 인스턴스들끼리의 중복이나 원치 않게 unattached 될수있고 unattached된 프래그먼트에 접근한다면 크래시가 날것이다.
그래서 만약 인스턴스를 만들어서쓴다면 항상 저렇게 검사코드를 통해 같은 프래그먼트임을 보장하거나 한번 만든 인스턴스를 여기저기서 접근하게되면 안될것이다.
그래서 위에서 말했듯 공식문서 또한 항상 같은 프래그먼트를 제공할수있도록 Class연산자를 통한 접근을 하라고 나와있는것이다.
아니 근데 여기서 실제로 여기저기 적용해볼라고 하다가 드는 생각이 그래서 bundle은 어캐넣어줄껀데? 라는 생각이 들었다.
여기서부터 눈물나는 사연이 펼쳐진다 ...
일단 arguments를 어떻게 넣는가를 이 정보를 알려준 명주에게 물어봤고
arguments 를 받는 함수 오버로딩도 있다는 답변과
자동완성을 이용해서 한번 사용하려는 함수를 한번쯤 쳐보면 필요한것들을 찾을수있다고 조언해줬다.
근데 진짜 이건 항상 해본다면 정말 유용한 팁인것도 같다.(그냥일단 쳐보고 생각하자 항상)
그래서 여기 본다면 bundle를 받는 부분도 나온다.
그래서 3번쨰 replace 를 사용하기위해 봤는데
fragmentClass : Class<out Fragment!> 라는 자료형이 자리잡고 있었는데 이러한 자료형을 처음본 나는 살짝 당황했고 구글링을하기 시작했다.
근데 나의 구글링스킬이 잘못되었는지 엉뚱한데가서 별의별 새로운 공부를 다했다.
일단 생소한 자료형이였던 Class<Out Fragment> 의 행방을 찾기위해 구글문서를 뒤졌고
요거에서 fragmentClass를 어떻게 설명했나 보았다.
그리고 이부분을 찾아 난 이 자료형이 프래그먼트인데 fragment factory 를 거쳐서나온 클래스인 줄 알았다.
그래서 프래그먼트 팩토리를 겁나게 찾아다녔다. (근데 지금 생각해보니 팩토리를 거쳐나온다고 새로운 자료형이 생기는게 말이안되는거 아닌가 역시 돌아버린 능지)
어쨋든 그렇게 찾아본결과
뭐 기타 등등 여러가지를 알게되었는데
프래그먼트로 데이터를 넘겨줄때 그냥 생성자로 넘겨주지 않고 왜 arguments에 bundle 로 넘겨줄까? 이런 의문을 제시해본적이 한번도 없는거같다 역시 모든 지식들은 일단 비판적으로 의심하고 봐야한다...
val index = 1
val newFragment = MyFragment(index)
이렇게 넣어주는 방법은 왜 아예 한번도 생각해본적이없을까?
근데 사실 그냥 생성자로 바로 넘겨준다면 앱이 강제종료 되는현상이 일어난다 그래서 그냥 무지성으로 넣어줄수는 없고 방법이 두가지있는데 그 중 하나가 bundle객체를 arguments로 넘겨주는거였고
한가지가 방금까지말한 생성자로 데이터를 넘겨줄수있는 방법으로Fragment Factory를 이용해서 넣어주는 방법이있다.
근데 여기서 떠오르는거 한가지 있지않는가 싶다 ViewModel 또한 원래는 ViewModel Factory를 통해서 생성자에 무언가를 주입해주는 과정을 거쳤다 물론 공부하기도 했고 그걸 거쳐야한다는걸 알지만 그걸 대체해주는 의존성주입 라이브러리들 koin,hilt등을 쓰면 자동으로 주입을 해주어서 막상 팩토리는 공부할때만 몇번 작성해봤다. 여기서도 마찬가지이다 fragment또한 팩토리를 일일이 만들어줄 필요없이 모듈에다가 갈겨놓고 hilt,koin 이런 라이브러리로 주입해준다면 저절로 라이브러리가 적절하게 넣어주어 편할것이다. 하지만 또 이 상황에서 그걸 하나하나 작성하느니 그냥 bundle을 통해 넘겨주는게 난 제일 편할거같다. 그래도 방법이 있다면 공부하고 넘어가는게 인지상정
과연 팩토리가 어떻게 돌아가는지 알아보자.
프래그먼트 관련 공식문서이다.
https://developer.android.com/reference/android/app/Fragment
우선 여기서 이런 구문이있다
All subclasses of Fragment must include a public no-argument constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore.
뭐 해석을 해보자면 프래그먼트를 상속 받을때는 항상 인자가 없는 생성자를 포함해야한다고 한다.
상황에 맞춰서 다시 생성될때는 인자가 없는 기본생성자를 통해서 프래그먼트를 다시 생성하고 그때 인자가없는 기본생성자가 없다면 runtime exception이 난다고한다.
상황을 살펴보자면
프래그먼트는 액티비티의 라이프 사이클에 종속적이고, 메모리가 부족하다거나 화면이 회전되는 등의 이벤트로 Activity가 재생성 될때 Fragment도 재생성된다 이렇게 re_insrtantiate 되는 상황에서 프래그먼트는 인자가없는 기본생성자로 인스턴스를 생성하게 되는데
당연히 그럼 이 상황에서 내가 넣어준 오버로딩 된 생성자는 사용되지않아서 데이터가 넘어가지않아 앱이 강제 종료될수있는 상황이 찾아올것이다.
그래서 프래그먼트로 데이터를 넘길때는 Bundle 혹은 팩토리 메서드 패턴을 사용하는것이다.
그래서 FragmentFactory 사용법을 살펴봐보자
우선 Android X부터 FragmentFactory 를 통해 인자가있는 생성자로 데이터를 넘겨받을수있다.
androidx.fragment.app 패키지의 Fragment 문서에서 instantiate()의 설명을 다시 찾아보면 deprecated 되었다는 것을 알 수 있다. Fragment의 instantiate 대신 FragmentFactory의 instantiate를 사용하라고 나와있다.
즉 이제 메모리에서 날아가고난후 다시 생성될때 Fragment Factory의 instantiate를 사용하기 때문에 팩토리를 만들어놓는다면 재생성될때도 그 생성자를 통해서 인스턴스를 다시만들어 반환해주는것이다.
이후의 팩토리의 사용내용은 https://zion830.tistory.com/85 블로그의 내용을 가져왔다.
여기서 부터가 블로그에서 가져온 사용방법이다
사용하는 방법
생성자로 인자를 받는 Fragment를 만들어보자
class MyFragment(private val index: Int) : Fragment() {
//...
}
FragmentFactory를 상속받은 후 클래스 이름에 따라 적절한 Fragment를 반환하도록 커스텀 해주자
class MyFragmentFactory(private val index: Int): FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
MyFragment::class.java.name -> MyFragment(index)
else -> super.instantiate(classLoader, className)
}
}
}
Activity에서는 다음과 같이 fragment를 생성할 수 있다.
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MyFragmentFactory()
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutId)
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, MyFragment::class.java.name)
supportFragmentManager.commit {
add(containerId, fragment, MyFragment.TAG)
addToBackStack(null)
}
}
이때 fragmentManager에 fragmentFactory 객체를 설정하는 부분은 반드시 lifecycle callback 중 super.OnCreate()보다 먼저 호출되어야 한다는 점을 유의하자.
근데 이렇게 사용하는건 오히려 더힘들고 불편한거같다 그냥 bundle를 이용하던지
차라리 hilt를 통해서 만들어서 넣어줄꺼같다.
근데 내가 애초에 찾던건 저런게 아니였다 이건 공부하다가 산으로 간거고 이제 원래 목적을 찾아보자
인스턴스를 자체적으로 생성하지말고
Class 연산을 통해서 프래그먼트를 넣어주어야하는데 bundle 을 넣어주는
요 3번쨰 Class<out Fragment!>는 무엇이였을까?
바보같게도 그냥 JAVA class였다
제너릭 을 사용하거나 공부하다보면<T> 이런걸 많이 봤었을것이다
그냥 저 out Fragment도 그냥 프래그먼트를 넣으라는 뜻이지 T 처럼 딱히 별 의미는 없는것이였다.
그리고 자료형 Class는 처음봐서 생소했지만 startActivity 를 하기위해 Intent 객체를 만들떄
ChangActivity::claa.java 이거 하면 java class가 생성되어 들어가는것처럼
Class라는 자료형은 자바클래스를 요구하는거였다
그리고 그냥 참고사항으로
ChangActivity::class 는 코틀린 객체가 나오므로 KClass라는 자료형을 가진다
-> 이런거는 코틀린 리플렉션이라는 키워드로 검색해서 나중에 공부해봐야겠다.
그래서 그냥 막상 사용법은 넣어달라는대로 자바클래스를 넣어주면되는거였다.
private fun initTransactionEvent() {
var bundle = Bundle()
bundle.putInt("roomId", roomId)
bundle.putInt("startPoint",startPoint)
if(startPoint == START_FROM_HOME || startPoint == START_FROM_JOIN_CODE) {
supportFragmentManager.beginTransaction()
.replace(R.id.container_waiting_room, WaitingRoomFragment::class.java,bundle).commit()
} else {
supportFragmentManager.beginTransaction()
.replace(R.id.container_waiting_room, CheckRoomFragment::class.java,bundle).commit()
}
}
이런식으로 말이다.
(기존코드)
private fun initTransactionEvent() {
val waitingRoomFragment = WaitingRoomFragment()
val checkRoomFragment = CheckRoomFragment()
var bundle = Bundle()
bundle.putInt("roomId", roomId)
bundle.putInt("startPoint",startPoint)
waitingRoomFragment.arguments = bundle
checkRoomFragment.arguments = bundle
if(startPoint == 1 || startPoint == 3) {
supportFragmentManager.beginTransaction()
.add(R.id.container_waiting_room, waitingRoomFragment).commit()
} else {
supportFragmentManager.beginTransaction()
.add(R.id.container_waiting_room, checkRoomFragment).commit()
}
}
코드도 기존보다 깔끔해지고 앱이 갑자기 꺼질일도 없는 좋은 방법인거같다.
-> 그리고 또한 프래그먼트에대한 공부도 했으니 돌아돌아 목적을이룬거같다!!
참고로 자료형을 몰라서 헤맬때는 이렇게해보라는 조언을 받았다
안드로이드 스튜디오 설정에서 hint를 검색해서 kotlin 관련 힌트를 다켜주면 자료형에 대한 힌트를 준다
출처:https://zion830.tistory.com/85
https://developer.android.com/reference/android/app/Fragment