정상에서 IT를 외치다

[Android, FragmentFactory] framgnet에서 newInstance() 쓰지 말라고? 본문

안드로이드

[Android, FragmentFactory] framgnet에서 newInstance() 쓰지 말라고?

Black-Jin 2020. 3. 18. 17:00
반응형

안녕하세요. 블랙진입니다.


우리는 프래그먼트를 인자와 함께 생성할 때 newInstance()를 사용하곤 합니다.  왜 그럴까요? 흔히 알고 있는 두가지 이유가 있을 겁니다.


1. 프래그먼트 재생성(화면 회전과 같은)시 빈생성자가 있어야 한다.

2. 재생성시 받아온 데이터를 유지하기 위해서 사용한다.


우리가 자주 사용하는 코드

companion object {

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

@JvmStatic
fun newInstance(param1: String, param2: String) =
MainFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}


newInstance() 방식은 빈 생성자를 만들고 bundle에 데이터를 넣어 처리해 주고 있습니다. 이렇게 함으로써 화면 회전과 같은 재사용이 되었을 때 데 받아온 데이터를 유지하며 화면에 보여줄 수 있습니다.



빈 생성자 없이 사용한다면?


MainFragment에 두개의 파라미터를 가지는 생성자를 만들어 줍니다.

class MainFragment(param1: String, param2: String) : Fragment()


MainActivity에서 함수를 호출해주면 정상적으로 화면을 보여줍니다.

val fragment = MainFragment("black","jin", this)
supportFragmentManager.beginTransaction()
.replace(R.id.container_fragment, fragment)
.commitNow()


하지만 화면을 회전하게 되면 아래 문구와 함께 InstantiationException이 발생합니다. 


could not find Fragment constructor


이는 프래그먼트의 빈 생성자가 없기 때문에 발생하는 에러입니다.



위 이유에 대해 구글 문서를 확인해보겠습니다.


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.

출처 - Google Developer


내용을 요약하면 모든 프레그먼트는 빈 생성자를 가져야 합니다.  그렇지 않다면 메모리 부족과 같은 현상으로 프레그먼트를 재생성할 때 런타임 에러를 유발합니다. 왜 그런지 위에서 언급만 했지 아마 이유는 몰랐을 겁니다. 그 이유는 Fragment.java에 있는 instantitae 함수를 보면 알 수 있습니다.



Fragment.java

@NonNull
public static Fragment instantiate(@NonNull Context context, @NonNull String fname,
@Nullable Bundle args) {
try {
Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(
context.getClassLoader(), fname);
Fragment f = clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(args);
}
return f;
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (NoSuchMethodException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": could not find Fragment constructor", e);
} catch (InvocationTargetException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": calling Fragment constructor caused an exception", e);
}
}


위 코드 중 아래 함수를 자세히 보겠습니다.

Class<? extends Fragment> clazz = FragmentFactory.loadFragmentClass(
context.getClassLoader(), fname);
Fragment f = clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(args);
}


이 부분을 보면 newInstance() 호출합니다. 이는 빈 생성자를 가진 Fragment를 의미합니다. 그래서 위 함수가 호출 될 때 빈 생성자가 없다면 에러를 발생하게 됩니다.

또한 bundle에 있는 데이터를 그대로 setArguments 해주기 때문에 bundle에 데이터를 넣어야만 화면 재생성시 데이터를 보존할 수 있게 됩니다.



우리는 위 두 가지 이유에서 인자를 가진 프래그먼트 생성자를 사용할 수 없었습니다. 하지만!



AndroidX 부터는 다른 방식으로 초기화 해줘도 가능합니다!



우리는 위 방식을 고수해 왔지만 AndroidX로 업데이트 되면서 위에서 살펴본 instantiate()는 deprecated 되었습니다.

@Deprecated
@NonNull
public static Fragment instantiate(@NonNull Context context, @NonNull String fname,
@Nullable Bundle args) {
//...
}


구글에서는 위 방식이 아닌 이제 FragmentFactory의 instantiate를 사용해 구현하라고 합니다.

* @deprecated Use {@link FragmentManager#getFragmentFactory()} and
* {@link FragmentFactory#instantiate(ClassLoader, String)}, manually calling
* {@link #setArguments(Bundle)} on the returned Fragment.


위 방법을 사용하는 이유는 기존에는 프래그먼트에 빈 생성자가 없다면 구성 변경 및 앱의 프로세스 재생성과 같은 특정 상황에서 시스템은 프래그먼트를 초기화 하지 못했습니다.  이 점을 해결하기 위해 FragmentFractory를 사용하게 되었고 Fragment에 필요한 인수 및 종속성을 제고하여 시스템이 Fragment를 더욱 잘 초기화하는데 도움을 줍니다.



Android X에서 인자가 있는 프래그먼트 생성 방법



1. 빈 생성자 없이 인자가 있는 생성자를 만들어 줍니다.

class MainFragment(private val param1: String, private val param2: String) : Fragment() {
    //...

}



2. FragmentFactory의 instantiate를 구현해 줍니다.

class MainFragmentFactoryImpl: FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
MainFragment::class.java.name -> MainFragment("black","jin")
else -> super.instantiate(classLoader, className)
}
}
}


3. MainActivity에서 다음과 같이 구현합니다.

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MainFragmentFactoryImpl()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, MainFragment::class.java.name)
supportFragmentManager.beginTransaction()
.replace(R.id.container_fragment, fragment)
.commitNow()
}


위 방법을 사용하면 Framgnet.java에 있던 instatiate를 사용하지 않고  우리가 정의한 FragmentFactory의 instatiate 사용해 초기화 하게 됩니다. 여기서 중요한 것은


supportFragmentManager.fragmentFactory = FragmentFactoryImpl()


위 함수를 Activity의 onCreate보다 먼저 선언해 주어야 한다는 것입니다.


이렇게 하면  빈 생성자를 굳이 생성할 필요가 없으며 bundle에 데이터를 넣지 않아도 재생성 상황에서 데이터가 보존됩니다.




FragmentFactory 꼭 필요한가?


그렇지 않습니다. 기존 방식대로 사용하셔도 무방합니다. 다만 인자가 있는 생성자를 사용할 시 반드시 빈 생성자 프래그먼트를 만들어 주어야 합니다. 만약 인자가 있는 생성자와 FragmentFactory를 사용한다면 위 방법을 권장합니다.




Koin을 사용한 Fragment 초기화


FragmentFactory 는 생성자를 주입하는 패턴과 비슷합니다. 이에 안드로이드 의존성 주입 라이브러리 중 하나인 Koin 에서는 위 방법을 지원해 줍니다.


 startKoin {
    // setup a KoinFragmentFactory instance
    fragmentFactory()

    modules(...)
}

fragmentFactory() 를 사용해 좀더 쉽게 인자가 있는 프래그먼트를 주입할 수 있습니다. 자세한 사용 방법은 링크를 확인해 주세요.



<참고자료>

koin - setupKoinFragmentFactory

proandroiddev - fragmentfactory

medium - fragmentfactory


반응형
Comments