Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: 여행 화면 구현 #51 #75

Merged
merged 23 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4211001
ui: 함께 간 사람들 item 구현
hxeyexn Jul 23, 2024
449d896
ui: 방문 기록 item 구현
hxeyexn Jul 23, 2024
9eaf1eb
ui: 여행 상세 화면 구현
hxeyexn Jul 23, 2024
780838f
ui: 여행 생성 화면 구현
hxeyexn Jul 24, 2024
7fffcd6
ui: 여행 수정 화면 구현
hxeyexn Jul 24, 2024
61d887b
ui: placeholder에 사용할 drawable 추가
hxeyexn Jul 24, 2024
c64e9ec
ui: 여행 삭제 완료 string 추가
hxeyexn Jul 24, 2024
34ef21b
feat: 둥근 모서리 이미지 BindingAdapter 구현
hxeyexn Jul 24, 2024
ca01dd4
feat: 함께 간 사람들 adapter 구현
hxeyexn Jul 24, 2024
88c5bf0
ui: 여행 상세 화면 NestedScrollView로 변경
hxeyexn Jul 24, 2024
c743c6d
feat: 방문 기록 adapter 구현
hxeyexn Jul 24, 2024
8c107b7
feat: 여행 상세 view 연결
hxeyexn Jul 24, 2024
a78bc55
feat: 함께 간 사람들, 방문 기록 adapter 연결
hxeyexn Jul 24, 2024
e2d0cff
feat: 삭제 다이얼로그 handler 구현
hxeyexn Jul 24, 2024
a11c5fe
feat: 여행 상세 화면 toolbar handler 구현
hxeyexn Jul 24, 2024
c280d22
feat: 여행 -> 방문 기록 화면 이동 구현
hxeyexn Jul 24, 2024
015a56d
ui: DatePickerStyle 추가
hxeyexn Jul 25, 2024
7d7ce22
feat: 여행 저장 버튼, 여행 기간 BindingAdapter 추가
hxeyexn Jul 25, 2024
4e36d02
feat: 여행 생성 view 연결
hxeyexn Jul 25, 2024
a2c4410
feat: 여행 수정 view 연결
hxeyexn Jul 25, 2024
3203aea
Merge branch 'develop-an' into feature/#51_travel_ui
hxeyexn Jul 25, 2024
236b1f6
style: formatting
hxeyexn Jul 25, 2024
06d7e3b
refactor: 기간 선택 로직 메서드 분리
hxeyexn Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions android/.idea/android.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions android/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions android/.idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions android/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import com.woowacourse.staccato.R
import com.woowacourse.staccato.presentation.visitcreation.model.TravelUiModel
import java.time.LocalDate

@BindingAdapter(
value = ["coilImageUrl", "coilPlaceHolder"],
Expand Down Expand Up @@ -104,6 +105,24 @@ fun ImageView.setRoundedCornerImageWithGlide(
.into(this)
}

@BindingAdapter(
value = ["travelTitle", "startDate", "endDate"],
)
fun Button.setTravelSaveButtonActive(
title: String?,
startDate: LocalDate?,
endDate: LocalDate?,
) {
isEnabled =
if (title.isNullOrEmpty() || startDate == null || endDate == null) {
setTextColor(resources.getColor(R.color.gray4, null))
false
} else {
setTextColor(resources.getColor(R.color.white, null))
true
}
}
Comment on lines +108 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바인딩 어댑터의 어노테이션 속성과 확장 함수를 잘 활용하여 작성해주신 것 같아요!


@BindingAdapter("bindSetSelectedTravel")
fun TextView.setSelectedTravel(selectedTravel: TravelUiModel?) {
if (selectedTravel == null) {
Expand Down Expand Up @@ -143,6 +162,21 @@ fun Button.setVisitUpdateButtonActive(
}
}

@BindingAdapter(
value = ["startDate", "endDate"],
)
fun TextView.setTravelPeriod(
startDate: LocalDate?,
endDate: LocalDate?,
) {
if (startDate == null || endDate == null) {
text = resources.getString(R.string.travel_creation_period_hint)
} else {
text = resources.getString(R.string.travel_creation_period_description).format(startDate, endDate)
setTextColor(resources.getColor(R.color.staccato_black, null))
}
}

@BindingAdapter("bindSetVisitedAtConfirmButtonActive")
fun Button.setVisitedAtConfirmButtonActive(items: List<String>?) {
isEnabled =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.woowacourse.staccato.presentation.common

/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event와 SingleLiveData 관련 클래스 추가해주셔서 감사합니다ㅎㅎ 쇽 샥 ~

var hasBeenHandled = false
private set // Allow external read but not write

/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}

/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.woowacourse.staccato.presentation.common

class MutableSingleLiveData<T> : SingleLiveData<T> {
constructor() : super()

constructor(value: T) : super(value)

public override fun postValue(value: T) {
super.postValue(value)
}

public override fun setValue(value: T) {
super.setValue(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.woowacourse.staccato.presentation.common

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData

abstract class SingleLiveData<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 나중에 해당 SingleLiveDataMutableSingleLiveData 의 구현을 참고한 레퍼런스를 공유해주실 수 있을까요?
제가 이전에 미션에서 구현에 참고했던 SingleLiveData 와 동작 방식이 달라서 궁금해졌습니다!

private val liveData = MutableLiveData<Event<T>>()

protected constructor()

protected constructor(value: T) {
liveData.value = Event(value)
}

protected open fun setValue(value: T) {
liveData.value = Event(value)
}

protected open fun postValue(value: T) {
liveData.postValue(Event(value))
}

fun getValue() = liveData.value?.peekContent()

fun observe(
owner: LifecycleOwner,
onResult: (T) -> Unit,
) {
liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) }
}

fun observePeek(
owner: LifecycleOwner,
onResult: (T) -> Unit,
) {
liveData.observe(owner) { onResult(it.peekContent()) }
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,99 @@
package com.woowacourse.staccato.presentation.travel

import android.app.ProgressDialog.show
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.woowacourse.staccato.DeleteDialogFragment
import com.woowacourse.staccato.R
import com.woowacourse.staccato.databinding.FragmentTravelBinding
import com.woowacourse.staccato.presentation.ToolbarHandler
import com.woowacourse.staccato.presentation.base.BindingFragment
import com.woowacourse.staccato.presentation.main.MainActivity
import com.woowacourse.staccato.presentation.travel.adapter.MatesAdapter
import com.woowacourse.staccato.presentation.travel.adapter.VisitsAdapter
import com.woowacourse.staccato.presentation.travel.viewmodel.TravelViewModel
import com.woowacourse.staccato.presentation.travel.viewmodel.TravelViewModelFactory
import com.woowacourse.staccato.presentation.travelupdate.TravelUpdateActivity

class TravelFragment : BindingFragment<FragmentTravelBinding>(R.layout.fragment_travel) {
class TravelFragment :
BindingFragment<FragmentTravelBinding>(R.layout.fragment_travel),
ToolbarHandler {
private val viewModel: TravelViewModel by viewModels {
TravelViewModelFactory()
}
private val deleteDialog = DeleteDialogFragment { findNavController().popBackStack() }

private lateinit var matesAdapter: MatesAdapter
private lateinit var visitsAdapter: VisitsAdapter

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
binding.btnTravelUpdate.setOnClickListener {
val travelUpdateLauncher = (activity as MainActivity).travelUpdateLauncher
TravelUpdateActivity.startWithResultLauncher(
this.requireActivity(),
travelUpdateLauncher,
)
// findNavController().navigate(R.id.action_travelFragment_to_travelUpdateFragment)
initBinding()
initToolbar()
initMatesAdapter()
initVisitsAdapter()
observeTravel()
navigateToVisit()
}

private fun initBinding() {
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.toolbarHandler = this
}

private fun initToolbar() {
binding.includeTravelToolbar.toolbarDetail.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.btnVisit.setOnClickListener {
findNavController().navigate(R.id.action_travelFragment_to_visitFragment)
}

private fun observeTravel() {
viewModel.loadTravel()
viewModel.travel.observe(viewLifecycleOwner) { travel ->
matesAdapter.updateMates(travel.mates)
visitsAdapter.updateVisits(travel.visits)
}
}

private fun initMatesAdapter() {
matesAdapter = MatesAdapter()
binding.rvTravelMates.adapter = matesAdapter
}

private fun initVisitsAdapter() {
visitsAdapter = VisitsAdapter(handler = viewModel)
binding.rvTravelVisits.adapter = visitsAdapter
}

private fun navigateToVisit() {
viewModel.visitId.observe(viewLifecycleOwner) { visitId ->
val bundle = bundleOf(VISIT_ID_KEY to visitId)
findNavController().navigate(R.id.action_travelFragment_to_visitFragment, bundle)
}
}

override fun onUpdateClicked() {
val travelUpdateLauncher = (activity as MainActivity).travelUpdateLauncher
TravelUpdateActivity.startWithResultLauncher(
requireActivity(),
travelUpdateLauncher,
)
}

override fun onDeleteClicked() {
val fragmentManager = parentFragmentManager
deleteDialog.apply {
show(fragmentManager, DeleteDialogFragment.TAG)
}
}

companion object {
const val VISIT_ID_KEY = "visitId"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.woowacourse.staccato.presentation.travel

interface TravelHandler {
fun onVisitClicked(visitId: Long)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.woowacourse.staccato.presentation.travel.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.woowacourse.staccato.databinding.ItemMatesBinding
import com.woowacourse.staccato.presentation.travel.model.MateUiModel

class MatesAdapter : ListAdapter<MateUiModel, MatesViewHolder>(diffUtil) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): MatesViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemMatesBinding.inflate(inflater, parent, false)
return MatesViewHolder(binding)
}

override fun onBindViewHolder(
holder: MatesViewHolder,
position: Int,
) {
holder.bind(getItem(position))
}

fun updateMates(mates: List<MateUiModel>) {
submitList(mates)
}

companion object {
val diffUtil =
object : DiffUtil.ItemCallback<MateUiModel>() {
override fun areItemsTheSame(
oldItem: MateUiModel,
newItem: MateUiModel,
): Boolean = oldItem.id == newItem.id

override fun areContentsTheSame(
oldItem: MateUiModel,
newItem: MateUiModel,
): Boolean = oldItem == newItem
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.woowacourse.staccato.presentation.travel.adapter

import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.woowacourse.staccato.databinding.ItemMatesBinding
import com.woowacourse.staccato.presentation.travel.model.MateUiModel

class MatesViewHolder(
private val binding: ItemMatesBinding,
) : ViewHolder(binding.root) {
fun bind(mate: MateUiModel) {
binding.mate = mate
}
}
Loading
Loading