Android (안드로이드)

[Android] Fragment / 프래그먼트 / 프래그먼트 백스택 / Activity vs Fragment / 액티비티와 프래그먼트

Oscar:) 2024. 1. 10. 18:45

 

 

이번 포스팅에서는 프래그먼트에 대해 알아보자.

 

 

 


 

 

프래그먼트란?

 

 

공식문서에서는 다음과 같이 설명한다.

 

Fragment는 FragmentActivity 내의 사용자 인터페이스 일부를 나타낸다.

 

여러 프래그먼트를 하나의 액티비티에 결합하여 여러 화면의 UI를 빌드할 수 있고,

하나의 프래그먼트를 여러 액티비티에서 재사용할 수도 있다.

 

프래그먼트는 액티비티의 모듈식 섹션이라고 생각하면 된다.

 

 = 다른 액티비티에 재사용할 수 있는 "하위 액티비티" 개념이다.

 

 

그리고 프래그먼트는 자체적인 생명 주기를 가진다.

 

 


 

프래그먼트 생명 주기

 

 

프래그먼트는 항상 액티비티 내에서 호스팅되어야 하므로,

프래그먼트의 생명 주기는 호스트 액티비티의 생명 주기에 직접적인 영향을 받는다.

 

쉽게 이야기하자면, 프래그먼트를 포함하고 있는 액티비티가 파괴되면

해당하는 프래그먼트도 파괴된다고 생각하면 된다.

 

 

공식문서에서 설명하는 프래그먼트 생명 주기는 다음과 같다.

 

출처 : 안드로이드 공식 문서

 

 

조금 업데이트된 점을 이야기하자면,

onActivityCreated() 생명 주기는 deprecated 되었다.

(공식 문서는 업데이트할 생각이 없는 것 같다...)

 

대신 onViewCreated() 생명 주기를 사용하면 된다.

 

Activity에서 Fragment를 호스팅했을 때,

생명 주기가 호출되는 순서는 각자 로그를 찍어보며 확인하기 바란다.

 

 


 

사용법

 

 

먼저 호스트 액티비티를 만들어주자.

 

FragmentActivity.java

public class FragmentActivity extends AppCompatActivity {

    FragmentManager fragmentManager;
    FragmentTransaction transaction;
    FragmentA fragmentA;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);

        fragmentManager = getSupportFragmentManager();
        transaction = fragmentManager.beginTransaction();
        fragmentA = new FragmentA();
        
        transaction.add(R.id.fragmentLayout, fragmentA);
        transaction.commit();
        
    }
}

 

프래그먼트를 사용할 액티비티에서는,

FragmentManager, FragmentTransaction 객체를 만들어 줘야 한다.

 

FragmentTransaction 객체로 프래그먼트를 관리할 것이다.

 

 

transaction.add() 메서드의 첫 번째 인수에는

프래그먼트가 들어갈 Activity의 View를 지정해 주면 된다.

두 번째 인수에는 띄워줄 프래그먼트를 지정해 주면 된다.

 

transaction에 변경 사항이 있을 때마다 commit() 해줘야 한다.

 


 

FragmentActivity의 레이아웃 파일은 다음과 같다.

 

activity_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Activity.FragmentActivity">

    <FrameLayout
        android:id="@+id/fragmentLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

 

프래그먼트가 들어갈 FrameLayout 만 간단히 만들어 주었다.

 

꼭 FrameLayout을 사용해야 하는 것은 아니다.

 

fragment 라는 View를 사용해도 무방하다.

 


 

그리고 프래그먼트를 만들어준다.

 

FragmentA.java

public class FragmentA extends Fragment {

    View view;
    Button fragmentChangeBtn;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        view = inflater.inflate(R.layout.fragment_a, container, false);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        fragmentChangeBtn = view.findViewById(R.id.fragmentAChangeBtn);

    }

}

 

기본적으로 Fragment 클래스를 상속해줘야 한다.

 

 

유심히 봐야할 부분은 2개의 메서드다.

 

● onCreateView()

 

Fragment 전용 레이아웃을 리턴해줘야 하는 메서드다.

전용 xml 레이아웃을 작성하고, inflate()로 실체화 후 리턴해주면 된다.

 

View 를 전역 변수로 만들어 파일 내 전역에서 참조할 수 있도록 해주었다.

 

 

● onViewCreated()

 

사실상 Activity의 onCreate() 와 비슷한 맥락으로 사용할 수 있다.

위에서 초기화한 View 객체로 해당 레이아웃의 View들을 참조해올 수 있다.

 


 

프래그먼트의 레이아웃 xml 파일은 따로 업로드하지 않겠다.

각자 입맛대로 만드시길.

 

 

대충 다음과 같이 생겼다.

 

 

프래그먼트 전환을 위해 B 프래그먼트로 이동할 수 있는 버튼만 넣어줬다.

 


 

프래그먼트 전환하는 방법은 다음과 같다.

(당연히 프래그먼트 관리는 액티비티에서 수행한다.)

fragmentManager = getSupportFragmentManager();
transaction = fragmentManager.beginTransaction();

transaction.replace(R.id.fragmentLayout, fragmentB);
transaction.commit();

 

초기 프래그먼트를 띄워줄 때 transaction.add() 메서드를 사용하였는데,

transaction.replace() 메서드로 변경해주면 나머지는 똑같다고 보면 된다.

 

 

가장 큰 문제는 프래그먼트 내에 있는 버튼으로

Activity의 FragmentTransaction을 관리해야 한다는 것이다.

 

 

다음과 같이 Activity를 참조하여 사용할 수 있다.

FragmentActivity fragmentActivity;

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    // Acitivy 참조하는 부분★
    fragmentActivity = (FragmentActivity) getActivity();
    fragmentChangeBtn = view.findViewById(R.id.fragmentAChangeBtn);

    fragmentChangeBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            fragmentActivity.changeFragment();

        }
    });
}

 

Fragment는 AppCompatActivity 클래스를 상속하지 않는다.

따라서 액티비티에서 기본적으로 수행할 수 있는 동작을 하지 못한다.

 

그러한 동작이 필요할 때마다 ( + Context가 필요할 때마다)

부모 Activity를 참조하여 사용해야 한다.

 

위 예시에서는 FragmentActivity의 changeFragment() 메서드를 호출했다.

해당 메서드에는 위에서 설명한 프래그먼트 전환 내용이 담겨있다.

 


 

이제 FragmentB를 만들어주고,

FragmentA ←→ FragmentB 이렇게 전환해 보겠다.

 

FragmentB는 FragmentA와 거의 똑같기 때문에

별도로 업로드하지 않겠다.

 

 

실행 결과는 다음과 같다.

 

 

전환이 잘 되는 것을 확인할 수 있다.

 

 


 

백스택 관리

 

 

보통 startActivity() 메서드로 액티비티를 추가할 경우

자동으로 스택이 쌓이게 된다.

 

 

● 총 3개의 액티비티가 백스택에 쌓여있다고 가정해본다.

 

 

사용자 입장에서는 시스템 뒤로가기 버튼을 누를때마다

백스택 최상단 액티비티가 하나씩 종료될것이고,

총 3번을 누르면 앱이 종료된다.

 


 

프래그먼트를 액티비티에 호스팅했다고 가정해본다.

 

 

따로 백스택 추가 없이 commit() 한다면 위와 같은 상황일 것이다.

 

액티비티 내에 프래그먼트가 호스팅되어 있기 때문에,

프래그먼트를 여러번 replace() 하더라도

사용자가 뒤로가기 버튼을 1번만 누르면 앱이 종료된다.

 


 

프래그먼트 트랜잭션을 수행할 때, 이를 백스택에 추가할 수 있다.

이는 액티비티가 관리하는 백스택이다.

 

fragmentManager = getSupportFragmentManager();
transaction = fragmentManager.beginTransaction();

transaction.replace(R.id.fragmentLayout, fragmentB);
// 이 부분만 추가해주면 된다.
transaction.addToBackStack(null);
transaction.commit();

 

addToBackStack() 메서드로 프래그먼트를 백스택에 추가할 수 있다.

인수에는 null을 작성해 주었는데, 별도의 식별자 없이 순서대로 쌓이게 된다.

 

만약 인수에 String key 값을 넣어주면 식별자로서 사용할 수 있고,

popBackStack() 메서드를 사용하여 해당 Transaction으로 돌아갈 수 있다.

 

 

 

이번에는 액티비티에 프래그먼트를 호스팅하고,

프래그먼트를 2번 addToBackStack(), commit() 했다고 가정한다.

 

 

위에서 설명했듯이, 액티비티가 관리하는 백스택이기 때문에

프래그먼트도 액티비티와 동일한 스택을 가진다.

 


 

따라서 액티비티와 스택을 섞어서 쌓을 수도 있다.

 

 

 

실행 결과를 확인해보자.

 

(이해를 돕기 위해 텍스트를 조금 넣어서 영상 편집을 했다)

 

액티비티와 프래그먼트가 섞여서 스택이 쌓이는 것을 확인할 수 있다.

 

 


 

액티비티 vs 프래그먼트

 

 

프래그먼트가 출시된 이후로 많은 이슈가 생겨났는데,

이는 프래그먼트가 액티비티의 역할을 충분히 해낸다는 논점에서 시작되었다.

 

 

Google I/O 2018 에서는 Jetpack Navigation과  함께

SAA 구조에 대해 언급되었는데,

 

*SAA (Single Activity Architecture) :

하나 또는 적은 수의 액티비티만을 사용하고

나머지 화면은 프래그먼트로 구성한 구조.

 

액티비티와 프래그먼트의 효율성을 따지며

프래그먼트의 이점을 잘 살린 구조를 설명했다.

 

 

관심 있는 사람은 시청하기 바란다.

 

Android Dev Summit 2018 ↓

Android Dev Summit 2018

 

 

가장 와닿았던 말은,

사용자는 자신이 보고 있는 화면이 액티비티인지 프래그먼트인지 관심이 없다는 것이다.

 

그렇다면 개발자 입장에서는

더 효율적인 방법으로 앱을 구성하는 것에만 초점을 맞추게 된다.

 

 

액티비티는 생성될 때마다 많은 자원을 필요로 한다.
UI가 구성되는 속도도 프래그먼트가 더 빠르다.
또한 프래그먼트에서 UI를 더 유연하게 다룰 수 있다.

그렇다면 화면을 구성한다는 목적에만 초점을 맞추었을 때,
프래그먼트를 놔두고 굳이 많은 액티비티를 사용할 이유가 없어지는 것이다.

 

 

하지만 프래그먼트도 한계가 분명하다.

애초에 액티비티에 호스팅하도록 설계되었기 때문에
최소 1개의 부모 액티비티는 필요로 한다.

자체적인 생명 주기를 가지고 있기 때문에
액티비티의 생명 주기와 연관지어 생명 주기를 더 복잡하게 관리해야 할수도 있다.

그리고 프래그먼트간의 동작은 비동기 처리되는데,
비동기 작업에 관련해서는 세밀한 예외 처리가 필요하다.

 

 

결국 정답은 없다.

 

SAA 구조에 대해서는 흥미롭게 생각하지만,

무조건 옳다는 방향은 아니다.

 

본인은 아직까지 언젠가 한 번쯤 시도해 볼 만하다는 생각 정도이다.

 

 

하지만 굳이 SAA가 아니더라도,

화면을 구성한다는 목적만을 생각했을 때는 최대한 프래그먼트를 사용할 것 같다.

 

 


 

 

 

 

 

이번 포스팅에서는 프래그먼트에 대해 알아보았다.

 

프래그먼트는 정말 유용하게 사용할 곳이 많다고 느낀다.

언젠가는 SAA 구조의 앱도 개발해보고 싶다.