안녕하세요, 저는 개인 프로젝트를 하고 있는 취준생입니다.
현재 의존성 주입을 학습하고 프로젝트에 적용하고 있습니다.
특정 Fragment에서만 사용하는 View 조작을 하는 클래스가 있습니다. 그 외에 RecyclerView Adapter도 그렇고, Fragment에서 생성해서 Fragment에서만 사용하는 객체들이 몇 개 있어요. 이러한 객체들도 DI(Hilt)로 외부에서 주입하는 것이 좋나요? 생성자에 Binding 객체와 콜백 함수를 받는 클래스가 있는데, 주입하려니 꽤 불편해 보이더라구요. 그러다가 이렇게 주입해서 얻는 이점이 무엇인가.. 그냥 내부에서 생성하는 게 낫나 의문이 들었어요.
DI가 의존성 역전 원칙과 나아가서 개방-폐쇄 원칙을 지키기 위한 것으로 알고 있습니다(+ 테스트 편의성). Adapter와 ViewBinding처럼 View와 1:1로 강하게 연결되는 객체에도 이것이 해당되는 건가요? 이러한 경우는 DI의 장점인 클래스 재사용성 증가 + 유지보수성 증가(클라이언트 객체를 변경으로부터 보호)가 해당되지 않는 것 같아서요. 왜냐면.. 사실상 Fragment와 한몸에 가까운 1:1 관계라 중간에 추상화를 두지 않을 것 같다고 생각했어요.
그래서 결론은.. 만약 View 관련 클래스가 충분히 비대하다면, SOLID의 목적이 아닌 테스트 편의성 목적으로 추상화를 한 후 구현 객체를 주입하는 방식으로 사용하기도 하나요? 그리고 그런 경우가 아니면 Adapter와 같은 것들은 Fragment/Activity 내부에서 직접 생성해도 괜찮나요?
길고 장황한 질문 읽어주셔서 감사합니다ㅠㅠ
아래 코드들은 View 관련 클래스입니다.
class PosterDragHandlerImpl(
private val binding: FragmentGameBinding,
private val removeCaughtContent: () -> Unit
) : PosterDragHandler {
override fun onStartDrag() {
binding.darkBackgroundCoverForPoster.visibility = View.VISIBLE
binding.ivContentRemovableArea.isVisible = true
}
override fun onDraggingPoster(y: Float) {
when (isContentInRemoveRange(y)) {
true -> binding.ivContentRemovableArea.setBackgroundResource(
R.drawable.game_white_filled_circle_button
)
false -> binding.ivContentRemovableArea.setBackgroundResource(
R.drawable.game_outlined_circle_button
)
}
}
override fun isPosterRemovable(y: Float): Boolean =
isContentInRemoveRange(y)
override fun onFinishDrag(lastY: Float) {
binding.darkBackgroundCoverForPoster.visibility = View.INVISIBLE
binding.ivContentRemovableArea.isVisible = false
if (isContentInRemoveRange(lastY)) {
removeCaughtContent()
}
}
private fun isContentInRemoveRange(y: Float): Boolean =
binding.ivContentRemovableArea.y + binding.ivContentRemovableArea.height * 0.65 >=
binding.viewPagerPoster.y + y
}
class HintButtonOpenHandler(
hintEntranceButton: FloatingActionButton,
hintButtons: List<Button>,
wasHintOpened: Boolean,
private val darkBackgroundView: View
) {
private val innerButtons = mutableListOf<HintButton>()
private var animationDistance = 0f
var isHintOpen = wasHintOpened
private set
init {
hintEntranceButton.setOnClickListener {
when (isHintOpen) {
true -> closeHintAndDarkBackground()
false -> openHintAndDarkBackground()
}
}
hintEntranceButton.doOnLayout {
animationDistance = it.height.toFloat() + hintEntranceButton.getHintButtonMargin()
hintButtons.forEach(this::addInnerButton)
rollbackToPrevState()
}
}
private fun addInnerButton(button: Button) {
button.doOnLayout {
val openAnimator = ObjectAnimator
.ofFloat(button, "TranslationY", -animationDistance)
.setDuration(ANIMATION_DURATION)
innerButtons += HintButton(button, openAnimator)
animationDistance += (button.height + button.getHintButtonMargin())
}
}
private fun openHintAndDarkBackground() {
openHint()
isHintOpen = true
darkBackgroundView.visibility = View.VISIBLE
}
fun closeHintAndDarkBackground() {
closeHint()
isHintOpen = false
darkBackgroundView.visibility = View.GONE
}
private fun openHint() {
innerButtons.forEach {
it.open()
}
}
private fun closeHint() {
innerButtons.forEach {
it.close()
}
}
private fun rollbackToPrevState() {
if (isHintOpen) {
openHintAndDarkBackground()
}
}
private class HintButton(
private val button: Button,
private val openAnimator: ObjectAnimator
) {
init {
button.elevation = button.resources.getDimension(R.dimen.game_hintbutton_elevation)
button.isVisible = false
}
fun open() {
openAnimator.start()
button.isVisible = true
}
fun close() {
button.translationY = 0f
button.isVisible = false
}
}
companion object {
private const val ANIMATION_DURATION = 400L
}
}
private fun <T : View> T.getHintButtonMargin() = resources.getDimension(R.dimen.game_hintbutton_margin)