Skip to content

Commit c461d9b

Browse files
authored
Merge pull request #533 from projects200/refactor/compose-custom-timer-form-532
[Refactor] 커스텀 타이머 폼 화면 Compose 전환 #532
2 parents 0a09f56 + 76cf49c commit c461d9b

17 files changed

Lines changed: 632 additions & 684 deletions

feature/timer/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ dependencies {
3636
androidTestImplementation(libs.androidx.navigation.testing)
3737

3838
implementation(libs.androidx.media)
39+
40+
// Compose Drag/Drop
41+
implementation(libs.reorderable)
3942
}

feature/timer/src/main/java/com/project200/feature/timer/custom/CustomTimerFormFragment.kt

Lines changed: 82 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -2,154 +2,105 @@ package com.project200.feature.timer.custom
22

33
import android.content.Context
44
import android.os.Bundle
5+
import android.view.LayoutInflater
56
import android.view.View
7+
import android.view.ViewGroup
68
import android.view.inputmethod.InputMethodManager
79
import android.widget.Toast
8-
import androidx.core.widget.addTextChangedListener
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.ui.platform.ComposeView
12+
import androidx.fragment.app.Fragment
913
import androidx.fragment.app.viewModels
14+
import androidx.lifecycle.Lifecycle
15+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
16+
import androidx.lifecycle.lifecycleScope
17+
import androidx.lifecycle.repeatOnLifecycle
1018
import androidx.navigation.fragment.findNavController
1119
import androidx.navigation.fragment.navArgs
12-
import androidx.recyclerview.widget.ItemTouchHelper
13-
import androidx.recyclerview.widget.LinearLayoutManager
14-
import androidx.recyclerview.widget.RecyclerView
1520
import com.project200.feature.timer.TimePickerDialog
16-
import com.project200.feature.timer.custom.adapter.AddedStepRVAdapter
17-
import com.project200.feature.timer.custom.adapter.OnStepItemClickListener
18-
import com.project200.feature.timer.custom.adapter.StepItemMoveCallback
19-
import com.project200.presentation.base.BindingFragment
21+
import com.project200.presentation.compose.applyAppTheme
2022
import com.project200.undabang.feature.timer.R
21-
import com.project200.undabang.feature.timer.databinding.FragmentCustomTimerFormBinding
2223
import dagger.hilt.android.AndroidEntryPoint
24+
import kotlinx.coroutines.launch
2325

2426
@AndroidEntryPoint
25-
class CustomTimerFormFragment : BindingFragment<FragmentCustomTimerFormBinding>(R.layout.fragment_custom_timer_form) {
27+
class CustomTimerFormFragment : Fragment() {
2628
private val viewModel: CustomTimerFormViewModel by viewModels()
27-
private lateinit var stepAdapter: AddedStepRVAdapter
28-
private lateinit var itemTouchHelper: ItemTouchHelper
2929
private val args: CustomTimerFormFragmentArgs by navArgs()
3030

31-
override fun getViewBinding(view: View): FragmentCustomTimerFormBinding {
32-
return FragmentCustomTimerFormBinding.bind(view)
33-
}
34-
3531
override fun onCreate(savedInstanceState: Bundle?) {
3632
super.onCreate(savedInstanceState)
3733
viewModel.loadData(args.customTimerId)
3834
}
3935

40-
override fun setupViews() {
41-
binding.baseToolbar.apply {
42-
setTitle(getString(if (viewModel.isEditMode) R.string.custom_timer_edit else R.string.custom_timer_create))
43-
showBackButton(true) { findNavController().navigateUp() }
44-
}
45-
initRecyclerView()
46-
initListeners()
47-
}
48-
49-
private fun initListeners() {
50-
binding.timerTitleEt.addTextChangedListener { title ->
51-
viewModel.updateTimerTitle(title.toString())
52-
}
53-
54-
binding.completeBtn.setOnClickListener {
55-
clearFocusAndHideKeyboard()
56-
viewModel.submitCustomTimer()
57-
}
58-
}
59-
60-
private fun initRecyclerView() {
61-
val itemTouchHelperCallback =
62-
StepItemMoveCallback { from, to ->
63-
viewModel.moveStep(from, to)
36+
override fun onCreateView(
37+
inflater: LayoutInflater,
38+
container: ViewGroup?,
39+
savedInstanceState: Bundle?,
40+
): View =
41+
ComposeView(requireContext()).apply {
42+
applyAppTheme {
43+
val state by viewModel.uiState.collectAsStateWithLifecycle()
44+
CustomTimerFormScreen(
45+
title = state.title,
46+
listItems = state.listItems,
47+
isEditMode = viewModel.isEditMode,
48+
onTitleChange = viewModel::updateTimerTitle,
49+
onStepNameChange = viewModel::updateStepName,
50+
onStepTimeClick = { id, time -> showTimePickerDialog(id, time) },
51+
onStepDelete = viewModel::removeStep,
52+
onMove = viewModel::moveStep,
53+
onNewStepNameChange = viewModel::updateNewStepName,
54+
onNewStepTimeClick = { time -> showTimePickerDialog(null, time) },
55+
onAddStep = viewModel::addStep,
56+
onCompleteClick = {
57+
clearFocusAndHideKeyboard()
58+
viewModel.submitCustomTimer()
59+
},
60+
onBackClick = { findNavController().navigateUp() },
61+
)
6462
}
65-
itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
66-
67-
stepAdapter =
68-
AddedStepRVAdapter(
69-
object : OnStepItemClickListener {
70-
// 스텝 아이템 이벤트
71-
override fun onDeleteClick(id: Long) {
72-
viewModel.removeStep(id)
73-
}
74-
75-
override fun onTimeClick(
76-
id: Long,
77-
time: Int,
78-
) {
79-
showTimePickerDialog(id, time)
80-
}
81-
82-
override fun onStepNameChanged(
83-
id: Long,
84-
name: String,
85-
) {
86-
viewModel.updateStepName(id, name)
87-
}
88-
89-
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
90-
itemTouchHelper.startDrag(viewHolder)
91-
}
92-
93-
// 입력 필드 이벤트
94-
override fun onNewStepNameChanged(name: String) {
95-
viewModel.updateNewStepName(name)
96-
}
97-
98-
override fun onNewStepTimeClick(time: Int) {
99-
showTimePickerDialog(null, time)
100-
}
101-
102-
override fun onAddStepClick() {
103-
viewModel.addStep()
104-
}
105-
},
106-
)
107-
108-
binding.stepRv.apply {
109-
adapter = stepAdapter
110-
layoutManager = LinearLayoutManager(requireContext())
111-
itemTouchHelper.attachToRecyclerView(this)
112-
}
113-
}
114-
115-
override fun setupObservers() {
116-
viewModel.uiState.observe(viewLifecycleOwner) { state ->
117-
if (state == null) return@observe
118-
119-
if (binding.timerTitleEt.text.toString() != state.title) {
120-
binding.timerTitleEt.setText(state.title)
121-
}
122-
stepAdapter.submitList(state.listItems)
12363
}
12464

125-
viewModel.toast.observe(viewLifecycleOwner) { toast ->
126-
val messageResId =
127-
when (toast) {
128-
// 검증 결과에 따른 메시지 매핑
129-
ToastMessageType.EMPTY_TITLE -> R.string.custom_timer_error_empty_title
130-
ToastMessageType.NO_STEPS -> R.string.custom_timer_error_no_steps
131-
ToastMessageType.INVALID_STEP_TIME -> R.string.custom_timer_error_invalid_time
132-
ToastMessageType.MAX_STEPS -> R.string.custom_timer_error_max_steps
133-
ToastMessageType.NO_CHANGES -> R.string.custom_timer_error_no_changes
134-
ToastMessageType.EMPTY_STEP_NAME -> R.string.custom_timer_error_empty_step_name
135-
// API 에러 메시지 매핑
136-
ToastMessageType.CREATE_ERROR -> R.string.custom_timer_error_create_failed
137-
ToastMessageType.EDIT_ERROR -> R.string.custom_timer_error_edit_failed
138-
ToastMessageType.GET_ERROR -> R.string.error_failed_to_load_list
139-
ToastMessageType.UNKNOWN_ERROR -> R.string.unknown_error
140-
null -> return@observe
65+
override fun onViewCreated(
66+
view: View,
67+
savedInstanceState: Bundle?,
68+
) {
69+
super.onViewCreated(view, savedInstanceState)
70+
71+
viewLifecycleOwner.lifecycleScope.launch {
72+
repeatOnLifecycle(Lifecycle.State.STARTED) {
73+
launch {
74+
viewModel.toast.collect { type ->
75+
val messageResId =
76+
when (type) {
77+
ToastMessageType.EMPTY_TITLE -> R.string.custom_timer_error_empty_title
78+
ToastMessageType.NO_STEPS -> R.string.custom_timer_error_no_steps
79+
ToastMessageType.INVALID_STEP_TIME -> R.string.custom_timer_error_invalid_time
80+
ToastMessageType.MAX_STEPS -> R.string.custom_timer_error_max_steps
81+
ToastMessageType.NO_CHANGES -> R.string.custom_timer_error_no_changes
82+
ToastMessageType.EMPTY_STEP_NAME -> R.string.custom_timer_error_empty_step_name
83+
ToastMessageType.CREATE_ERROR -> R.string.custom_timer_error_create_failed
84+
ToastMessageType.EDIT_ERROR -> R.string.custom_timer_error_edit_failed
85+
ToastMessageType.GET_ERROR -> R.string.error_failed_to_load_list
86+
ToastMessageType.UNKNOWN_ERROR -> R.string.unknown_error
87+
}
88+
Toast.makeText(requireContext(), messageResId, Toast.LENGTH_SHORT).show()
89+
}
14190
}
142-
Toast.makeText(requireContext(), messageResId, Toast.LENGTH_SHORT).show()
143-
}
144-
145-
viewModel.submitResult.observe(viewLifecycleOwner) { id ->
146-
if (id != null && findNavController().currentDestination?.id == R.id.customTimerFormFragment) {
147-
if (viewModel.isEditMode) {
148-
findNavController().navigateUp()
149-
} else {
150-
findNavController().navigate(
151-
CustomTimerFormFragmentDirections.actionCustomTimerFormFragmentToCustomTimerFragment(id),
152-
)
91+
launch {
92+
viewModel.submitResult.collect { id ->
93+
if (findNavController().currentDestination?.id != R.id.customTimerFormFragment) {
94+
return@collect
95+
}
96+
if (viewModel.isEditMode) {
97+
findNavController().navigateUp()
98+
} else {
99+
findNavController().navigate(
100+
CustomTimerFormFragmentDirections.actionCustomTimerFormFragmentToCustomTimerFragment(id),
101+
)
102+
}
103+
}
153104
}
154105
}
155106
}
@@ -166,21 +117,16 @@ class CustomTimerFormFragment : BindingFragment<FragmentCustomTimerFormBinding>(
166117
Toast.makeText(requireContext(), R.string.custom_timer_error_invalid_time, Toast.LENGTH_SHORT).show()
167118
return@TimePickerDialog
168119
}
169-
id?.let { viewModel.updateStepTime(id, newTimeInSeconds) } ?: run {
170-
// id가 null인 경우는 새 스텝을 추가할 때이므로 새 스텝 시간 업데이트
171-
viewModel.updateNewStepTime(newTimeInSeconds)
172-
}
120+
id?.let { viewModel.updateStepTime(it, newTimeInSeconds) }
121+
?: viewModel.updateNewStepTime(newTimeInSeconds)
173122
},
174123
).show(parentFragmentManager, this::class.java.simpleName)
175124
}
176125

177-
// 포커스 해제 및 키보드 숨기기
178126
private fun clearFocusAndHideKeyboard() {
179-
val currentFocusView = activity?.currentFocus
180-
if (currentFocusView != null) {
181-
currentFocusView.clearFocus()
182-
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
183-
imm?.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
184-
}
127+
val current = activity?.currentFocus ?: return
128+
current.clearFocus()
129+
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
130+
imm?.hideSoftInputFromWindow(current.windowToken, 0)
185131
}
186132
}

0 commit comments

Comments
 (0)