본문 바로가기

Android(Kotlin) Study A부터 Z까지 등반

안드로이드 프래그먼트에 대해서 LifeCycle, Manager

Fragment란

  • 한 화면에 유기적으로 동작하는 UI가 2개 이상인 경우에 사용할 수 있는 서브액티비티
  • 전체 화면에서 일부만 수정해야하는 경우 많이 사용
  • 모듈화, Adaptability(변경성, 적응성) 제공
  • Activity에서 추가 혹은 제거 가능
  • Activity의 생명주기와도 연관이 있음
  • 재사용 가능, Lifecycle 존재

모듈화란

다음과 같이 한 화면에서 좌측 네이게이션바, 메일 목록, 세부 목록이 있다. 각각 단일 활동으로 실행되며, 자체 수명주기를 가지고 있다

Adaptability란

프래그먼트를 사용하면 Tab의 이동으로 화면 이동 없이 화면(?)을 변환 시켜줄 수 있다. → viewPager에 대한 이야기

💡 프래그먼트 수명 주기는 호스트 액티비티의 수명 주기와 밀접하게 관련되어 있다. 즉, 호스트 액티비티가 일시 중지 상태에 있을 때 액티비티에서 사용 가능한 모든 프래그먼트도 중지된다.

 

간단한 생명주기는 다음과 같다.

각각의 생명주기에서 뭘 할 수 있을까?

  • onAttach() : 프래그먼트가 액티비티에 attach 될 때 호출된다. 호출되면서 인자로 부모 activity의 Context를 받는다. 이 Context를 가지고 부모 액티비티에 listener interface를 implement했다면 형변환을 통해 가져올 수 있다. 솔직히 여기서는 뭐 하기가 애매하다.
  • onCreate(): 초기화를 할 때 사용, UI는 초기화할 수 없다.
  • onCreateView(): 여기서부터 Layout을 inflate한다. View 객체를 얻을 수 있다.
  • onActivityCreated(): Activty에 Fragment가 bind된 후에 호출된다. 여기서 UI 변경이 가능하다.
  • onViewStateRestored(): • view에 저장된 상태가 복원될 때 사용하긴 하는데 MVVM쓰다보니 크게 쓸 일이 없을듯 하다. savedInstanceState 를 인자로 받는다.
  • onStart(): Activity의 Start와 비슷 UI에 보이게하는 작업(이때부터 Started 상태)
  • onResume(): 이제부터 화면에 보여지고 focus가 잡혀있으며 상호작용 가능

뒤로가기 누르거나, 지우거나, 프래그먼트 전환시 백스택에 저장했을 때

  • onPause(): 부모 Activity가 다른 액티비티가 foreground로 나올 때, 다른 프래그먼트로 전환시 호출, 중요한 데이터를 저장하는 부분
  • onStop(): 이때부터 이 Fragment UI가 안보임, onStateInstance()를 호출해서 UI상태를 저장한다고 한다. 원래는 Stop이전에 호출됐지만 API28 이상부터는 바뀌었다.

  • onDestroyView(): Fragment의 View가 제거될 때 실행, activity에서 Fragment실행시에 addToBackStack() 해놓으면 다시 실행 될 때 onAttach() 부터가 아닌 onCreateView()부터 호출된다.
  • onDestroy(): fragment를 제거하기 직전에 호출된다.
  • onDetach(): Activity 제거를 완료하고 Activity와의 연결도 해제시킨다.

그리고 액티비티와의 생명주기 흐름은 다음과 같다.

그리고 자세히 파보면 콜백이 생각보다 더 복잡하게 생겼다.

💡 이걸 하나하나 처리해주기 어렵겠지?

 

사실 지금까지 말했던 것은 생명주기라고 말할 수 없다. 특정한 생명주기로 천이하기 위한 콜백 메소드인것이다.

Fragment에는 생명주기 뿐만 아니라 View Lifecycle도 있다

  • 여튼 이런 복잡한 생명주기의 관리를 보다 편하게 하기 위해서, AAC에서는 lifecycle이라는 컴포넌트를 제공한다.
  • 이는 UI 구성요소의 생명주기의 모니터링을 도와주며 activity와 fragment를 더 가볍게 유지할 수 있게 해준다.

lifeCycle

lifecycle은 main enum 클래스 2개와 interface 2개 로 구성된다.

main enum 클래스 : EVENT, STATES를 가지며 이 둘을 가지고 연결된 구성요소의 수명주기를 추적한다.

  • EVENT는 전달된 수명 주기 이벤트이며, activity나 fragment의 콜백 이벤트에 맵핑된다. (onCreate() 와 같은,,,)
  • STATUS는 lifecycle 객체가 추적한 구성요소의 현 상태를 뜻한다.
  • Android 활동 수명 주기를 구성하는 상태 및 이벤트

interface : lifecycleOwner와 lifecycleObserver로 구성된다.

  • lifecycleOwner: getLifecycle()을 통해 클래스에 lifecycle이 있음을 나타내는 단일 메서드 인터페이스로서 fragment나 activiy의 lifecycle의 소유권을 추출해 가지고 있으며 이를 lifecycle에 제공한다.
  • lifecycleObserver: lifecycle로 부터 상태변화에 대한 이벤트를 감지하고 알려줍니다.

+) lifeCycle은 아래의 경우 사용을 권장한다.

  • ui controller가 data를 수집하지 않고 viewModel에 위임한 상태
  • liveData를 observing하여 view를 update할 때,
  • view와 ui component의 관계를 dataBinding 할 때

즉 liveData + viewModel + dataBinding과 함께 이용 시 두각을 드러냄.

Fragment View lifecycle

  • 프래그먼트의 view lifecycle은 fragment가 유효한 view instance를 제공할 때만 생성된다. 사용 할 수 있는 단계는 onCreateView()을 오버라이드한 뒤에 프래그먼트 뷰를 inflate할 때이다.
  • 그 다음 getViewLifecycleOwnerLiveData()가 fragment view와 함께 새로 initalized된 lifecycleOwner를 업데이트하고, onViewCreated()를 호출하는 순서이다.

💡 onViewCreated()는 뷰 상태를 초기화하거나, livedata를 옵저빙하거나, recyclerView나 ViewPager2의 어뎁터를 셋팅하기 적절한 위치이다. 물론 onCreateView에서도 사용가능

 

다음과 같이 ID를 넘겨주면 onCreateView에서 inflate 안해도 된다고 한다.


화면 회전시 onCreate() 두 번 생성되는거 막는 방법 VM없이

 

val fragment = fragmentmanager.findFrgmentByTag("tag")
     val newFragment : BlankFragment
     if(fragment==null){
       newFragment = BlankFragment()
     }else{
       newFragment = fragment as BlankFragment()
     }
     //use newFragment

그래서 프래그먼트를 왜 쓰는가?

  1. Activity는 상대적으로 Fragment보다 무겁다.
  2. Activity 내에서 Fragment는 상대적으로 가볍게 추가/제거 가능하다.
  3. Activity Stack에 Activity를 쌓아두기보다 Fragment BackStack에서 Fragment를 관리하는게 메모리 관리에서도 효율도 챙기고 화면 전환시에 Activity보다 더 순조롭다.
  4. 데이터 공유에도 용이
  5. 재사용성의 증가
  • View or Business Logic을 Fragment 단위로 분리 가능
  • 아키텍쳐 원칙에서 가장 중요한 원칙인 관심사 분리를 통해 의존성을 분리하고 독립성을 키우게 된다.
  1. 유연한 UI/UX 구현

프래그먼트란 무엇인가? Google I/O팀 2016년도꺼라 ViewModel관련은 없음

https://www.youtube.com/watch?v=k3IT-IJ0J98

Fragment Manager란?

  • 생성한 프래그먼트를 액티비티에 부착만 한다고 되는 것이 아니다. 액티비티 또는 프래그먼트간의 상호작용을 하는 것이 필수이다. 중간에 서로를 이어주는 것이 Fragment Manager이다.

하는 역할

Fragment Transaction

  • 액티비티가 사용자의 입력 이벤트에 따라 프래그먼트를 추가 및 삭제 교체 등의 작업들을 수행할 수 있게 해준다. 트랜잭션의 상태를 프래그먼트 백스택을 통해서 저장도 가능하다.
// 프래그먼트 매니저 선언
FragmentManager fragmentManager = getSupportFragmentManager();
// 프래그먼트 트랜잭션 시작
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

....     // 여기에서 프래그먼트 트랜잭션, 백스택, 애니메이션 등을 설정합니다.
//추가 가능
fragmentTransaction.add(R.id.fragment_container, firstFragment);

// 프래그먼트 제거
fragmentTransaction.remove(firstFragment);

// 전달받은 fragment 를 replace
transaction.replace(R.id.fragment_container, fragment);

// 해당 transaction 을 Back Stack 에 저장
transaction.addToBackStack(null);

// 프래그먼트 트랜잭션 마무리
fragmentTransaction.commit();

여러 백 스택 지원

💡 앱에서 여러 개의 백 스택을 지원해야 하는 경우가 있다. 일반적인 예로 앱에서 하단 탐색 메뉴를 사용하는 경우를 들 수 있다. FragmentManager를 사용한다면saveBackStack() 및 restoreBackStack() 메서드로 여러 백 스택을 지원할 수 있다. 이러한 메서드는 하나의 백 스택을 저장하고 다른 스택을 복원하여 여러 백 스택 간에 전환할 수 있도록 지원한다.

 

예시: 이전 addToBackStack("replacement")을 통해서 백스택에 추가해놨다고 가정해보고 사용, 이걸 써야 사용가능

supportFragmentManager.saveBackStack("replacement")
//트랜잭션과 상태를 저장할 수 있습니다.

supportFragmentManager.restoreBackStack("replacement")
//해당 이름에 대한 모든 트랜잭션과 모든 저장된 프래그먼트 상태를 복원할 수 있습니다.

프래그먼트 추가할 때 트랜잭션에 추가가능

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

액티비티와의 통신

  • 단일 프래그먼트에 대해서 내부 구성요소들을 하나하나 접근할 수 있게 해준다. 액티비티의 이벤트를 받아서 해당 프래그먼트의 적절한 UI 동작을 구현할 수 있게 해준다.

프래그먼트에서 액티비티의 자원에 접근하기 (참조 값으로 접근)

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);

TextView textView = (TextView) getActivity().findViewById(R.id.textView1);
textView.setText("applied!!");
}
  • 프래그먼트 내에서 자식 프래그먼트를 관리하는 FragmentManager 에 접근하기 위해서는 getChildFragmentManager() 사용
  • 자식 프래그먼트에서 부모의 FragmentManager 에 접근하기 위해서는 getParentFragmentManager() 를 사용

어디서 호출하느냐에 따라서 사용하는 메소드도 달라진다고 한다.

액티비티에서 프래그먼트 접근하기

  • 모든 FragmentActivity, 그리고 그것의 subclass (AppCompatActivity 와 같은) 는 getSupportFragmentManager로 FragmentManager 에 접근

FragmentActivity와 Activity 의 차이가 무엇일까?

  • FragmentActivity 는 Activity 의 subclass 로, 안드로이드 오래된 버전과의 호환성을 보장하기 위해 추가적으로 몇 가지 메서드를 제공하지만 크게는 별 차이 없다고 한다.

Using FragmentFactroy

Fractory를 사용하여 트랜잭션을 변경할 수 있다. 시나리오에 삽입도 가능

 

프래그먼트를 교체할 때 FrameLayot을 확장한 FragmentsContainerView를 추가했다. 프래그먼트만 넣을 수 있음

 

Lifecycle을 FragmentPagerAdapter에 추가할 수 있게 되었다.

FragmentPagerAdapter vs FragmentStateAdapter의 차이

하단 탭으로 프래그먼트를 교체한다고 해보자.

  • FragmentPagerAdapter는 다른 탭으로 이동시 View 계층에서 Destroy 되었을지도 모르는 상황에서도 Fragment 인스턴스가 원하는 만큼이 메모리를 계속 차지하고 있다.
  • FragmentStateAdapter는 다른 탭으로 이동시 해당 Fragment의 State를 저장하고 Destroy 되기 때문에 FragmentPagerAdapter와 비교 했을때 훨씬 적은 메모리를 사용하며, Page가 전환될 때 발생하는 오버헤드도 더 많이 감수할 수 있다.

💡 FragmentStatePagerAdapter는 ViewPager2에서 deprecated

 

하지만!

Note: We strongly recommend using the Navigation library  to manage your app's navigation. The framework follows best practices for working with fragments, the back stack, and the fragment manager. For more information about Navigation, see Get started with the Navigation component and Migrate to the Navigation component.

대충 정리하면 fragment, 백스택, fragment manager 와 관련된 작업을 하려면 jetpack 의 navigation library 를 사용하라는 것. jetpack navigation library 를 사용할 경우, 개발자를 대신해서 fragment manager 를 사용하기 때문에 개발자가 직접 fragment manager 를 만질 일은 없다 정도로만 이해하면 될듯

Navigation Component의 주요 구성요소 3가지

  • Navigation Graph : 모든 Navigation 관련 정보가 모여있는 XML 리소스 입니다. 여기에는 대상이라고 불리는 앱 내의 모든 개별적 콘텐츠 영역과 앱에서 갈 수 있는 모든 이용가능한 path가 포함됩니다.
  • NavHost : 탐색 그래프에서 대상을 표시하는 빈 컨테이너 입니다. 대상 구성요소에서는 프래그먼트 대상을 표시하는 기본 NavHost 구현인 NavHostFragment가 포함됩니다.
  • NavController : NavHost에서 앱 탐색을 관리하는 객체입니다. NavController는 사용자가 앱 내에서 이동할 때 NavHost에서 대상 콘텐츠의 전환을 오케스트레이션 합니다.

할 수 있는 활동

  • Fragment 트랜잭션을 관리할수 있다.
  • Up, Back 버튼의 작업 등(백스택 관리)을 간단하게 처리
  • 화면 전환시, Animation 이나 Transition을 위한 표준화된 리소스를 제공
  • 딥링크 구현 및 처리
  • Navigation UI 패턴을 사용한 Navigation drawers, Bottom Navigation의 연동을 쉽게 구현 가능하게 지원
  • fragment간의 이동시 안전하게 데이터를 전달 가능
  • Navigation Editor를 통해 화면흐름을 시각적으로 관리할 수 있다.
  • 기본 FragmentManager, Intent, Bundle등의 사용을 대체할수 있다.

다음과 같이 선언 android:name을 주요하게 보자 defaultNavHost를 true로 설정하면 현재 구현되는 Activity에서의 뒤로 가기 버튼이 우리가 구현한 Navigation에 설정한대로 동작합니다.

<fragment
    android:id="@+id/nav_host"
    **android:name="androidx.navigation.fragment.NavHostFragment"**
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
    app:menu="@menu/menu_bottom_navigation"
    android:id="@+id/main_bottom_navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

BottomNavigation은 해당 menu 레이아웃을 참조하고 있음

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@id/firstHomeScreen"
        android:icon="@drawable/ic_folder"
        android:title="@string/title_menu_first" />

    <item
        android:id="@id/secondHomeScreen"
        android:icon="@drawable/ic_folder"
        android:title="@string/title_menu_second" />

    <item
        android:id="@id/thirdHomeScreen"
        android:icon="@drawable/ic_folder"
        android:title="@string/title_menu_third" />
</menu>

Activity에서 선언

NavigationUI.setupWithNavController(main_bottom_navigation, findNavController(R.id.nav_host))
  • NavigationUI.setupWithNavController은 view와 navController를 인자로 받아 view를 navController에 맞게 구현해준다. 이 과정에서 bottomNavigation의 menu값의 id와 fragment 목적지가 자동으로 맵핑된다.
  • Navigation의 요소들

Arguments

Array와 Optional(Nullable) 타입이 가능할 뿐만 아니라 enum, Parceble 값도 가능하고 기본값도 설정할 수 있기 때문에 이전처럼 Bundle에 putString, putInt을 할 필요가 없다. 만약 스택에 쌓였던 이전 Fragment들이 메모리에서 정리된다면 인자로 받은 Argument를 기준으로 다시 생성.

Actions

네비게이션이 어떻게 움직일지에 대한 Action 값

화면을 하나 선택한 후 오른쪽 클릭을 하면 Add Action이라는 메뉴가 보입니다. 액션은 상당히 다양하게 정의할 수 있습니다.

  • To Destination : 현재 화면에서 다른 화면으로 이동하는 액션(이거는 Design탭에서도 Arrow 연결로 가능)
  • To Self : 자기 자신으로 이동하는 액션
  • Return To Source: popBackStack시 사용될 액션
  • Global: 어떤 화면에서든 현재 화면으로 이동할때의 액션

액션 화살표를 클릭하면 오른쪽에 Argument Default Values, Pop Behavior, launchSingleTop 메뉴를 볼 수 있다.

  • Argument Default Values: 만약 목적지에 Argument들이 있다면 해당 액션에서 각각의 Argument들에 대한 기본값을 입력해줄 수 있습니다.
  • Pop Behavior — popUpToIncursive : 화면 간 이동시 쌓여있는 Fragment 스택들에 대해 어떻게 처리할지를 정의합니다. 예를 들어 [스플래시] -> [로그인] -> [메인] 화면이 있다고 가정하고 로그인 화면에서 메인화면으로 가는 액션에 popUpToIncursive 옵션을 false로 주면 메인에서 Back button을 눌렀을 때 로그인 화면으로 돌아오게 됩니다. 하지만 true를 주면 로그인 화면은 스택에서 사라지고 C화면에서 Back버튼을 눌렀을 때 스플래시로 이동합니다.
  • Pop Behavior- popUpTo 옵션은 선택한 Fragment까지 쌓여있는 모든 스택을 popup 시킵니다. 만약 [스플래시] -> [로그인] -> [메인화면] 화면에서 popupTo값으로 메인화면을 준다면 쌓여있던 스플래시와 로그인 화면은 정리되고 메인화면만 남게 됩니다.

Deep Links

  • 딥링크는 안드로이드에서 어플의 특정 목적지까지 포인팅하는 URI이다. 이런 URI는 사용자에게 어떤일을 위해 특정 목적지로 보낼때 유용하다. ex> 빠르게 송금, 빠르게 메세지 확인, 빠르게 상품 확인 등등.. 기존의 플로우 과정없이 한번에 이동하는 로직을 짤때 필요하다.
  • Navigation의 DeepLink은 딥링크로 특정목적지를 들어왔어도, 첫화면부터 특정 목적지 까지의 일반적인 백스택을 그대로 지원해준다. Navigation에서 DeepLink는 간단하게 짤수있다.

Deeplink는 두가지 구현방법이 있다.

1. Explicit Deep Linking - 명시적 딥링크(NavDeeplinkBuilder를 사용하여 인스턴스를 생성하여 핸들링이 가능) pendingIntent를 사용

  • NavGraph, Desination Fragment, Args 를 명시적으로 선언하여 사용할 수 있다.
val args = Bundle()
args.putString("itemId", itemId)
val pendingIntent = NavDeepLinkBuilder(context)
  .setGraph(R.navigation.mobile_navigation)
  .setDestination(R.id.detailFragment)
  .setArguments(args)
	.setComponentName(DestinationActivity::class.java)
  .createPendingIntent()

2. Implicit Deep Linking - 암시적 딥링크

  • 개인적으로 NavGraph에서 Navigation의 대한 정보를 정의하고 사용해야 Navigation Component가 의미가 있는거 같기 때문에 이방법을 추천한다. 또한 더 간단하다. NavGraph에 선언하여 사용하면된다.
<fragment android:id="@+id/detailFragment"
          android:name="com.namjackson.arch.sample.view.ui.detail.DetailFragment"
          android:label="detail_fragment"
          tools:layout="@layout/detail_fragment">

    ...action 생략...

    <argument
          android:name="itemId"
          app:argType="integer"
          android:defaultValue="0"/>

    <deepLink android:id="@+id/deepLink"
          app:uri="sample.detail/{itemId}" />
  </fragment>

매니페스트에도 추가 작업을 해줘야한다.

<activity
       android:name=".view.ui.MainActivity"
       android:label="@string/app_name">
   <intent-filter>
     <action android:name="android.intent.action.MAIN"/>

    <category android:name="android.intent.category.LAUNCHER"/>
  </intent-filter>
**//이부분**
  **<nav-graph android:value="@navigation/mobile_navigation" />**
</activity>