Activity와 Fragment간의 화면전환

너 오늘 코드 짰니?·2021년 10월 2일
1

Android

목록 보기
1/12

우리가 어플을 사용할 때 보는 화면은 크게 두가지가 있다.

Activity

  • 어플에 필수적으로 들어가야 하며 앱이 UI를 그리는 창을 제공
  • 액티비티 위에 여러가지 뷰를 올릴 수 있으며 이를 통해 유저는 앱 내에서 활동을 하게 된다.
  • Intent 를 통해 액티비티 간에 이동하고 데이터를 공유할 수 있다.

Fragment

  • 앱 UI의 재사용 가능한 부분으로써 독립적으로 존재할 수 없고 활동이나 다른 프래그먼트에서 호스팅 되어야 한다.
  • FragmentManager 를 통해서 액티비티와 프래그먼트, 또는 프래그먼트와 프래그먼트 사이에서 이동하고 데이터를 주고받을 수 있다.
  • 액티비티 위에 fragment를 올려서 여러 fragment xml파일을 만들고 재사용하는 방식으로 사용한다.

Fragment exe

다양한 크기의 화면을 가진 기기들이 나오면서 단순 액티비티 하나만으로는 다양한 크기의 화면에 맞는 UI 디자인을 하기 어려워졌다. 그래서 나온것이 fragment이다. 개발을 할 때에도 fragment를 적절히 사용한다면 Activity만 사용하여 개발하는 것보다 더 가볍고 다양하게 보여지는 view를 만들 수 있다.

Fragment 사용하기

build.gradle 파일에 다음의 종속성을 추가한다.

dependencies{
	implementation("androidx.fragment:fragment-ktx:1.3.6")
    }

avtivity xml파일에 fragment를 추가하고 fragment xml 파일을 생성한다.

이제 FragmentManager를 사용하여 액티비티와 프래그먼트를 연결하는 일만 남았다.

메인액티비티에 검색창 역할을 하는 SearchFragment를 연결시켜보자.

class MainActivity : AppCompatActivity() {
    lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
//첫번째 방법
supportFragmentManager.beginTransaction()
                        .replace(R.id.main_frm, SearchFragment())
                        .commit()
                        
//두번째 방법
supportFragmentManager.commit {
                        replace(R.id.main_frm, LookFragment())
                    }

onCreate 함수 안에 supportFragmentManager를 호출하여 트랜잭션을 수행할 수 있다. 트랜잭션 수행 시에는 FragmentManager가 갖고 있는 함수를 사용하여 fragment를 추가(add()), 삭제(remove()), 교체(replace()) 또는 백스택에 추가를 할지 말지를 결정할 수 있다.
위의 코드에서는 replace()를 사용하여 기존 프래그먼트를 새로운 SearchFragment로 교체해준 모습.

트랜젝션 수행시 마지막에 commit()을 꼭 호출 해주어야 트랜잭션이 진행되는데, 두번째 방법의 commit{...}블럭을 사용하면 마지막에 commit()이 자동으로 되어 편리하다.

FragmentManager의 종류

FragmentManager에는 세 종류가 있다.

  • supportFragmentManager
  • parentFragmentManager
  • childFragmentManager

아래 그림을 보면 조금 이해하기가 쉽다.
FragmentManager 도표

우리는 방금 HostActivity인 MainActivity에서 SearchFragment를 호출했기에 supportFragmentManager를 사용하였다. 그런데 만약 SearchFragment에서 검색을 하여 그 검색정보를 다루고 있는 AlbumInfoFragment를 호출하여 MainActivity의 main_frm에 띄워져 있는 SearchFragment를 AlbumInfoFragment로 교체하고 싶어졌다고 해보자.

class SearchFragment : Fragment() {
    lateinit var binding: FragmentSearchBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentSearchBinding.inflate(inflater, container, false)

//     앨범 버튼을 눌렀을 때 AlbumInfoFragment를 호출하여 
//     MainActivity의 main_frm을 replace()하는 메소드
        binding.homeBackgroundIv.setOnClickListener{
            supportFragmentManager.beginTransaction()
                    .replace(R.id.main_frm, AlbumInfoFragment())
                    .commit()
        }

        return binding.root
    }

아마도 Unresolved reference : supportFragmentManager
라는 에러가 뜰 것이다.

그 이유는 위의 사진에서 알 수 있다시피 Host Activity인 MainActivity에서 프래그먼트를 교체하고 싶은데 Host Fragment인 SearchFragment에서 supportFragmentManager를 호출하였기 때문이다. Host Fragment에서 Host Activity의 Fragmentmanager를 사용하고 싶다면 parentFragmentManager를 호출해야 한다.

binding.homeBackgroundIv.setOnClickListener{
            parentFragmentManager.beginTransaction()
                    .replace(R.id.main_frm, AlbumInfoFragment())
                    .commit()

위의 코드로 바꾸면 잘 동작할 것이다.

정리하면, MainActivity에서의 동작으로 프래그먼트를 교체하고싶다면 supportFragmentManager를 사용하고 MainActivity 위에 올라가 있는 프래그먼트에서의 동작으로 MainActivity 위의 프래그먼트를 교체하고 싶다면 parentFragmentManager를 사용하면 된다.

childFragmentManager의 경우 프래그먼트 위에 또 프래그먼트가 올라가는 상황에서 사용하게 되는데 다중프래그먼트의 경우 구현이 복잡해지기 때문에 가능하다면 custom view를 사용하는것을 권장한다.

백스택

어플을 사용할 때 계속해서 다른 액티비티로 이동하면서 활동을 하다가 어플 종료를 하려고 뒤로가기버튼을 누르면, 현재 액티비티가 종료되고 계속해서 이전에 사용했던 액티비티가 나오다가 맨 처음 액티비티까지 돌아가면 그제서야 어플이 종료되는 경험을 모두 해봤을 것이다.

이것을 백 스택이라 부른다.

LIFO(Last In First Out) 구조를 따르는 스택의 형식으로 액티비티의 활동히스토리들이 저장되었다가 뒤로가기 되면 현재의 액티비티가 destroy되고 이전의 액티비티가 복원된다.

하지만 프래그먼트의 경우 액티비티처럼 자동으로 백스택에 push되지 않는다.
위에서 작성했던 코드를 다시 되새겨보자.
MainActivity에서 검색을 하기위해 SearchFragment를 열었다. SearchFragment에서 검색을 하고, 다시 MainActivity로 돌아가 활동을 계속하고 싶어졌다. 그래서 뒤로가기 버튼을 누르면, 어플이 종료되어버릴 것이다.

binding.homeBackgroundIv.setOnClickListener{
            parentFragmentManager.beginTransaction()
                    .replace(R.id.main_frm, AlbumInfoFragment())
                    //프래그먼트 트랜잭션을 백스택에 push
                    .addToBackStack(null)
                    .commit()

이럴 때 .addToBackStack()을 호출하면 프래그먼트 트랜잭션이 백스택에 푸쉬되어 뒤로가기를 눌렀을 때 이전의 활동으로 돌아갈 수 있게 된다.

null 자리에 String자료형으로 TagName을 넣어 나중에 popBackStack()을 호출하여 특정 트랜잭션으로 다시 돌아가는 것이 가능하지만, 뒤로가기를 누르면 자동으로 popBackStack()이 실행되기에 그냥 이전의 화면으로 가고싶다면 null을 넣어도 충분하다.

.setReorderingAllowed(true)

프래그먼트에 각종 애니메이션이 들어가고 여러 프래그먼트가 존재 할 때, 특정 활동에 의해서 두 개 이상의 프래그먼트가 동시에 add(), remove(), replace()될 수 있다.
이 때 애니메이션이 혼용되어 제대로 동작하지 않거나 어플의 성능이 저하되어 보이는 문제가 발생할 수 있는데 이때 .setReorderingAllowed(true)를 사용하면 불필요한 트랜잭션을 최소화하여 프래그먼트 상태전환을 최적화 할 수 있다.

binding.homeBackgroundIv.setOnClickListener{
            parentFragmentManager.beginTransaction()
                    .replace(R.id.main_frm, AlbumInfoFragment())
                    //프래그먼트 상태전환 최적화
                    .setReorderingAllowed(true)
                    .commit()

그냥 단순하게 복잡한 프래그먼트 전환 시 퍼포먼스를 더 좋게 해주는 코드라고 생각하면 될 것 같다.

commit()에 대하여

프래그먼트가 트랜잭션 시 마지막에 항상 commit()이 호출되어야 트랜잭션이 이루어진다고 했다.
하지만 commit()에는 조건이 하나 있다.

  • 액티비티의 onSavedInstance()가 이루어지기 전 시행될 것.

예를들어 액티비티1과 액티비티2가 있다고 해보자.
액티비티1에서 프래그먼트를 띄워놓고 액티비티2로 넘어갔다. 그럼 액티비티1에서는 onSavedInstance()가 실행되어 현재 Activity State를 저장하게 된다. 그리고 액티비티2에서 활동을 마치고 다시 액티비티1로 돌아오면 이미 onSavedInstance()가 실행되어버렸기 때문에 프래그먼트 트랜잭션을 시도 할 시

E/UncaughtException: java.lang.IllegalStateException: 
    Can not perform this action after onSaveInstanceState

와 같은 에러가 나며 crash될 것이다.
이때 .commit()대신 .commitAllowingStateLoss()를 사용하면 Activity State Loss를 허용하기 때문에 문제없이 동작할 것이다.

가장 좋은것은 State Loss 없이 commit()을 하도록 코딩을 하는것이지만 어려우면 그냥 commitAllowingStateLoss()를 사용하자.

profile
안했으면 빨리 백준하나 풀고자.

0개의 댓글