본문 바로가기

내일배움캠프

본 캠프 33일 차(Fragment,액티비티와 프래그먼트 데이터 전달)

프래그먼트

  • 프래그먼트란 액티비티 위에서 동작하는 사용자 인터페이스
  • 액티비티와 분리되어 독립적으로 동작할 수는 없다.
  • 여러 개의 프래그먼트를 하나의 액티비티에 조합하여 창이 많은 UI를 만들 수 있고, 하나의 프래그먼트를 여러 액티비티에서 재사용 할 수 있다.
  • 예를 들어 당근마켓 등 여러 앱에서 밑에 홈,마이페이지,상품 등 이렇게 밑이나 위 등 고정된 곳에 버튼들이 존재 하고 이걸 누르면 이 버튼들은 그대로인데 가운데 화면만 그에 따라 바뀌는걸 볼 수 있는데 이게 프래그먼트를 활용한 것이라고 한다.

 

프래그먼트와 액티비티의 비교

  • 액티비티는 시스템의 액티비티 매니저에서 인텐트를 해석해 액티비티간 데이터를 전달한다.
  • 프래그먼트는 액티비티의 프래그먼트매니저에서 매소드로 프래그먼트간 데이터를 전달한다.

 

프래그먼트를 사용하는 이유

  • 액티비티로 화면을 넘기는 것보다 프래그먼트로 일부만 바꾸는게 자원 이용량이 적어 속도가 빨라진다.
  • 액티비티의 복잡도를 줄일 수 있다.
  • 재사용 할 수 있어서 똑같은 액티비티를 만들 일이 없어진다.
  • 프래그먼트를 사용하면 최소 1개의 액티비티 안에서 프래그먼트 공간에 view만 집어넣으면 여러 액티비티를 만들지 않아도 여러 화면을 보여 줄 수 있다.

 

프래그먼트에 대해 레이아웃을 제공하려면 반드시 onCreateView() 콜백 메서드를 구현 해야한다.

라고 설명되었는데, 직접 만들어보니

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_third, container, false)
    }

이미 프래그먼트를 만들자마자 이렇게 다 만들어져 나온다.

프래그먼트를 일단 추가하는 방법으로 java/com.android.앱이름 -> new -> fragment -> fragment(blank) 로 여기서 이름만 바꿔서 만들었다.

 

 

액티비티에서 프래그먼트로, 프래그먼트에서 프래그먼트로 이동하기

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.apply {
            fragment1Btn.setOnClickListener{
                setFragment(FirstFragment())
            }
            fragment2Btn.setOnClickListener {
                setFragment(SecondFragment())
            }
        }
        setFragment(FirstFragment())
    }

    private fun setFragment(frag : Fragment) {
        supportFragmentManager.commit {
            replace(R.id.frameLayout, frag)
            setReorderingAllowed(true)
            addToBackStack("")
        }
    }
}

바인딩 하는건 저번 포스트에서 쓴거처럼 쓰면 되고 binding을 쓰고 버튼을 호출하면 되는데 binding을 더 쓰지 않게 apply을 써서 자기 자신을 호출 하게끔한 상태.

우선 setFragment 라는 프래그먼트를 가져오는 함수를 온크리에이트 밑에 따로 만들어 줘야한다. 다만 그냥 하면 저기 .commit부터 입력이 되지 않는데 이건 앱그라들에 implementation("androidx.fragment:fragment-ktx:1.6.2") 이거를 집어넣어주고 sync now를 눌러주면 이제 밑에를 다 쓸 수 있게 된다. 버전은 나중에 달라질순 있지만 androidx.fragment ...을 넣어야 한다는걸 기억해야 할 거 같다.

replace()는 예를 들어 replace(a,b)라고 하면 a라는 위치에 b로 바꿔 넣어준다는 함수이다.

이제 밑에 setReorderingAllowed(true) 는 애니메이션과 전환이 올바르게 작동하도록 트랜잭션과 관련된 프래그먼트의 상태 변경을 최적화 한다는데..그냥 일단 써야하는거 같다. 프래그먼트를 쓸 때 세트로 쓴다고 생각해야 할 거 같다.

그리고 addTobackStack은 이걸 넣어주지 않으면 back 버튼을 눌렀을 때 액티비티가 꺼질 수 있으니 backstack에 저장하는 용도로 넣는거 같다.

이제 저렇게 함수를 만든 뒤 버튼들에 함수를 넣고 그 안에 호출할 프래그먼트를 집어넣으면 끝이다.

 

 

프래그먼트의 데이터와 전달

  • 프래그먼트로 데이터를 전달할 때는 인스턴스를 생성하고 그 안에 메소드를 넣어 전달해야한다.
  • Bundle을 객체로 사용하여 데이터를 프래그먼트의 arguments(인자)로 설정하고 이 인자를 프래그먼트가 받아 사용한다.

 

액티비티에서 프래그먼트로 데이터 전달

        binding.run {
            fragment1Btn.setOnClickListener {
                //액티비티 -> firstfragment 로 데이터 전달
                val dataToSend = "Hello First Fragment! \n From Activity"
                val fragment = FristFragment.newInstance(dataToSend)
                setFragment(fragment)
            }

우선 데이터를 전달할 때는 그냥 화면을 이동할 때와는 다르게 메인에서 apply대신 run을 사용하게 된다. run을 사용하는 이유는 자기 자신을 호출만하고 끝내는게 아니라 동작을 해서 결과값을 리턴받기 위해서인거 같다.

setFragment라는 함수에 fragment(인스턴스로 정보를 넣은 fristfragment)넣은 값을 리턴받는 식인거 같다.

어쨋든 이렇게 하면

이렇게 정보가 온 것을 볼 수 있다.

다만 이렇게 fragment로 인자를 넣어 보낼 때는 좀 변경 사항이 있었다.

  • 우선 처음 프래그먼트를 만들 때 인자가 1,2 이렇게 총 두개가 생성되서 나온다.
  • 물론 그냥 화면만 바꾸는 거면 상관없지만 데이터를 전달 하기위해서는 1은 전달하고 2는 전달 안하고 이런식은 안되기에(물론 숨기거나 하는 식으로라면 모르겠지만 하나만 전달하는 방식은 존재하는 건지 아닌지도 모르겠다. 일단 지금 상태에서는 안된다...) 우선 여기서는 인자1 만 사용하였기에 2에 대한 내용은 프래그먼트에서 삭제해줘야 한다.
  • 인자를 두개로 할 거면 프래그먼트 xml에서 칸도 더 만들어서 할당해야 한다.
  • 프래그먼트1은 인자1개, 프래그먼트2는 인자2개 이런식으로 프래그먼트마다 인자를 다르게 설정 할 수도 있다.

 

프래그먼트에서 프래그먼트로 데이터 전달

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        //액티비티에서 firstfragment로 데이터 전달 받기
        binding.tvFrag1.text = param1

        //다른 프래그먼트로 데이터 넘기기
        binding.btnFrag1.setOnClickListener {
            val dataToSend = "Hellow Fragment2! \n From Fragment1"
            val dataToSend2 = "프래그먼트 1에서"
            val fragment2 = SecondFragment.newInstance(dataToSend,dataToSend2)
            requireActivity().supportFragmentManager.beginTransaction()
                .replace(R.id.frameLayout,fragment2)
                .addToBackStack(null)
                .commit()
        }

위에 액티비티에서 프래그먼트로 전달 할 때 프래그먼트에서 받는 방법에 대해서 안 쓰고 넘어왔는데 여기에 다 합쳐져 있다.

우선 데이터를 전달 받을 경우 override fun onCreateView 밑에 override fun onViewCreated()를 만들고 그 안에 바인딩으로 프래그먼트에 있는 받을곳 아이디를 받아오고 거기에 text를 호출하고 "= 인자" 를 넣으면 된다. 

 

그리고 프래그먼트에서 프래그먼트로 데이터를 넘길 때는 좀 더 신기한 것들을 사용한다.

우선 fragment2 로 데이터를 넘길 건데 프래그먼트2는 인자 2개를 사용하기로 하였다.

그래서 보낼 데이터 두개를 선언해 주고 액티비티에서 보낼 때 처럼 이번엔 프래그먼트2번이니 SecondFragment를 넣고 인스턴스를 호출해 데이터 만든 두개를 여기에 넣어준다.

이제 이후에 액티비티랑 다르게 좀 더 들어가는 구문이다. 액티비티에서는 addTobackStack에 ""을 넣고 null을 넣었는데 이는 null을 넣든 ""을 넣든 똑같이 작동은 되었다. 이건 프래그먼트에서도 동일 했다.

requireActivity()는 !! 처럼 null이 아니라고 보장을 해주는 느낌의 함수인 듯 하다. 액티비티처럼 바로 supportFragmentManager를 사용할 수 없어서 이렇게 넣어주는거 같다.

그리고 beginTransaction() 는 프래그먼트 매니저와 연결된 프래그먼트에 대해 일련의 편집작업을 시작한다는 것을 말한다고 한다.

마지막으로 액티비티에서는 .commit{} 안에 내용들이 들어가는데 프래그먼트에서는 .commit()으로 마무리된다. 이것은 커밋을 호출할 때 프래그먼트가 이미 상태를 저장하지 않았는지를 검사하는 함수라고 한다.

 

이렇게 코드로 보니 본문에 너무 길게 쓴거 같기도 하고 해서 한번 밑에 함수를 액티비티처럼 따로 만들어 보았다.

        binding.btnFrag1.setOnClickListener {
            val dataToSend = "Hellow Fragment2! \n From Fragment1"
            val dataToSend2 = "프래그먼트 1에서"
            val fragment2 = SecondFragment.newInstance(dataToSend,dataToSend2)
            setToFragment(fragment2)
        }
      }  
   
   
   private fun setToFragment(frag : Fragment) {
       requireActivity().supportFragmentManager.beginTransaction()
          .replace(R.id.frameLayout,frag)
          .addToBackStack(null)
          .commit()
    }

이렇게 강의를 보면서 한거를 조금이라도 다르게 해보려고 노력중이다...

 

 

프레그먼트에서 액티비트로 데이터 전달

  • 프래그먼트에서 액티비티로 데이터를 전달할 때는 콜백 인터페이스를 정의하고 해당 인터페이스를 액티비티가 구현하도록 해야한다. 프래그먼트는 이 인터페이스를 사용하여 액티비티에 데이터를 전달한다.
  • 따로 인터페이스와 그안에 함수를 우선 설정.
  • 인터페이스의 함수가 액티비티에 있어야 한다.
  • 프래그먼트에서의 생명주기에서 제일 앞에 있는 onAttach()를 만들고 여기에 인터페이스 함수가 메인에 있는지 확인 해주어야 한다.
interface FragmentDataListener {
    fun onDataReceived(data : String)
}

우선 FragmentDataListener 라는 인터페이스를 만들어주고 그 안에 onDataReceived라는 함수를 만들었다. 이제 이 함수를 액티비티에 따로 private fun onDataReceived() 로 만들어줘야 하는데 그 전에 class Activity : AppcompatActivity() { 

여기 부분에 Activity가 위에 만든 인터페이스를 상속받게 만들어줘야한다.

class MainActivity : AppCompatActivity(),FragmentDataListener {

    private lateinit var binding : ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

이런식으로 인터페이스도 추가해준다. 그 뒤

override fun onDataReceived(data: String) {
        //Fragment에서 받은 데이터를 처리
    }

이렇게 인터페이스의 함수를 선언해서 그 안에 데이터를 받아서 어떻게 처리할 지 넣으면 된다.

우선 토스트 메세지를 받는 거로 하면

    override fun onDataReceived(data: String) {
        //Fragment에서 받은 데이터를 처리
        Toast.makeText(this,data,Toast.LENGTH_SHORT).show()
    }

이렇게 쓰면 된다. 

이제 액티비티에서 받는거는 완료 되었으니 프래그먼트로 가서 보내기를 해보자.

class SecondFragment : Fragment() {
    //액티비티로 전달할 기능 함수 listener 선언
    private var listener : FragmentDataListener? = null

프래그먼트에서 인터페이스를 통해 넘겨야 하기 때문에 우선 프래그먼트 클래스 안에 인터페이스에 해당하는 변수를 선언 해 준다.

그리고 프래그먼트 생명주기에서 제일 먼저인 onAttach()를 만들고 메인액티비티에 인터페이스와 함수가 존재하는지 확인부터 해준다.

    override fun onAttach(context: Context) {
        super.onAttach(context)
        //SecondFragment 에서 Activity로
        //context가 메인택티비티에서 왔으니 메인액티비티에 FragmentDataListener가 있는지 체크(is)
        //초기화 하는 과정이라고 한다.
        if (context is FragmentDataListener) {
            listener = context
        }else {
            throw RuntimeException("$context must implement FragmentDataListener")
        }
    }

밑에 throw는 예외처리인 듯 하다.

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
            //현재 프래그먼트에서 액티비티로
        binding.btnActivity.setOnClickListener {
            val dataToSend = "Hello from SecondFragment!"
            listener?.onDataReceived(dataToSend)
        }
    }

onCreateView() 다음에 오는 onViewCreated에서 위에 선언한 listener와 함께 null을 포함할 수 있게 ?를 넣고 데이터를 만들어 넣고 인터페이스의 함수를 가져와서 그안에 데이터를 집어 넣으면 된다.

현재 액티비티에서 토스트 메세지로 Hello from SecondFragment! 만 뜨게끔 되어 있어서 실행 시켜보면

이렇게 토스트 메세지가 뜬다.

강의에는 없지만 문득 그냥 액티비티로 돌아가게끔 할 수 있을 지 궁금해서 찾아보았다.

그냥 Intent로 보내는건 같지만 여기서는 this를 쓸 수 없어서 그 자리에 getActivity를 넣으면 된다고 했는데 입력해보니 그냥 간단하게 activity만 쓰면 되는거로 업그레이드 된거 같다.

            val intent = Intent(activity,MainActivity::class.java)
            startActivity(intent)

간단하게 이렇게 넣으니 바로 된다.

토스트 메세지와 함께 화면도 액티비티로 바뀌었다.

 

요약

  • 프래그먼트는 액티비티 위에 여러개를 사용할 수 있고 다른 액티비티에서도 재사용 할 수 있다.
  • 프래그먼트는 액티비티의 복잡도를 줄여주어 좀 더 빨라진다.
  • 프래그먼트로 데이터를 전달 할 때는 인스턴스에 데이터를 넣어서 전달한다.
  • 프래그먼트에서 액티비티로 데이터 전달 할 때는 인터페이스를 선언하고 그 인터페이스의 함수가 액티비티에 있어야 한다. 거기에 프래그먼트에서 인터페이스를 통해 넘긴다.

 

어제 실습을 실패해서 한번 그대로 복붙도 해보았지만 실패해서 다시 처음부터 만들어 보았다. 일단 성공적으로 되긴하였고 아무래도 주말에도 나머지 강의를 듣고 다음주에는 과제를 시작해야 할 거 같다.

'내일배움캠프' 카테고리의 다른 글

본 캠프 35일 차  (0) 2024.01.09
본 캠프 34일 차  (1) 2024.01.08
본 캠프 32일차  (0) 2024.01.04
본 캠프 31일 차(ViewBinding)  (1) 2024.01.03
본 캠프 30일 차  (1) 2024.01.02