Android

[삽질] Dialog 커스텀해서 쓰기 ! (input있는 dialog)

김한토 2024. 6. 12. 03:23
반응형

졸업 프로젝트에서 어쩌다보니 전반적인 XML 구현을 하게 되었다.
얼마 안되는 화면이지만 까다로웠던 부분이 꽤 있었따.
 
그중에서 제일 기억에 남는 Dialog

 
이번 프로젝트에서 핵심은 링크를 저장할때, 링크에 제목과, 설명, 그리고 태그를 달 수 있따는 것이다.
태그를 달면 태그별로 링크들을 모아볼 수 있다. 자세한 설명은 프로젝트 뽀개기 포스팅에서 ..
 
아무튼 태그 창을 클릭하면 dialog가 나오고 태그를 선택할 수 있고, ADD버튼을 누르면 새로운 태그를 추가하는 방식이었다. material Design의 builder을 이용해서 간단하게 구현하였는데
 
PM님께서 태그를 추가하는 방식이 상당히 비효율적이라고,  
 

 
창 한켠을 새로운 태그를 받는 곳으로 바꿔주실 수 있냐고 여쭤보셨다..
 
처음에는 할게 산더미인데 굳이? 라는 생각이 들고, 다시 코드를 짤 생각에 정신이 아득해졌다.
개발은 참.. 아예 백지에서 구현하는건 괜찮은데 했던 것을 다 허물고 새로 하는 그 과정..이 정말 고통스러운 것 같다..
 
하지만 PM님 말을 듣고나니 정말 비효율적이고 세세한 불편함이 있었따.
1. 태그 추가하려면 add버튼을 누른다 -> 태그 입력한다. (여까지는 어케 봐줄 수 있음)
2. 근데 태그를 추가하고 나면 초기 builder가 꺼지는데(태그 선택창이 냅다 꺼져버림) 태그 수정이나, 추가하려면 태그 창을 또 눌러야함
3. 태그 추가할게 여러개다? 1,2번 반복 ;;
 
유저 입장에선 상당한 번거로움이다..
 
그래서 해보자라는 생각으로 구상을 해보았다. 
 
구상1. Material Design 
메테리얼 디자인이 정말 편하게 세세한 기능들은 이미 되어있기 때문에 사용할 수 있으면 좋을 거 같아 자료를 찾아보았따.
 
https://m2.material.io/components/dialogs

Material Design

Build beautiful, usable products faster. Material Design is an adaptable system—backed by open-source code—that helps teams build high quality digital experiences.

m3.material.io

 
기존에는 Confirmation dialog를 사용했었는데
 
simple dialog처럼 추가칸을 만든다면? 
 
시도해 보았지만 오히려 material 디자인의 기본 테마나 틀이 어느정도 정해져 있어서 원하는대로 커스텀하기가 어려웠다.
 
구상 2. Fragment 만들기
 
그냥 만드는게 편하겠다 싶어 레이아웃을 만들었따.
 

 
태그 리스트 창은 리사이클러뷰를 사용했고 edittext를 사용해서 입력창을 만들었따.
 
여까진 순조로웠으나 문제는 다이얼로그를 띄우는 창인 AddHookActivity에서 발생하였다.
 

정상 화면

 
태그를 선택하고 OK 버튼을 누르면 액티비티에 선택되었던 태그 리스트가 #태그1 #태그2 이런식으로 나오고
액티비티에서 태그선택 창을 누르면 화면에 있던 #태그1 #태그2 들이 선택되어 나와야하는데 반영이 안되고 있었다.
 

문제화면

 
 
처음에는 챗지피티한테 이 문제를 ㄱㅖ속 물어봤는데 코드 자체에는 특별한 오류가 없다는 말만 반복할 뿐이었다.
 
그래서 근본적인 데이터 흐름을 생각해봤다.
 
AddHookActivity -> TagSelectionListener -> TagListFragment
 
AddHookActivity에서 사용자가 태그를 선택하면, TagSelectionListener 인터페이스를 통해 이벤트가 발생. 그리고 TagListFragment는 이 인터페이스를 구현하고 있어서, 선택된 태그를 받아서 처리
 
이 루틴인데 TagListFragment에 리사이클러뷰에 들어갈 리스트를 정의하고 여기에 뷰모델에서 바로 값을 받아오고 있었다.
 

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

        viewModel.loadFindMyTags()
        viewModel.tagData.observe(viewLifecycleOwner) { tagData ->
            tagData?.let {
                for (tag in tagData.tag) {
                    tag.displayName?.let { displayName ->
                        multiChoiceList[displayName] = false
                    }
                }
                val adapter = TagListAdapter(requireContext(), multiChoiceList)
                binding.lvTags.adapter = adapter
                binding.lvTags.layoutManager = LinearLayoutManager(requireContext())
            }
        }

 
유저가 추가하기 버튼을 눌러 새로운 태그들이 데이터 베이스에 반영이 되지 않는 이상 다시 태그 선택창을 늘렀을때 체크박스에 반영이 안되었던 것이다...
 
1. 액티비티에서 받아오는 값과, viewModel에서 받아오는 값이 서로 상이하여 액티비티 태그창에도 값이 잘 안나오고 있었따.
 
2. Fragment의 멤버 변수를 직접 초기화해서 Fragment의 생성시점에 모두 초기화가 된다. 그래서 Fragment가 재생성 될때마다 값들이 초기화가 되고 있다.
 
기존코드 (TagFragment였던것)

private val apiServiceManager by lazy { ApiServiceManager() }
private val viewModelFactory by lazy { ViewModelFactory(apiServiceManager) }
private val viewModel: MainViewModel by lazy {
    ViewModelProvider(
        this,
        viewModelFactory
    )[MainViewModel::class.java]
}
private val multiChoiceList = linkedMapOf<String, Boolean>()
private val binding get() = _binding!!

 
최종 코드

package com.hanto.hook.data

interface TagSelectionListener {
    fun onTagsSelected(tags: List<String>)
}

 
↑ TagSelectionListener

TagSelectionListener는 사용자가 태그를 선택할 때 호출되는 콜백 인터페이스이다. TagListFragment에서 사용자가 태그를 선택하고 확인 버튼을 클릭하면 이 인터페이스를 통해 선택된 태그 리스트를 AddHookActivity로 전달한다.

 

 

class AddHookActivity : BaseActivity(), TagSelectionListener {
    private lateinit var binding: ActivityAddHookBinding

    // 기타 코드 생략...

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

        // 기타 초기화 코드 생략...

        // 태그 선택 버튼 클릭 리스너
        binding.containerTag.setOnClickListener {
            val fragment = TagListFragment.newInstance(multiChoiceList)
            fragment.setTagSelectionListener(this)  // TagSelectionListener 설정
            Log.d("minamina", "Sending tags to TagListFragment: $multiChoiceList")
            fragment.show(supportFragmentManager, "TagListFragment")
        }
    }

    // TagSelectionListener 구현
    override fun onTagsSelected(tags: List<String>) {
        binding.containerTag.text = tags.joinToString(" ") { "#$it" }
        Log.d("minamina", "Received tags from TagListFragment: $tags")
    }
}

 

↑  AddHookActivity

AddHookActivity는 사용자가 URL과 제목, 설명, 태그를 입력하여 새로운 "hook"을 추가하는 화면이다. 이 액티비티는 TagSelectionListener를 통하여 TagListFragment로부터 선택된 태그를 받고 원하는 형식으로 데이터를 변환한다.
 

class TagListFragment : DialogFragment() {

    private var tagSelectionListener: TagSelectionListener? = null
    private var _binding: FragmentTagListBinding? = null
    private val binding get() = _binding!!
    private lateinit var multiChoiceList: LinkedHashMap<String, Boolean>
    private lateinit var adapter: TagListAdapter

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

    @SuppressLint("NotifyDataSetChanged")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // arguments로부터 multiChoiceList 값을 받아옵니다.
        arguments?.let {
            multiChoiceList = it.getSerializable("multiChoiceList") as LinkedHashMap<String, Boolean>
        }

        adapter = TagListAdapter(requireContext(), multiChoiceList)
        binding.lvTags.adapter = adapter
        binding.lvTags.layoutManager = LinearLayoutManager(requireContext())

        // btn_add_tag 클릭 리스너 설정
        binding.btnAddTag.setOnClickListener {
            val newTag = binding.tvAddNewTag.text.toString().trim()
            if (newTag.isEmpty()) {
                Toast.makeText(requireContext(), "태그를 입력하세요.", Toast.LENGTH_SHORT).show()
            } else if (multiChoiceList.containsKey(newTag)) {
                Toast.makeText(requireContext(), "이미 존재하는 태그입니다.", Toast.LENGTH_SHORT).show()
            } else {
                multiChoiceList[newTag] = true
                binding.lvTags.adapter?.notifyDataSetChanged()
                binding.tvAddNewTag.text = null
            }
        }

        binding.btnCancel.setOnClickListener {
            dismiss()
        }

        // OK 버튼에 클릭 리스너 설정
        binding.btnOk.setOnClickListener {
            val selectedTags = multiChoiceList.filterValues { it }.keys.toList()
            Log.d("minaminamina", "Selected tags: $selectedTags")
            tagSelectionListener?.onTagsSelected(selectedTags)  // 선택된 태그 전달
            dismiss()
        }
    }

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

    fun setTagSelectionListener(listener: TagSelectionListener) {
        tagSelectionListener = listener
    }

    companion object {
        fun newInstance(multiChoiceList: LinkedHashMap<String, Boolean>): TagListFragment {
            val fragment = TagListFragment()
            val args = Bundle()
            args.putSerializable("multiChoiceList", multiChoiceList)
            fragment.arguments = args
            return fragment
        }
    }
}

 

↑  TagListFragment

TagListFragment는 사용자가 태그를 선택하거나 새로운 태그를 추가할 수 있는 다이얼로그이다. 사용자가 태그를 선택하고 확인 버튼을 클릭하면 TagSelectionListener를 통해 선택된 태그를 AddHookActivity로 전달한다.
 
기존 코드와 달라진점
 
1. ViewModel 및 API 서비스 관련 로직 제거: 이전 코드에서 ViewModel 및 ApiServiceManager와 관련된 코드가 모두 삭제되었다. ViewModel 및 API 서비스 관련 로직이 필요 없다는 것을 의미한다.(AddHookActivity쪽으로 옮김)
=> 재사용성 ↑
 
2. Arguments 전달 및 사용: newInstance 메서드를 통해 Fragment에 arguments를 전달하는 방법이 추가되었다. arguments로부터 multiChoiceList 값을 받아온 후, 해당 값으로 멤버 변수를 초기화한다.
=> Fragment 재생성 시 데이터 보존 : Fragment가 시스템에 의해 재생성될 때, Fragment의 상태가 arguments에 저장되어 있으면 해당 데이터가 보존된다. 사용자가 화면을 회전하거나 메모리가 부족한 상황에서도 데이터를 올바르게 유지할 수 있다.
=> multiChoiceList는 Fragment의 arguments로 전달되므로(newInstance 메서드를 사용하여), 화면 회전 등의 상황에서도 데이터가 보존될 수 있도록 변경하였다.
 
 
삽질 완

반응형