MOBILE PROGRAMING

[모바일 프로그래밍] Listener를 이용한 두 프래그먼트 간의 통신

ch010104 2025. 11. 17. 21:06

실습 목표 및 내용

  1. 텍스트 전달 앱: Fragment A의 EditText에 텍스트를 입력하고 "OK" 버튼을 누르면 해당 텍스트가 Fragment B의 EditText(혹은 TextView)에 표시됩니다. 이 통신은 양방향으로 구현됩니다.
  2. 카운터 앱: Fragment A의 "Inc." 버튼을 클릭할 때마다 Fragment B에 있는 "Count" 값이 1씩 증가합니다.

1. 텍스트 전달 앱: Interface를 이용한 통신

  • 이 실습에서는 Interface(인터페이스)를 사용하여 프래그먼트와 액티비티 간의 통신을 구현
  • 데이터 흐름은 [Fragment A] → [Activity] → [Fragment B] 순서

1. 송신 측 (Fragment A)

  • 리스너 인터페이스 정의: 데이터를 보내는 Fragment A 내부에 리스너 인터페이스(예: FragmentAListener)를 정의
  • 콜백 메서드 정의: 이 인터페이스에는 데이터를 전달할 콜백 메서드(예: onInputASent(input: CharSequence))가 포함
  • 리스너 등록 (onAttach): onAttach() 콜백에서 context(즉, 호스팅 Activity)가 이 리스너 인터페이스를 구현했는지 확인
    • 구현했다면, 해당 context를 프래그먼트의 리스너 멤버 변수에 저장
    • 구현하지 않았다면, RuntimeException을 발생시켜 개발자에게 알림
  • 데이터 전송 (이벤트 발생 시): 사용자가 버튼을 클릭하면 , 저장된 리스너의 콜백 메서드(예:listener.onInputASent(...))를 호출하여 데이터를 Activity로 전송

2. 중재자 (Host Activity)

  • 인터페이스 구현: MainActivity는 Fragment A (그리고 Fragment B)에서 정의한 리스너 인터페이스(예: FragmentAListener)를 implements
  • 콜백 메서드 구현: 인터페이스의 콜백 메서드(예: onInputASent)를 override하여 Fragment A로부터 데이터를 수신
  • 데이터 전달: 수신한 데이터( input )를 Fragment B로 전달해
    • FragmentManager를 사용해 Fragment B의 인스턴스를 찾음 (예: findFragmentById).
    • 찾아낸 Fragment B 인스턴스의 공개 메서드(예: updateEditText(input))를 호출하여 데이터를 전달

3. 레이아웃 (XML)

  • MainActivity 레이아웃: activity_main.xml 파일에는 Fragment A와 Fragment B를 각각 담을 수 있는 2개의 FragmentContainerView가 포함
  • Fragment 레이아웃: fragment_a.xml (및 fragment_b.xml)에는 각 프래그먼트의 UI(예: EditText 1개와 Button 1개)가 정의
// MainActivity.kt
package com.cookandroid.fragementcommunicationexcercise

import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.FragmentTransaction
import com.cookandroid.fragementcommunicationexcercise.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity(), FragmentA.FragmentAListner, FragmentB.FragmentBListner {
    private lateinit var bindingMain : ActivityMainBinding
    private lateinit var fragmentA: FragmentA
    private lateinit var fragmentB: FragmentB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        enableEdgeToEdge()
//        setContentView(R.layout.activity_main)
//        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
//            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
//            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
//            insets
//        }

        bindingMain = ActivityMainBinding.inflate(layoutInflater)
        setContentView(bindingMain.root)

        fragmentA = FragmentA()
        fragmentB = FragmentB()

        val transaction : FragmentTransaction = supportFragmentManager.beginTransaction()

        transaction.add(R.id.container_a, fragmentA)
        transaction.add(R.id.container_b, fragmentB)
        transaction.addToBackStack(null)
        transaction.commit()
    }

//    fun onRecievedFromFragA(input : CharSequence){
//        Log.i("프래그먼트간 데이터 전달", "Recieved msg: $input")
//        val fragmentManager : androidx.fragment.app.FragmentManager = supportFragmentManager
//        val fragB : FragmentB = fragmentManager.findFragmentById(R.id.container_b) as FragmentB
//        fragB.updateEditText(input)
//    }

    override fun onInputASent(input: CharSequence) {
        Log.i("프래그먼트간 데이터 전달", "Recieved msg: $input")
        val fragmentManager : androidx.fragment.app.FragmentManager = supportFragmentManager
        val fragB : FragmentB = fragmentManager.findFragmentById(R.id.container_b) as FragmentB
        fragB.updateEditText(input)
    }

    override fun onInputBSent(input: CharSequence) {
        Log.i("프래그먼트간 데이터 전달", "Recieved msg: $input")
        val fragmentManager : androidx.fragment.app.FragmentManager = supportFragmentManager
        val fragA : FragmentA = fragmentManager.findFragmentById(R.id.container_a) as FragmentA
        fragA.updateEditText(input)
    }
}
// FragmentA.kt
package com.cookandroid.fragementcommunicationexcercise

import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.cookandroid.fragementcommunicationexcercise.databinding.FragmentABinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [FragmentA.newInstance] factory method to
 * create an instance of this fragment.
 */
class FragmentA : Fragment() {

    private lateinit var bindingFragmentA : FragmentABinding

    private lateinit var listner : FragmentAListner

    public interface FragmentAListner {
        fun onInputASent(input : CharSequence)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        if(context is FragmentAListner){
            listner = context
        } else {
            throw RuntimeException("$context must implement FragmentAListner")
        }

    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        bindingFragmentA = FragmentABinding.inflate(inflater, container, false)
        return bindingFragmentA.root
    }

    public fun updateEditText(newText : CharSequence) {
        bindingFragmentA.editText.setText(newText)
    }

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

        bindingFragmentA.buttonOk.setOnClickListener(object : View.OnClickListener{
            override fun onClick(v: View?) {
//                val mainActivity : MainActivity = activity as MainActivity
//                mainActivity.onRecievedFromFragA(bindingFragmentA.editText.text)

                listner.onInputASent(bindingFragmentA.editText.text)
                bindingFragmentA.editText.text.clear()
            }
        })
    }


//    companion object {
//        /**
//         * Use this factory method to create a new instance of
//         * this fragment using the provided parameters.
//         *
//         * @param param1 Parameter 1.
//         * @param param2 Parameter 2.
//         * @return A new instance of fragment FragmentA.
//         */
//        // TODO: Rename and change types and number of parameters
//        @JvmStatic
//        fun newInstance(param1: String, param2: String) =
//            FragmentA().apply {
//                arguments = Bundle().apply {
//                    putString(ARG_PARAM1, param1)
//                    putString(ARG_PARAM2, param2)
//                }
//            }
//    }
}
// FragmentB.kt
package com.cookandroid.fragementcommunicationexcercise

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.cookandroid.fragementcommunicationexcercise.databinding.FragmentBBinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [FragmentB.newInstance] factory method to
 * create an instance of this fragment.
 */
class FragmentB : Fragment() {
    private lateinit var bindingFragmentB : FragmentBBinding
    // TODO: Rename and change types of parameters
//    private var param1: String? = null
//    private var param2: String? = null

//    override fun onCreate(savedInstanceState: Bundle?) {
//        super.onCreate(savedInstanceState)
//        arguments?.let {
//            param1 = it.getString(ARG_PARAM1)
//            param2 = it.getString(ARG_PARAM2)
//        }
//    }

    private lateinit var listner : FragmentBListner

    public interface FragmentBListner {
        fun onInputBSent(input : CharSequence)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        if(context is FragmentBListner){
            listner = context
        } else {
            throw RuntimeException("$context must implement FragmentAListner")
        }

    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
//        return super.onCreateView(inflater, container, savedInstanceState)
        bindingFragmentB = FragmentBBinding.inflate(layoutInflater)
        return  bindingFragmentB.root
    }

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

        bindingFragmentB.buttonOk.setOnClickListener(object : View.OnClickListener{
            override fun onClick(v: View?) {
//                val mainActivity : MainActivity = activity as MainActivity
//                mainActivity.onRecievedFromFragA(bindingFragmentA.editText.text)

                listner.onInputBSent(bindingFragmentB.editText.text)
                bindingFragmentB.editText.text.clear()
            }
        })
    }

    public fun updateEditText(newText : CharSequence) {
        bindingFragmentB.editText.setText(newText)
    }

//    companion object {
//        /**
//         * Use this factory method to create a new instance of
//         * this fragment using the provided parameters.
//         *
//         * @param param1 Parameter 1.
//         * @param param2 Parameter 2.
//         * @return A new instance of fragment FragmentB.
//         */
//        // TODO: Rename and change types and number of parameters
//        @JvmStatic
//        fun newInstance(param1: String, param2: String) =
//            FragmentB().apply {
//                arguments = Bundle().apply {
//                    putString(ARG_PARAM1, param1)
//                    putString(ARG_PARAM2, param2)
//                }
//            }
//    }
}
// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/container_a"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/container_b"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>
// fragment_a.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/holo_green_light"
    android:gravity="center_horizontal"
    tools:context=".FragmentA">

    <!-- TODO: Update blank fragment layout -->
    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    
    <Button
        android:id="@+id/button_ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="OK"/>

</LinearLayout>
// fragment_b.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:background="@android:color/holo_blue_bright"
    tools:context=".FragmentB">

    <!-- TODO: Update blank fragment layout -->
    <EditText
        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/button_ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="OK"/>

</LinearLayout>

2. 카운트 앱: Interface를 이용한 통신

 

  • 실습 목표: Fragment A의 버튼을 클릭하면 Fragment B의 숫자가 1씩 증가하도록 구현
  • UI 구성:
    • Fragment A: "Inc." (Increase) 버튼이 있음
    • Fragment B: "Count: [숫자]" 형태의 텍스트가 있음
  • 구현 방식:
    1. 사용자가 Fragment A의 "Inc." 버튼을 클릭
    2. Fragment A는 23~27페이지에서 배운 인터페이스 방식을 사용해 이 클릭 이벤트를 호스팅 Activity로 전달
    3. Activity는 이 이벤트를 수신한 뒤, Fragment B의 참조를 찾아 "카운트를 증가시키라"는 신호(예: 공개 메서드 호출)를 보냄
    4. Fragment B는 신호를 받아 내부의 카운트 변수 값을 1 증가시키고, 화면의 텍스트(TextView)를 갱신
// MainActivity.kt
package com.cookandroid.fragmentexcercise3

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentTransaction
import com.cookandroid.fragmentexcercise3.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity(), FragmentA.FragmentAListner{

    private lateinit var bindingMain : ActivityMainBinding
    private lateinit var fragmentA: FragmentA
    private lateinit var fragmentB: FragmentB

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

        // Fragment 초기화
        fragmentA = FragmentA()
        fragmentB = FragmentB()

        // Fragment를 컨테이너에 추가
        val transaction : FragmentTransaction = supportFragmentManager.beginTransaction()
        transaction.add(R.id.container_a, fragmentA)
        transaction.add(R.id.container_b, fragmentB)
        transaction.addToBackStack(null)
        transaction.commit()
    }

    // FragmentA의 버튼 클릭 시 호출되는 메서드
    override fun onButtonClicked() {
        // FragmentB의 카운트를 증가시킴
        fragmentB.incrementCount()
    }
}

 

// FragmentA.kt
package com.cookandroid.fragmentexcercise3

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.cookandroid.fragmentexcercise3.databinding.FragmentABinding

// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

/**
 * A simple [Fragment] subclass.
 * Use the [FragmentA.newInstance] factory method to
 * create an instance of this fragment.
 */
class FragmentA : Fragment() {

    private lateinit var bindingFragmentA : FragmentABinding

    private lateinit var listner : FragmentAListner

    public interface FragmentAListner {
        fun onButtonClicked()
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        if(context is FragmentAListner){
            listner = context
        } else {
            throw RuntimeException("$context must implement FragmentAListner")
        }

    }
    // TODO: Rename and change types of parameters
//    private var param1: String? = null
//    private var param2: String? = null
//
//    override fun onCreate(savedInstanceState: Bundle?) {
//        super.onCreate(savedInstanceState)
//        arguments?.let {
//            param1 = it.getString(ARG_PARAM1)
//            param2 = it.getString(ARG_PARAM2)
//        }
//    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        bindingFragmentA = FragmentABinding.inflate(inflater, container, false)

        // 버튼 클릭 리스너 설정
        bindingFragmentA.BtnA.setOnClickListener {
            listner.onButtonClicked()
        }

        return bindingFragmentA.root
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment FragmentA.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            FragmentA().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}
// FragmentB.kt
package com.cookandroid.fragmentexcercise3

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView

class FragmentB : Fragment() {

    private var count = 0
    private lateinit var tvCount: TextView

//    interface FragmentBListner {
//        // FragmentB에서 필요한 경우 MainActivity에 알릴 메서드를 여기에 추가
//    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_b, container, false)

        tvCount = view.findViewById(R.id.tvCount)
        updateCount()

        return view
    }

    // MainActivity에서 호출할 수 있는 public 함수
    fun incrementCount() {
        count++
        updateCount()
    }

    private fun updateCount() {
        tvCount.text = "Count: $count"
    }
}
// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/container_a"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/container_b"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>
// fragment_a.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:background="@android:color/holo_green_light"
    tools:context=".FragmentA">

    <!-- TODO: Update blank fragment layout -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="24dp"
        android:text="FragmentA"/>

    <Button
        android:id="@+id/BtnA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Inc"/>

</LinearLayout>
// fragment_b.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:background="@android:color/holo_blue_bright"
    android:orientation="vertical"
    tools:context=".FragmentB">

    <!-- TODO: Update blank fragment layout -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="24dp"
        android:text="FragmentB"/>

    <TextView
        android:id="@+id/tvCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24dp"
        android:text="Count: 0" />

</LinearLayout>