프래그먼트로 하단바 만들기, 프래그먼트 생명주기, Frame Layout 안에서 view가 최상단으로 오지 않는 문제 해결

임현주·2021년 11월 5일
1
post-thumbnail

시작

혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 제가 잘못 이해한 부분이 있다면 알려주시면 감사하겠습니다 💌


🔹 프래그먼트로 하단바 만들기

그동안 하단바는 하드코딩해서 include layout으로 사용했는데 meterial 라이브러리를 사용하면 (세상 편하고..) 일반적인 어플들에서 접할 수 있는 심플한 ui를 구현할 수 있다.

https://github.com/material-components/material-components-android

하단 바에 있는 아이콘들은 피그마 무료 리소스 사이트를 통해 벡터 이미지로 저장한 아이콘들이다. 먼저 res 폴더에 menu 폴더를 생성하고 하단에 들어갈 item 메뉴 xml 파일 작성한다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/menu_home"
        android:title="홈"
        android:icon="@drawable/ic_home" />

    <item
        android:id="@+id/menu_member"
        android:title="멤버"
        android:icon="@drawable/ic_user" />

    <item
        android:id="@+id/menu_send"
        android:title="보내기"
        android:icon="@drawable/ic_send" />

    <item
        android:id="@+id/menu_profile"
        android:title="프로필"
        android:icon="@drawable/ic_profile" />

</menu>

activity_main.xml로 돌아가, 하단 메뉴 navigationview에서 app:menu에 메뉴 파일을 호출한다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    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"
    android:background="#F3F3F3"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/fragment_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/bottom_nav"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"/>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="@color/white"
        app:labelVisibilityMode="labeled"
        app:menu="@menu/bottom_nav_menu" />

</RelativeLayout>

해당 메뉴 각각 Fragment를 상속받는 클래스를 만들어 프래그먼트를 가지고 있는 액티비티(activity_main.xml)에 붙이고, 뷰가 생성되면 프래그먼트와 레이아웃을 연결시켜주는 작업을 해준다.

구조는 대충 이렇게 된다!

class HomeFragment: Fragment() {

    companion object {
        fun newInstance():HomeFragment {
            return HomeFragment()
        }
    }

    // 메모리에 올라갔을 때
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    // 프래그먼트를 안고 있는 액티비티에 붙었을 때
    override fun onAttach(context: Context) {
        super.onAttach(context)
    }

    // 뷰가 생성되었을 때, 프래그먼트와 레이아웃을 연결시켜주는 부분
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 레이아웃과 조각을 서로 연결
        val view = inflater.inflate(R.layout.fragment_home, container, false)
        return view
    }

}

이제 MainActivity에서 하단 바 메뉴를 누를 때마다 프래그먼트 조각들을 전환시켜 주어야 한다. 위의 companion object에 작성한 newInstance()를 통해 호출하는 곳에서 계속 나 자신을 반환시킨다고 이해하면 될 것 같다(싱글톤 패턴). 프래그먼트 조각을 교체해 주려면 add로 초기 셋팅을 해주고, 클릭할 때마다 replace를 써주면 된다.

class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {

    private lateinit var homeFragment: HomeFragment
    private lateinit var memberFragment: MemberFragment
    private lateinit var sendFragment: SendFragment
    private lateinit var profileFragment: ProfileFragment

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

        bottom_nav.setOnNavigationItemSelectedListener(this)

        homeFragment = HomeFragment.newInstance()
        // 처음에는 add로 추가해서 셋팅해주고
        supportFragmentManager.beginTransaction().add(R.id.fragment_frame, homeFragment).commit()

    }

    override fun onNavigationItemSelected(item: MenuItem): Boolean {
        // 클릭하면 계속 교체 replace
        when(item.itemId) {
            R.id.menu_home -> {
                Log.d(TAG, "MainActivity - menu_home 클릭")
                homeFragment = HomeFragment.newInstance()
                supportFragmentManager.beginTransaction().replace(R.id.fragment_frame, homeFragment).commit()
            }
            R.id.menu_member -> {
                Log.d(TAG, "MainActivity - menu_member 클릭")
                memberFragment = MemberFragment.newInstance()
                supportFragmentManager.beginTransaction().replace(R.id.fragment_frame, memberFragment).commit()
            }
            R.id.menu_send -> {
                Log.d(TAG, "MainActivity - menu_send 클릭")
                sendFragment = SendFragment.newInstance()
                supportFragmentManager.beginTransaction().replace(R.id.fragment_frame, sendFragment).commit()
            }
            R.id.menu_profile -> {
                Log.d(TAG, "MainActivity - menu_profile 클릭")
                profileFragment = ProfileFragment.newInstance()
                supportFragmentManager.beginTransaction().replace(R.id.fragment_frame, profileFragment).commit()
            }
        }

        return true
    }
}

🔹 프래그먼트 생명주기 (Fragment LifeCycle)

프래그먼트 안에 viewpager2를 넣어 슬라이드 화면을 만들려는 도중 만나게 된 NPE 🤯
(MainActivity가 아닌 Fragment를 상속받는 클래스에 넣은 상태이다.)

문제의 라인인데.. 대체 왜 null 오류가 발생한 건지 끙끙대다가 생각난 프래그먼트 생명주기...

  • onAttach()
    프래그먼트와 액티비티 연결 시점
    아직 프래그먼트가 완벽하게 생성된 상태가 아님!!
    인자로 context가 주어짐

  • onCreate()
    프래그먼트가 액티비티에 호출을 받아 생성되는 시점
    초기화해야 하는 리소스들을 여기서 초기화 처리해 주면 되는데,
    주의할 점은 액티비티와 달리 여기서 ui를 초기화할 수는 없다는 것...

  • onCreateView()
    레이아웃을 inflate 하는 곳, view 객체를 얻을 수 있어 초기화가 가능하고 view를 반환해야 함
    view나 viewGroup에 대한 ui 바인딩 작업 가능
    프래그먼트에서 ui를 그릴 때 호출되는 콜백

  • onActivityCreated()
    액티비티와 프래그먼트의 뷰가 모두 생성되고, 연결된 상태
    view를 변경하는 작업이 가능한 단계

  • onStart()
    프래그먼트가 사용자에게 보이기 전에 호출되는 함수 (표시 전 시점)

  • onResume()
    드디어 사용자에게 보이는 시점, 사용자와 상호작용 가능

  • onPause()
    프래그먼트는 사용자와의 상호작용을 중지함 (일시 정지 상태)
    부모 액티비티가 아닌 다른 액티비티가 올라오거나 다른 프래그먼트가 add 되는 경우
    ui 관련 처리를 정지하고, 중요한 데이터 저장

  • onStop()
    비표시 상태, 프래그먼트 기능 중지
    시스템에서 onStateInstance()를 호출하여 ui의 상태를 저장하므로 액티비티를 다시 띄우면 이전 상태 그대로 보여짐

  • onDestoryView()
    프래그먼트 관련 view가 제거될 때 실행
    액티비티에서 프래그먼트 생성 시 addToBackStack()을 요청했을 경우 인스턴스에 저장되어 있다가 프래그먼트를 다시 부를 때 onCreateView()를 실행하여 다시 화면에 보여지게 함

  • onDestroy()
    view가 제거된 후 프래그먼트가 완전히 소멸되기 직전 호출

  • onDetach()
    프래그먼트가 완전히 소멸, 액티비티와의 연결도 끊어짐

❗ 그리하여 onCreate()에서 아무리 뷰에 접근하여 작업하려고 해도 NPE가 뜰 수밖에 없었던 것... 슬라이드 변경 ui 작업을 해줘야 했기에 onActivityCreated() ~ onResume() 단계에 코드를 옮겨주면 정상적으로 작동한다. (생명주기 0순위로 알고가자 😥)


그리고 사소하지만 애먹었던 view 오류...

분명 Frame 레이아웃 안에서는 최하단에 위치한 view가 최상단에 보여지는 것으로 알고 있는데... 프로그래스바가 무슨 짓을 해도 앞으로 나와주지를 않는다 🔨 그리하여 찾은 해결 방법..

  1. 아래 코드를 프로그래스바 안에 넣어주면 된다. Widget.MaterialComponents.Button의 elevation의 기본 값이 2dp라서 최소 2dp 이상을 설정해 주어야 하며, API 레벨 21 이상에서만 동작한다고 한다.
android:elevation="2dp"
  1. <Widget.MaterialComponents.Button/>을 Constraint 레이아웃으로 감싼다.

이렇게 두 가지 방법으로 답답함을 해결할 수 있었다 😱
API 레벨에 맞춰서 편한 대로 골라서 쓰면 될 것 같다.

profile
🐰 피드백은 언제나 환영합니다

0개의 댓글