Android - Fragment 핵심

동키·2025년 3월 23일

안드로이드

목록 보기
5/14

이번 포스팅에서는 공식문서(Fragment) 를 공부한 핵심 내용에 대해 다뤄보겠습니다~.

Fragment

안드로이드에서 UI 일부를 독립적으로 구성하고 관리할 수 있게 해주는 모듈형 UI컴포넌트

프래그먼트는 자체 레이아웃을 정의 및 관리하고 자체 수명주기를 가짐

단독으로 실행될 수 없기 때문에 Activity나 다른 Fragment가 필요함

언제 사용할가?

  • 화면 분할이 필요할 때

  • 여러 화면을 하나의 Activity에서 관리할 때

  • 재사용 가능한 UI컴포넌트가 필요할 때

  • 동적으로 UI를 바꿔야 할 때


생명주기

여기서 주의깊게 봐야할 내용은 Fragment는 자체 생명주기와 Fragment View의 생명주기는 서로 다르다는 것입니다.

왜 Fragment는 자체 생명주기와 / View 생명주기를 가질까요?
예시의 탭 화면처럼 Fragment를 오래 유지하면서 그 안의 View만 필요할 때마다 생성/해제해서 메모리 효율을 높일 수 있습니다.

예를 들어 Todo 탭에서 Calendar 탭으로 이동했다고 가정해보겠습니다.

TodoFragment 자체 객체는 유지되며(onDestroy 되지 않음) View는 메모리를 아끼기 위해 onDestroyView()가 호출됩니다.

다시 Calendar탭에서 Todo 탭으로 돌아갔을 땐 기존의 Todo Fragment는 재사용되고 View만 다시 생성되게 됩니다.

onCreate()

  • 프래그먼트가 FragmentManager에 추가

  • 뷰 생성 상태 X

onCreateView()

  • FragmentView의 생명주기 시작

  • 뷰 infalte

onViewCreate()

  • View 인스턴스를 전달받음

  • 이 시점에서 뷰의 초기 상태를 저장하고 LiveData 관찰, 어댑터 설정을 하면 안정성을 보장받을 수 있습니다.

onStart()

  • 프래그먼트와 뷰 STARTED 상태

  • 뷰가 보이기 시작함

  • LiveData의 observe, repeatOnLifecycle(STARTED) flow 수집이 시작됨

onResume()

  • Fragment가 화면에서 포커스를 가짐

onPause()

  • 사용자가 프래그먼트를 벗어나기 시작할 때

  • ON_PAUSE 이벤트 보냄

onStop()

  • 뷰가 더 이상 보이지 않음

  • ON_STOP 이벤트 -> STOPPED 상태

  • LiveData의 observe나 repeatOnLifecylce 수집이 중지됨

onDestroyView()

  • 프래그먼트 뷰의 Lifecycle이 DESTROYED 상태로 전환

  • 이 시점에서 프래그먼트 뷰의 모든 참조를 삭제해야 가바지 컬렉터가 가비지를 수집할 수 있습니다. ex) _binding = null, adapter = null


viewLifecycleOwner

여기서 주목해야 할 건 viewLifecycleOwner 입니다.

Fragment 안에 있는 View의 생명주기를 따르는 LifecycleOwner

이 viewLifecycleOwner가 어떻게 내부적으로 관리되고 생성될가요?

Fragment안에서 viewLifecycleOwner를 작성하고 내부 코드로 들어가보겠습니다.

public class Fragment implements LifecycleOwner {
@Nullable
FragmentViewLifecycleOwner mViewLifecycleOwner;
MutableLiveData<LifecycleOwner> mViewLifecycleOwnerLiveData = new MutableLiveData<>();

@NonNull
public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {
    return mViewLifecycleOwnerLiveData;
	}
}

Fragment는 기본적으로 LifecycleOwner를 구현하고 있습니다.
그래서 Fragment.lifecycle은 Fragment 객체 자체의 생명주기를 가집니다.

그러나 Fragment 안의 View는 따로 생성/파괴됩니다.
그래서 등장한 것이 viewLifecycleOwner 입니다.

Fragment가 View를 생성할 때 (onCreateView 직후)

내부적으로 FragmentViewLifecycleOwner 인스턴스를 생성해서 저장하고
→ mViewLifecycleOwnerLiveData에 넣어줍니다.

실제 흐름 타이밍

onCreateView 시점에 mViewLifecycleOwner 생성
onViewCreated 시점에 viewLifecycleOwner 접근 가능
onDestroyView 시점에 getViewLifecycleOwnerLiveData가 null을 반환

사용 예제

class BlankFragment : Fragment() {
    private var _binding: FragmentBlankBinding? = null
    private val binding get() = _binding!!
    private val viewModel: TestViewModel = ViewModelProvider(owner = this)[TestViewModel::class.java]
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

    }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentBlankBinding.inflate(inflater, container, false)
        return inflater.inflate(R.layout.fragment_blank, container, false)
    }

    @SuppressLint("SetTextI18n")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.someLiveData.observe(viewLifecycleOwner) { number ->
            binding.basicTextView.text = number.toString()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

viewModel의 owner를 Fragment 자신 this로 설정했습니다.
만약 이때 Activity와 뷰모델을 공유하고 싶다면

val model = ViewModelProvider(requireActivity()).get(TestViewModel::class.java)

requireActivity()를 전달해주면 됩니다.

또 주목해야할 점은 someLiveData.observe에 LifecycleOwner로 viewLifecycleOwner를 넘겨줬습니다.

왜 viewLifecycleOwner 뷰의 생명주기를 넘겨줬을까요?

바로 메모리 효율성을 위해서입니다.

유저가 1번탭에서 2번탭으로 이동했을 때 1번탭에서 계속 데이터를 수집하고 있다면 이는 메모리 누수가 발생하게 됩니다.

즉, Fragment객체 자체는 살아있지만 View는 DESTROYED된 상태이며 뷰가 보일때만 데이터를 수집하고 뷰가 보이지 않을 때는 수집을 멈춥니다.

왜 써야할까?

LiveData의 observe 내부 코드를 살펴보겠습니다.

@MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }
 class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
        @NonNull
        final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive() {
            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
        }

        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
            if (currentState == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            Lifecycle.State prevState = null;
            while (prevState != currentState) {
                prevState = currentState;
                activeStateChanged(shouldBeActive());
                currentState = mOwner.getLifecycle().getCurrentState();
            }
        }

Lifecycle의 상태가 바뀔 때마다 onStateChanged 함수가 호출되고 현재 상태가 STARTED면
shouldBeActive() 함수가 true를 반환하게 됩니다.

현재 상태가 STOPPED라면 false가 반환되고
activeStateChanged(false)에 옵저버는 비활성 상태로 전환되게 됩니다.

만약 상태가 DESTROYED라면 removeObserver가 불려 옵저버가 제거됩니다.

이를 통해 알 수 있는 점은

뷰의 상태가 STARTED(화면에 보일 때)상태에 데이터 수집 / STOPEED 수집 중지

된다는 점입니다. (메모리 효율성)


FragmentManager

Fragment를 컨트롤하는 컨트롤러

Fragment를 추가, 제거, 교체 등 트랜잭션 을 수행하는 관리자 클래스

  • add() 현재 레이아웃에 Fragment 추가
  • replace() 기존 Fragment를 새 Fragment로 교체
  • remove() 현재 Fragment 제거
  • show() 컨테이너의 Fragment를 보여준다

  • hide() 컨테이너의 Fragment를 숨긴다

기존 프래그먼트의 변경사항(교체 추가 등)을 Transaction을 통해 이뤄졌습니다.

val transaction = supportFragmentManager.beginTransaction()
transaction.add(R.id.container, MyFragment())
transaction.addToBackStack(null)
transaction.commit()

최근 FragmentTransaction에 KTX 확장함수가 추가되어

supportFragmentManager.commit {
   setReorderingAllowed(true) // 성능 최적화
   add(R.id.container, MyFragment())
   addToBackStack(null)
}

더 가독성 좋게 작성할 수 있습니다.

setReorderingAllowed(true)란 여러 Fragment 트랜잭션을 최적화하고,
중간에 생명주기 이벤트가 여러 번 발생하지 않게 하는 최적화 설정합니다.

profile
오늘 하루도 화이팅

0개의 댓글