안드로이드(코틀린) 채팅앱 만들기 - MainActivity 1

박준식·2022년 8월 5일
0

Catting

목록 보기
3/8
MainActivity는 프레그먼트 전환에 사용될 ViewPager2와 3개의 TabItem을 가진 TabLayout으로 구성된다. MainActivity가 실행되면 우선 로그인 액티비티인 SignInActivity가 실행된다. SignActivity는 사용자의 정보를 포함하는 UserInfo클래스 데이터를 반환하고 이를 토대로 프레그먼트를 구성한다. 본문에서는 ViewPager2와 TabLayout을 연결하는 법과 그 과정에서 생긴 문제점을 정리하고자 한다.

1. 전체 코드

  • activity_main.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=".MainActivity">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewPager"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@color/main_white"
            app:layout_constraintBottom_toTopOf="@+id/main_tab"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <com.google.android.material.tabs.TabLayout
            android:id="@+id/main_tab"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/main_orange"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:tabIndicatorColor="@color/main_orange"
            app:tabIndicatorGravity="stretch"
            app:tabRippleColor="@android:color/transparent">
    
            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:icon="@drawable/ic_baseline_chat_bubble_24" />
    
            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:icon="@drawable/ic_baseline_person_outline_24" />
    
            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:icon="@drawable/ic_baseline_folder_open_24" />
        </com.google.android.material.tabs.TabLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
  • MainActivity.kt
  • package com.example.catting
    
    import android.content.Intent
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import androidx.activity.result.ActivityResultLauncher
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.fragment.app.Fragment
    import androidx.fragment.app.FragmentActivity
    import androidx.viewpager2.adapter.FragmentStateAdapter
    import com.example.catting.databinding.ActivityMainBinding
    import com.google.android.material.snackbar.Snackbar
    import com.google.android.material.tabs.TabLayout
    import com.google.android.material.tabs.TabLayoutMediator
    import com.google.gson.Gson
    
    class MainActivity : AppCompatActivity() {
    	// 뷰 바인딩
        val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    
        lateinit var signInResult :ActivityResultLauncher<Intent>
        lateinit var api: RetrofitApplication
      
    	// 사용자 정보, SignInActivity를 통해 사용자 정보를 받기 전에 MainActivity와 그 하위의 프레그먼트의 onCreate가 실행되므로 lateinit은 불가능
      	// 임시값을 지정, 로그인에 성공하면 SignInActivity의 반환값으로 바뀜
        var userInfo: UserInfo = UserInfo(null,null,null, arrayListOf<CatProfile>())
      	// 뒤로가기를 두번누르면 앱 종료를 위한 마지막으로 뒤로가기를 누른 시간을 저장하는 변수
        var mBackWait:Long = 0
    
        init{
            instance = this
        }
    
      	// 타 Activity에서 MainActivity의 함수 및 변수를 사용하기 위함, mainActivity = MainActivity.getInstance() 식으로 사용
        companion object{
            private var instance:MainActivity? = null
            var isChattingFragmentNeedRefresh = true
            var isUserInfoFragmentNeedRefresh = true
            var isCatInfoFragmentNeedRefresh = true
            fun getInstance(): MainActivity? {
                return instance
            }
        }
    
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
    
      		// Rest API를 사용한 통신을 위해 만든 RetrofitApplication 클래스를 통해 생성한 인스턴스
            api = RetrofitApplication.create()
    
      		// Activity Result API를 사용한 SignInActivity의 반환값을 처리할 방식 정의
            signInResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if(it.resultCode == RESULT_OK) {  // 정상적인 로그인이면
                    val data: Intent? = it.data
    				// SignInActivity가 반환한 UserInfo형 변수를 intent에서 꺼내어 저장
                    userInfo = data?.getParcelableExtra<UserInfo>("userInfo")!!
                }
                else if(it.resultCode == RESULT_FIRST_USER){  // 첫 로그인이면 
                    binding.mainTab.selectTab(binding.mainTab.getTabAt(1)) // UserInfoFragment로 전환
                }
            }
    
    		// SignInActivity를 실행하기 위한 intent
            val intent = Intent(this@MainActivity, SignInActivity::class.java) 
    		// MainActivity가 실행되면 자동으로 SignInActivity 실행
            signInResult.launch(intent) 
    
            with(binding) {
                // 1. 페이지 데이터를 로드
                val fragmentList =
                    listOf<Fragment>(ChattingFragment(), UserInfoFragment(), CatInfoFragment())
                // 2. 어댑터 생성
                val mainPagerAdapter = MainFragmentPagerAdapter(fragmentList, this@MainActivity)
                // 3. 어댑터와 뷰 페이저 연결
                viewPager.adapter = mainPagerAdapter
                // 4. 탭 메뉴 구성요소 생성
                val mainFragmentName = listOf(R.drawable.ic_baseline_chat_bubble_24,
                    R.drawable.ic_baseline_person_outline_24, R.drawable.ic_baseline_folder_open_24)
                // 5. 탭 레이아웃과 뷰 페이저 연결
                TabLayoutMediator(mainTab, viewPager){ tab, position->
                    tab.setIcon(mainFragmentName[position])
                }.attach()
    
                // 뷰 페이저 스와이프 막기
                viewPager.isUserInputEnabled = false
                // 탭 레이아웃 선택시 수행 동작 설정
                mainTab.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener{
                    override fun onTabSelected(tab: TabLayout.Tab?) {
                        // 뷰 페이저 화면 전환 애니메이션 제거
                        tab?.position?.let{viewPager.setCurrentItem(it, false)}
        				// 탭 레이아웃 선택시 icon을 바꿔서 선택됨을 표시
                        when(tab!!.position){
                            0->tab.setIcon(R.drawable.ic_baseline_chat_bubble_24)
                            1->tab.setIcon(R.drawable.ic_baseline_person_24)
                            2->tab.setIcon(R.drawable.ic_baseline_folder_24)
                        }
                    }
    
                    override fun onTabReselected(tab: TabLayout.Tab?) {
                    }
    
                    override fun onTabUnselected(tab: TabLayout.Tab?) {
       					// 선택 해제시 icon을 원상복귀
                        when(tab!!.position){
                            0->tab.setIcon(R.drawable.ic_baseline_chat_bubble_outline_24)
                            1->tab.setIcon(R.drawable.ic_baseline_person_outline_24)
                            2->tab.setIcon(R.drawable.ic_baseline_folder_open_24)
                        }
                    }
                })
    
            }
        }
    
        override fun onBackPressed() {
    		// 뒤로가기 버튼을 일정시간 사이에 처음 누르면
            if(System.currentTimeMillis() - mBackWait >=2000 ) { 
                mBackWait = System.currentTimeMillis()
                Snackbar.make(binding.viewPager,"뒤로가기 버튼을 한번 더 누르면 종료됩니다.",Snackbar.LENGTH_LONG).show()
    		// 뒤로가기 버튼을 빠르게 두번 연달아 누르면
            } else {
                finish()
            }
        }
    	
        // ChattingActivity를 여는 함수
        fun openChattingActivity(catProfile: CatProfile) {
            val intent = Intent(this@MainActivity, ChattingActivity::class.java).apply {
    		    // 사진이 포함되어 사이즈가 큰 CatProfile은 일반 put/getExtra로 주고 받을 수 없기에 putLargeExtra를 따로 만들어 사용함
                putLargeExtra("CatProfile",catProfile) 
            }
            startActivity(intent)
        }
        
    	// SettingActivity를 여는 함수
        fun openSettingActivity(){
            val intent = Intent(this@MainActivity, SettingActivity::class.java)
            startActivity(intent)
        }
    }
    
    // ViewPager2를 위한 어댑터
    class MainFragmentPagerAdapter(val fragmentList: List<Fragment>, fragmentActivity: FragmentActivity)
        : FragmentStateAdapter(fragmentActivity){
        override fun getItemCount() = fragmentList.size
        override fun createFragment(position: Int) = fragmentList[position]
    }

2. 정리

  • ViewPager2와 TabLayout 연결하기
  •     ...
        	with(binding) {
                // 1. 페이지 데이터를 로드
                val fragmentList =
                    listOf<Fragment>(ChattingFragment(), UserInfoFragment(), CatInfoFragment())
                // 2. 어댑터 생성
                val mainPagerAdapter = MainFragmentPagerAdapter(fragmentList, this@MainActivity)
                // 3. 어댑터와 뷰 페이저 연결
                viewPager.adapter = mainPagerAdapter
                // 4. 탭 메뉴 구성요소 생성
                val mainFragmentName = listOf(R.drawable.ic_baseline_chat_bubble_24,
                    R.drawable.ic_baseline_person_outline_24, R.drawable.ic_baseline_folder_open_24)
                // 5. 탭 레이아웃과 뷰 페이저 연결
                TabLayoutMediator(mainTab, viewPager){ tab, position->
                    tab.setIcon(mainFragmentName[position])
                }.attach()
    		}    
        ...
        
    // ViewPager2를 위한 어댑터
    class MainFragmentPagerAdapter(val fragmentList: List<Fragment>, fragmentActivity: FragmentActivity)
        : FragmentStateAdapter(fragmentActivity){
        override fun getItemCount() = fragmentList.size
        override fun createFragment(position: Int) = fragmentList[position]
    }
    
    1. ViewPager가 출력할 프래그먼트들을 담은 list를 생성한다.
    2. FragmentStateAdapter를 상속받은 클래스를 만들어 ViewPager2를 위한 어댑터를 생성한다. 이때 ViewPager가 출력할 프래그먼트들을 담은 list와 해당 ViewPager가 포함된 액티비티를 넘겨줘야 한다.
    3. 해당 어댑터를 ViewPager와 연결한다.
    4. TabLayout의 구성요소를 담은 list를 생성한다. 각 요소는 순서에 맞게 각 프래그먼트와 연결된다.
    5. TabLayout과 ViewPager를 TabLayoutMediator로 연결한다. 이때 TabLayout의 요소를 설정해준다.

3. 주의사항

  • TabItem은 ID를 지정해주면 안된다!!
    • 에러코드
    • java.lang.RuntimeException: Unable to start activity ComponentInfo{...}: java.lang.NullPointerException: Missing required view with ID: ...
    • 발생경위
    • 처음에 TabItem을 구분하고 뷰 바인딩으로 접근하기 위해 ID를 할당하였는데 Runtime에 NullPointerException이 발생했다.
    • 원인
    • TabItem에 ID를 지정한 것이 문제였다. Material Design 라이브러리에서는 지원하지 않는 기능인 것 같다.
    • 해결방법
    • TabItem의 ID를 제거하면 된다. TabItem에 대한 접근은 TabLayout.tab.position으로 index형태로 접근하면 된다.

0개의 댓글