-
Notifications
You must be signed in to change notification settings - Fork 0
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
#7 [feat] 우리 집 규칙 기능 구현 #8
Changes from all commits
ec3eb4d
c9d1134
dde7c42
8a0d8f9
0679bcc
fcb5882
23e5916
95af413
41290c6
388e448
befe512
e780853
e0198b3
549346f
f1cff56
411c881
a21fc90
9c9fda5
a35b2d9
d1f2abe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,70 @@ | ||
package hous.release.android.presentation.our_rules | ||
|
||
import android.os.Bundle | ||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import androidx.fragment.app.Fragment | ||
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels | ||
import dagger.hilt.android.AndroidEntryPoint | ||
import hous.release.android.R | ||
import hous.release.android.databinding.FragmentOurRuleBinding | ||
import hous.release.android.presentation.our_rules.adapter.OurRulesAdapter | ||
import hous.release.android.util.ItemDecorationUtil | ||
import hous.release.android.util.binding.BindingFragment | ||
import hous.release.android.util.extension.repeatOnStarted | ||
import hous.release.android.util.extension.safeLet | ||
import timber.log.Timber | ||
|
||
@AndroidEntryPoint | ||
class OurRulesFragment : Fragment() { | ||
|
||
override fun onCreateView( | ||
inflater: LayoutInflater, | ||
container: ViewGroup?, | ||
savedInstanceState: Bundle? | ||
): View? { | ||
return inflater.inflate(R.layout.fragment_our_rule, container, false) | ||
class OurRulesFragment : BindingFragment<FragmentOurRuleBinding>(R.layout.fragment_our_rule) { | ||
|
||
private val viewModel: OurRulesViewModel by hiltNavGraphViewModels(R.id.nav_our_rules) | ||
private var ourRulesAdapter: OurRulesAdapter? = null | ||
Comment on lines
+18
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희는 hilt를 사용하기 때문이지요 :D private val viewModel: OurRulesViewModel by navGraphViewModels(R.id.nav_our_rules) 위의 방식으로 생성된 모든 ViewModel 객체는 연결된 NavHost와 ViewModelStore가 삭제되거나 nav_graph가 백 스택에서 소멸되기 전까지 유지되기 때문에 그런데 우리는 hilt를 사용하기 때문에
관련 공식 문서: https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodel-navigation |
||
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
super.onViewCreated(view, savedInstanceState) | ||
binding.vm = viewModel | ||
setOurRulesAdapter() | ||
observeOurRules() | ||
} | ||
|
||
override fun onResume() { | ||
super.onResume() | ||
// 여기에서 서버통신을 하는게 맞을 지 고민이 되네요 | ||
// 일단은 프래그먼트 백스택에서 꺼내올 때, 서버통신을 하도록 넣어놓았습니다 ㅎ ㅎ ㅎ | ||
viewModel.getOurRulesInfo() | ||
} | ||
|
||
override fun onDestroyView() { | ||
super.onDestroyView() | ||
ourRulesAdapter = null | ||
} | ||
|
||
private fun setOurRulesAdapter() { | ||
ourRulesAdapter = OurRulesAdapter() | ||
safeLet(ourRulesAdapter, context) { ourRulesAdapter, context -> | ||
binding.rvOurRules.run { | ||
adapter = ourRulesAdapter | ||
addItemDecoration( | ||
ItemDecorationUtil(context, MARGIN, POSITION) | ||
) | ||
} | ||
} ?: Timber.e("NullPointException - ourRulesAdapter, context : $ourRulesAdapter, $context") | ||
} | ||
|
||
private fun observeOurRules() { | ||
repeatOnStarted { | ||
viewModel.uiState.collect { uiState -> | ||
ourRulesAdapter?.let { ourRulesAdapter -> | ||
// this callback runs when the list is updated | ||
ourRulesAdapter.submitList(uiState.ourRuleList) { | ||
binding.rvOurRules.invalidateItemDecorations() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
Comment on lines
+54
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. uiState만을 수집할 땐, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #8 (comment) |
||
|
||
companion object { | ||
private const val MARGIN = 4 | ||
private const val POSITION = 3 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package hous.release.android.presentation.our_rules | ||
|
||
import androidx.lifecycle.ViewModel | ||
import dagger.hilt.android.lifecycle.HiltViewModel | ||
import hous.release.domain.entity.Rule | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
import kotlinx.coroutines.flow.asStateFlow | ||
import javax.inject.Inject | ||
|
||
@HiltViewModel | ||
class OurRulesViewModel @Inject constructor() : ViewModel() { | ||
private var _uiState = MutableStateFlow(OurRulesUiState()) | ||
val uiState: StateFlow<OurRulesUiState> = _uiState.asStateFlow() | ||
private var _isEmptyRepresentativeRuleList = MutableStateFlow(true) | ||
val isEmptyRepresentativeRuleList: StateFlow<Boolean> = | ||
_isEmptyRepresentativeRuleList.asStateFlow() | ||
private var _isEmptyGeneralRuleList = MutableStateFlow(true) | ||
val isEmptyGeneralRuleList: StateFlow<Boolean> = _isEmptyGeneralRuleList.asStateFlow() | ||
|
||
fun getOurRulesInfo() { | ||
// 일단 dummy data로 (추후에 repository or usecase에서 받아오기) | ||
val rules: List<Rule> = | ||
listOf( | ||
Rule("1111", "우리 집 대장은 최코코"), | ||
Rule("2222", "10시, 18시 코코님 밥 챙겨드리기"), | ||
Rule("3333", "자기가 먹은 건 자기가 치우기!"), | ||
Rule("4444", "아침에 일어나면 이불 정리하기"), | ||
Rule("5555", "밤 10시 지나면 이어폰 끼기"), | ||
Rule("11412311", "우리 집 대장은 최코코"), | ||
Rule("4444", "아침에 일어나면 이불 정리하기"), | ||
Rule("512355", "밤 10시 지나면 이어폰 끼기"), | ||
Rule("612366", "냄새나는 음식 먹고나서 환기하기"), | ||
Rule("7721377", "맛있는 식당 찾으면 공유하기"), | ||
Rule("112311", "우리 집 대장은 최코코"), | ||
Rule("2221322", "10시, 18시 코코님 밥 챙겨드리기"), | ||
Rule("3331233", "자기가 먹은 건 자기가 치우기!"), | ||
Rule("4412344", "아침에 일어나면 이불 정리하기"), | ||
Rule("5552135", "밤 10시 지나면 이어폰 끼기"), | ||
Rule("6612366", "냄새나는 음식 먹고나서 환기하기"), | ||
Rule("7771237", "맛있는 식당 찾으면 공유하기") | ||
) | ||
if (rules.isEmpty()) { | ||
_uiState.value = _uiState.value.copy( | ||
ourRuleList = defaultRuleList | ||
) | ||
_isEmptyRepresentativeRuleList.value = true | ||
_isEmptyGeneralRuleList.value = true | ||
} else if (rules.size <= 3) { | ||
val tmpRuleList = _uiState.value.ourRuleList.toMutableList() | ||
rules.forEachIndexed { idx, value -> | ||
tmpRuleList[idx] = value | ||
} | ||
_uiState.value = _uiState.value.copy( | ||
ourRuleList = tmpRuleList | ||
) | ||
_isEmptyRepresentativeRuleList.value = false | ||
_isEmptyGeneralRuleList.value = true | ||
} else { | ||
_uiState.value = _uiState.value.copy( | ||
ourRuleList = rules | ||
) | ||
_isEmptyRepresentativeRuleList.value = false | ||
_isEmptyGeneralRuleList.value = false | ||
} | ||
} | ||
|
||
data class OurRulesUiState( | ||
val ourRuleList: List<Rule> = defaultRuleList | ||
) | ||
|
||
companion object { | ||
private val defaultRuleList = listOf( | ||
Rule("1111", ""), | ||
Rule("2222", ""), | ||
Rule("3333", "") | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package hous.release.android.presentation.our_rules.adapter | ||
|
||
import android.view.LayoutInflater | ||
import android.view.ViewGroup | ||
import androidx.recyclerview.widget.DiffUtil | ||
import androidx.recyclerview.widget.ListAdapter | ||
import androidx.recyclerview.widget.RecyclerView | ||
import hous.release.android.databinding.ItemOurRulesGeneralRuleBinding | ||
import hous.release.android.databinding.ItemOurRulesRepresentativeRuleBottomBinding | ||
import hous.release.android.databinding.ItemOurRulesRepresentativeRuleMiddleBinding | ||
import hous.release.android.databinding.ItemOurRulesRepresentativeRuleTopBinding | ||
import hous.release.android.presentation.our_rules.type.RuleItemViewType | ||
import hous.release.domain.entity.Rule | ||
|
||
class OurRulesAdapter : ListAdapter<Rule, RecyclerView.ViewHolder>(ourRulesDiffUtilCallback) { | ||
|
||
override fun getItemViewType(position: Int): Int { | ||
return when (position) { | ||
0 -> RuleItemViewType.REPRESENTATVIE_TOP.id | ||
1 -> RuleItemViewType.REPRESENTATVIE_MIDDLE.id | ||
2 -> RuleItemViewType.REPRESENTATVIE_BOTTOM.id | ||
else -> RuleItemViewType.GENERAL.id | ||
} | ||
} | ||
|
||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { | ||
return when (viewType) { | ||
RuleItemViewType.REPRESENTATVIE_TOP.id -> RepresentationTopViewHolder( | ||
ItemOurRulesRepresentativeRuleTopBinding.inflate( | ||
LayoutInflater.from(parent.context), | ||
parent, | ||
false | ||
) | ||
) | ||
RuleItemViewType.REPRESENTATVIE_MIDDLE.id -> RepresentationMiddleViewHolder( | ||
ItemOurRulesRepresentativeRuleMiddleBinding.inflate( | ||
LayoutInflater.from(parent.context), | ||
parent, | ||
false | ||
) | ||
) | ||
RuleItemViewType.REPRESENTATVIE_BOTTOM.id -> RepresentationBottomViewHolder( | ||
ItemOurRulesRepresentativeRuleBottomBinding.inflate( | ||
LayoutInflater.from(parent.context), | ||
parent, | ||
false | ||
) | ||
) | ||
RuleItemViewType.GENERAL.id -> GeneralViewHolder( | ||
ItemOurRulesGeneralRuleBinding.inflate( | ||
LayoutInflater.from(parent.context), | ||
parent, | ||
false | ||
) | ||
) | ||
else -> throw IllegalArgumentException("viewType : $viewType") | ||
} | ||
} | ||
|
||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { | ||
val data = currentList[position] | ||
when (holder) { | ||
is RepresentationTopViewHolder -> holder.onBind(data) | ||
is RepresentationMiddleViewHolder -> holder.onBind(data) | ||
is RepresentationBottomViewHolder -> holder.onBind(data) | ||
is GeneralViewHolder -> holder.onBind(data) | ||
else -> throw IllegalArgumentException("holder : $holder") | ||
} | ||
} | ||
|
||
class RepresentationTopViewHolder( | ||
private val binding: ItemOurRulesRepresentativeRuleTopBinding | ||
) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun onBind(data: Rule) { | ||
binding.data = data | ||
} | ||
} | ||
|
||
class RepresentationMiddleViewHolder( | ||
private val binding: ItemOurRulesRepresentativeRuleMiddleBinding | ||
) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun onBind(data: Rule) { | ||
binding.data = data | ||
} | ||
} | ||
|
||
class RepresentationBottomViewHolder( | ||
private val binding: ItemOurRulesRepresentativeRuleBottomBinding | ||
) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun onBind(data: Rule) { | ||
binding.data = data | ||
} | ||
} | ||
|
||
class GeneralViewHolder( | ||
private val binding: ItemOurRulesGeneralRuleBinding | ||
) : RecyclerView.ViewHolder(binding.root) { | ||
|
||
fun onBind(data: Rule) { | ||
binding.data = data | ||
} | ||
} | ||
|
||
companion object { | ||
private val ourRulesDiffUtilCallback = | ||
object : DiffUtil.ItemCallback<Rule>() { | ||
override fun areItemsTheSame( | ||
oldItem: Rule, | ||
newItem: Rule | ||
): Boolean { | ||
return oldItem.id == newItem.id | ||
} | ||
|
||
override fun areContentsTheSame( | ||
oldItem: Rule, | ||
newItem: Rule | ||
): Boolean { | ||
return oldItem == newItem | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package hous.release.android.presentation.our_rules.type | ||
|
||
/** | ||
* 우리 집 규칙 뷰 타입 */ | ||
enum class RuleItemViewType(val id: Int) { | ||
REPRESENTATVIE_TOP(1), REPRESENTATVIE_MIDDLE(2), REPRESENTATVIE_BOTTOM(3), GENERAL(4) | ||
} | ||
Comment on lines
+5
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저한테는 util 패키지가 적합해 보입니다..! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하! 어디에 둘지 많이 고민했는데.. 최고네요~ 워뇽쿤 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package hous.release.android.util | ||
|
||
import android.content.Context | ||
import android.graphics.Rect | ||
import android.util.TypedValue | ||
import android.view.View | ||
import androidx.recyclerview.widget.RecyclerView | ||
|
||
class ItemDecorationUtil( | ||
private val context: Context, | ||
dp: Int, | ||
private val position: Int | ||
) : | ||
RecyclerView.ItemDecoration() { | ||
private val margin = getPixelValue(dp) | ||
|
||
override fun getItemOffsets( | ||
outRect: Rect, | ||
view: View, | ||
parent: RecyclerView, | ||
state: RecyclerView.State | ||
) { | ||
super.getItemOffsets(outRect, view, parent, state) | ||
val viewPosition = parent.getChildAdapterPosition(view) | ||
if (viewPosition == position) { | ||
outRect.top = margin | ||
} | ||
} | ||
|
||
private fun getPixelValue(dp: Int): Int { | ||
return TypedValue.applyDimension( | ||
TypedValue.COMPLEX_UNIT_DIP, | ||
dp.toFloat(), | ||
context.resources.displayMetrics | ||
).toInt() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package hous.release.android.util.extension | ||
|
||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.fragment.app.Fragment | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleOwner | ||
import androidx.lifecycle.lifecycleScope | ||
import androidx.lifecycle.repeatOnLifecycle | ||
import kotlinx.coroutines.launch | ||
|
||
/** | ||
* Lifecycle에 맞게 알아서 collect/cancel을 반복해주게 해주는 확장 함수 | ||
* | ||
* @param block | ||
*/ | ||
inline fun LifecycleOwner.repeatOnStarted(crossinline block: suspend () -> Unit) { | ||
when (this) { | ||
is AppCompatActivity -> { | ||
lifecycleScope.launch { | ||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
block() | ||
} | ||
} | ||
} | ||
is Fragment -> { | ||
lifecycleScope.launch { | ||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
block() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
Comment on lines
+16
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 확장함수를 만든 의도가 궁금합니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locationFlow when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
또한, 이 뷰 뿐만 아니라 다른 뷰(Fragment/Activity)에서도 collect해줄 때마다 더 간결하게 사용하기 위해 확장함수를 만들었습니다!! lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// do something~
}
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one Activity
를 의도한 것인지 아닌지 궁금합니다.one Activity
를 의도했음 이OurRulesActivity
가 없어도 된다고 생각하고,one Activity
를 의도하지 않았으면OurRulesFragment
의 로직이OurRulesActivity
로 옮겨져야 한다고 생각하는데어떤 의도인지 궁금합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞습니다 제가 맡은
우리 집 규칙
뷰에서는 oneActivity를 사용할 생각입니다.위에서 Our Rules를 클릭하면
우리 집 규칙
으로 넘어오게 됩니다.그래서, by activityViewModels()로 ViewModel을 생성해줘도 되지만, 저는
hiltNavGraphViewModels
를 적용해보고 싶었습니다 하하~