본문 바로가기

TIL

[TIL] Dialog Fragment / Dialog Fragment 데이터 Fragment로 이동

[오늘 배운 내용]

-1- Dialog Framgent로 Dialog 띄우기

  • 먼저 일반 Dialog와 Dialog Fragment의 차이점을 알아보자
  • LifeCycle관리
    • DialogFragment는 Fragment의 일부로 간주되어 Fragment의 LifeCycle을 따른다. 이에따라 액티비티나 프래그먼트와 함께 동작하며, 액티비티나 프래그먼트의 LifeCycle 변경에 따라 자동으로 처리 된다. 반면 일반 Dialog는 독립적인 객체로서 액티비티나 프래그먼트와 별개로 존재하며, LifeCycle을 직접 관리해야 한다.
  • 재사용성
    • DialogFragment는 재사용 가능한 컴포넌트이다. 여러 곳에서 동일한 다이얼로그를 사용할 수 있으며, 필요한 데이터 전달 및 결과 수신을 위해 인자 전달과 리스너 인터페이스 등을 활용할 수 있다. 반면에 Dialog는 보다 단순한 형태로 한번만 사용되거나 간단한 상황에서 주로 활요된다.
  • Configuration Changes 처리
    • Activity가 Configuration Changes(예 : 화면 회전) 으로 인해 재생성될 때 DialogFragment는 자동으로 상태를 보존하고 다시 생성된다. 이는 DialogFragment를 사용하면 화면 회전 등 상태 변경에 대해 안정적으로 처리할 수 있게 되는 것을 확인할 수 있다. 반면에 Dialog는 Configuration Changes 발생 시 직접 처리해야 되며, 보존 된 상태를관리하기 위한 추가적인 작업이 필요하다.
  • UI구성
    • DialogFragment는 Fragment로서 기본적인 레이아웃을 가질 수 있으며, 복잡한 UI 구성을 위해 xml레이아웃 파일을 사용할 수 있다. 이에 따라 DialogFragment 내부에서 여러개의 뷰나 위젯을 조합하여 다양한 형태의 Dialog를 만들 수 있다. 반면에 Dialog는 일반적으로 코드로 직접 UI를 구성하는 방식으로 사용 된다.
  • 모달/비모달
    • DialogFragment는 모달 다이얼로그(사용자가 다른 작업을 할 수 없고 해당 다이얼로그 외부의 상호작용이 차단된 형태)로 표시될 수도 있고, 비모달 다이얼로그(다른 작업과 동시에 상호작용 가능한 형태)로 표시될 수 있다. 이에 대한 설정은 setCancelable() 메서드를 통해 제어할 수 있다. 반면에 일반 Dialog는 주로 모달 형태로 동작된다.

 

 

 

 

DialogFragment 만들기

  • 먼저 res/layout/ 디렉토리 안에 xml파일을 생성해 DialogFragment의 UI를 먼저 구성해준다
  • UI 코드 작성이 마무리 되었으면 MyDialog kt.class파일 생성 후 DialogFragment 코드 작성

 

DialogFragment 생성 코드 

ShowDialogFragment.kt

class ShowDialogFragment : DialogFragment() {
    private var _binding: AddDialogBinding? = null
    private val binding get() = _binding!!

    interface DialogListener {
        // 해당 Dialog를 띄우는 Activity or Fragment에서 확인, 취소 버튼 이벤트 관리 하도록 Interface 생성
        fun onDialogPositiveClick(dialog: DialogFragment)
        fun onDialogNegativeClick(dialog: DialogFragment)
    }

    private lateinit var listener: DialogListener

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = parentFragment?.let {
                parentFragment as DialogListener
            } ?: context as DialogListener
        } catch (e: ClassCastException) {
            throw ClassCastException(
                (context.toString() + " 오류 ")
            )
        }
    }


    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        isCancelable = false
        _binding = AddDialogBinding.inflate(LayoutInflater.from(context))
        return AlertDialog.Builder(requireContext())
            .setView(binding.root)
            .create()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding.확인버튼ID.setOnClickListener {
            listener.onDialogPositiveClick(this)
            확인 버튼 클릭 시 이벤트
            dismiss()
            }
        }
        binding.취소버튼ID.setOnClickListener {
            listener.onDialogNegativeClick(this)
            취소 버튼 버튼 클릭 시 이벤트
            dismiss()
        }
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}

 

 

onAttach(){}

  • onCrate가 실행되기 전에 실행 되는 매소드, listener를 인스턴스화 시키고 host에 event를 보낸다.
  • host가 Activity일 경우 호스트의 context에, host가 Fragment일 경우 parentFragment에 interface가 없다면 예외를 반환한다.

 

onCreateDialog(){}

  • isCancelable
    • false로 설정할 시 Dialog가 바깥화면 터치 또는 뒤로가기 버튼 클릭시에도 사라지지 않음, dismiss()로만 사라짐.
    • 다이얼로그 외부 클릭시 Dialog가 꺼지게 하려면, isCancelable = true로 설정하거나, isCancelable을 안넣으면 기본값이 true다.
  • onCreateDialog()
    • DialogFragment의 onCreateDialog() 메서드를 오버라이드하여 Dialog를 생성한다.
  • return AlertDialog.Builder(requireContext())
    • return AlertDialog.Builder(requireContext()) 를 통해 인스턴스를 생성 하고 Activity또는 Fragment에 대한 Context객체를 반환하도록 한다.
  • setView(binding.root)
    • AlertDialog.Builder의 Dialog View를 미리 바인딩으로 인스턴스화 시켜놓은 binding.root 로 지정해준다
  • .create()
    • AlertDialog.Builder 로부터 Dialog 객체를 생성하고 반환한다.

 

 

 

DialogFragment 클릭 이벤트 적용하기

(1) DialogFragment에서 버튼 클릭 시 이벤트 추가

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding.dialogBtnSave.setOnClickListener {
            if (binding.dialogUserName.text.isEmpty() || binding.dialogUserPhoneNumber.text.isEmpty() || binding.dialogUserEmail.text.isEmpty()) {
                Toast.makeText(requireContext(), "이름, 번호, 이메일은 필수 입력사항입니다.", Toast.LENGTH_SHORT)
                    .show()
            } else {
                listener.onDialogPositiveClick(this)
                val addName = binding.dialogUserName.text.toString()
                val addNum = binding.dialogUserPhoneNumber.text.toString()
                val addEmail = binding.dialogUserEmail.text.toString()

                setFragmentResult(
                    DataKey.ADD_USER_NAME_KEY,
                    bundleOf(DataKey.ADD_USER_NAME_KEY to addName)
                )
                setFragmentResult(
                    DataKey.ADD_USER_NUM_KEY,
                    bundleOf(DataKey.ADD_USER_NUM_KEY to addNum)
                )
                setFragmentResult(
                    DataKey.ADD_USER_EMAIL_KEY,
                    bundleOf(DataKey.ADD_USER_EMAIL_KEY to addEmail)
                )
                dismiss()
            }
        }
        binding.dialogBtnCancel.setOnClickListener {
            listener.onDialogNegativeClick(this)
            dismiss()
        }
        return binding.root
    }
  • EditText가 비어있을 경우 처리.
    • 토스트 메시지 출력.
  • Positive 버튼 클릭시 이벤트 설정.
    • FragmentResultListener 를 통해 데이터 이동 시키려고 requestKey 와 BundleKey를 설정해두고 데이터를 이동시키려는 값을 넣어준다
  • Negative 버튼 클릭시 이벤트 설정
    • 별다른 이벤트 없이 dismiss() 로 Dialog 종료

 

 

FragmentA.kt

class ContactListFragment : Fragment(), ShowDialogFragment.DialogListener {

    val dataList = mutableListOf<ContactListFragmentData>()

    init {
        dataList.add (
            { ... }
    }

    override fun onDialogPositiveClick(dialog: DialogFragment) {
        Toast.makeText(dialog.context, "저장 됨", Toast.LENGTH_SHORT).show()
    }

    override fun onDialogNegativeClick(dialog: DialogFragment) {
        Toast.makeText(dialog.context, "취소 됨", Toast.LENGTH_SHORT).show()
    }

    private var _binding: ContactListViewBinding? = null
    private val binding get() = _binding!!


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val dialogResult = mutableListOf<String?>()
        setFragmentResultListener(DataKey.ADD_USER_NAME_KEY) { requestKey, bundle ->
            if (dialogResult.size < 3) {
                dialogResult.add(bundle.getString(DataKey.ADD_USER_NAME_KEY))
            } else {
                dialogResult[0] = bundle.getString(DataKey.ADD_USER_NAME_KEY)
            }
        }
        setFragmentResultListener(DataKey.ADD_USER_NUM_KEY) { requestKey, bundle ->
            if (dialogResult.size < 3) {
                dialogResult.add(bundle.getString(DataKey.ADD_USER_NUM_KEY))
            } else {
                dialogResult[1] = bundle.getString(DataKey.ADD_USER_NUM_KEY)
            }
        }
        setFragmentResultListener(DataKey.ADD_USER_EMAIL_KEY) { requestKey, bundle ->
            if (dialogResult.size < 3) {
                dialogResult.add(bundle.getString(DataKey.ADD_USER_EMAIL_KEY))
            } else {
                dialogResult[2] = bundle.getString(DataKey.ADD_USER_EMAIL_KEY)
            }
            dataList.add(
                ContactListFragmentData(
                    R.drawable.dialog_user_img,
                    dialogResult[0]!!,
                    dialogResult[1]!!,
                    dialogResult[2]!!,
                    false
                )
            )
        }

    }


    override fun onCreateView(
        { ... }
    }

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

★★★

  • DialogFragment를 띄우는 Fragment로 이동해서 DialogFragment의 Interface를 상속받아준다.
  • 상속받은 Interface 구현해준다.

 

 

FragmentA 에서 FloatingActionButton으로 Dialog 호출

위 FragmentA 코드내의 onCreateView () 부분

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = ContactListViewBinding.inflate(inflater, container, false) // Binding 초기화
        binding.fabAddUser.setOnClickListener {
            val showDialogFragment = ShowDialogFragment()
            showDialogFragment.show(parentFragmentManager, "addDialog")
        }
        return binding.root
    }

 

완성 작동화면

 

 

 


 

[오늘 복습한 내용]

1. 

 TextView, EditText 등 배경 꾸미기 ( 외각선, 테두리 둥글게만들기, 배경 gradient )

  • EditText및 TextView 등의 배경을 꾸미기 위해 res/drawable 폴더안에 xml파일을 생성해준다
  • Dialog에 들어가는 EditText의 배경을 꾸미기 위해서 xml 파일 이름을 dialog_edit_text_bg으로 생성해봤다. 파일이름은 본인이 원하는대로 지정해주면 된다. ex) main_title_tv_bg 등등... res폴더 내부에 있기 때문에 대문자 사용은 안됨.

 

  • 코드 작성은 <shape>   내부에 작성해준다 </shape>
<corners android:radius="20dp"/>
  • corners android:radius = 모서리 둥글기를 설정합니다

 

<solid android:color="@color/white" />
  • solid : 배경색을 설정.

 

<padding
        android:left="0dp"
        android:top="0dp"
        android:right="0dp"
        android:bottom="0dp"
        />
  • padding : 내부 여백 설정

 

<size
        android:width="270dp"
        android:height="60dp"
        />
  • size : item 의 크기 설정

 

<stroke
        android:width="3dp"
        android:color="@color/button_stroke"
        />
  • stroke : 외각선 설정 및 외각선 색상 설정

 

2.

 

3.

 


[오류,에러 등등]

1. ViewPager 안에 있는 Fragment 에서 floatingButton으로 Viewpager외부에 있는 Layout의 DialogFragment 띄우기.

  • 기존에 Activity에서 DialogFragment가 정상적으로 실행이 되는데, ViewPage안에 있는 Fragment에서 다른 Fragment로 이동하려고 하니까 정상적으로 작동이 되질 않는다.
  • Activity에서 
  • 작성 코드
class ShowDialogFragment:DialogFragment() {
    private var _binding: AddDialogBinding? = null
    private val binding get() = _binding!!

    interface DialogListener {
        fun onDialogPositiveClick(dialog:DialogFragment)
        fun onDialogNegativeClick(dialog:DialogFragment)
    }
    private lateinit var listener: DialogListener

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = parentFragment as DialogListener
        }catch (e:ClassCastException){
            throw ClassCastException(
                (context.toString() + " 필수요소 구현안됨")
            )
        }
    }

 

오류 Logcat

 

onAttach 내부의 코드를 다음과 같이 변경해 주었더니 해결되었다.

해결코드

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            listener = parentFragment?.let {
                parentFragment as DialogListener    
            } ?: context as DialogListener
        }catch (e:ClassCastException){
            throw ClassCastException(
                (context.toString() + " 필수요소 구현안됨")
            )
        }
    }

 

2. Fragment가 새로생성 되는 문제

  • 앱 실행 후 FloatingButton으로 Dialog 가 생성되고 Positive 버튼을 클릭하게 되면 RecyclerView가 있는 ContactListFragment가 보여지고 Dialog로 생성한 List Item이 추가된 줄 알았으나, 기존에 보여지는 Fragment1 위에 새로운 Fragment2 가 생성되고 새롭게 생성된 Fragment2에 List에만 Item이 추가된 것이였다.
  • 오늘 배운내용의 코드로 해결.

[느낀 점]

1. 기초부터 탄탄하게 꾸준히 누적복습 해야겠다.

 

2. 어렵다;

 

3. 근데 재밌다 시간이 너무 빠르다

 

 


[Reference]

 

// Fragment 데이터 이동

https://moon-i.tistory.com/entry/Fragment-Result-API

https://pgo-dev.medium.com/fragment-result-api-%EB%8F%99%EC%9D%BC%ED%95%9C-%ED%82%A4%EB%A1%9C-fragment-result-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-f4b84ba8301b

https://developer.android.com/guide/fragments/communicate#fragment-result

// DialogFragment

https://chachas.tistory.com/81