혼자 공부하면서 정리하고 싶은 부분을 작성한 글입니다 👀
함께 공부하는 사람에게는 도움이 되었으면 좋겠고,
혹시 제가 잘못 이해한 부분이 있다면 알려주시면 감사하겠습니다 💌
그동안 하단바는 하드코딩해서 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
}
}
프래그먼트 안에 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가 최상단에 보여지는 것으로 알고 있는데... 프로그래스바가 무슨 짓을 해도 앞으로 나와주지를 않는다 🔨 그리하여 찾은 해결 방법..
android:elevation="2dp"
이렇게 두 가지 방법으로 답답함을 해결할 수 있었다 😱
API 레벨에 맞춰서 편한 대로 골라서 쓰면 될 것 같다.