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

#7 [feat] 우리 집 규칙 기능 구현 #8

Merged
merged 20 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ dependencies {
implementation(project(":domain"))

Deps.AndroidX.run {
implementation(hilt_navigation)
implementation(navigation)
implementation(navigationFragment)
implementation(core)
implementation(appcompat)
implementation(material)
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
tools:targetApi="31">
<activity
android:name=".presentation.our_rules.OurRulesActivity"
android:exported="false" />
android:exported="true" >
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package hous.release.android.presentation.our_rules

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import hous.release.android.R

@AndroidEntryPoint
class OurRulesActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Comment on lines +8 to 10
Copy link
Member

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 로 옮겨져야 한다고 생각하는데

어떤 의도인지 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

맞습니다 제가 맡은 우리 집 규칙뷰에서는 oneActivity를 사용할 생각입니다.
image
위에서 Our Rules를 클릭하면 우리 집 규칙으로 넘어오게 됩니다.
image
그래서, by activityViewModels()로 ViewModel을 생성해줘도 되지만, 저는 hiltNavGraphViewModels를 적용해보고 싶었습니다 하하~

super.onCreate(savedInstanceState)
Expand Down
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
Copy link
Member

Choose a reason for hiding this comment

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

hiltNavGraphViewModels 의도가 궁금합니다

Copy link
Member Author

@murjune murjune Sep 2, 2022

Choose a reason for hiding this comment

The 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가 백 스택에서 소멸되기 전까지 유지되기 때문에 by navGraphViewModels(layoutRes)을 사용하는 것입니다.

그런데 우리는 hilt를 사용하기 때문에 by hiltNavGraphViewModels(R.id.nav_our_rules)을 사용해줬습니당

If your ViewModel is scoped to the navigation graph, use the hiltNavGraphViewModels function that works with fragments that are annotated with @androidentrypoint.

관련 공식 문서: 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
Copy link
Member

Choose a reason for hiding this comment

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

uiState만을 수집할 땐, reapeatLifeCycle 보단 flowWithLifeCycle 함수를 사용해주세요!

Copy link
Member Author

Choose a reason for hiding this comment

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

#8 (comment)
혹시, 이유를 알 수 있을까요?! 하나만 수집할 때는 reapeatLifeCycle보다 flowWithLifeCycle가 더 성능이 좋나요?!


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
Copy link
Member

Choose a reason for hiding this comment

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

저한테는 util 패키지가 적합해 보입니다..!

Copy link
Member Author

Choose a reason for hiding this comment

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

아하! 어디에 둘지 많이 고민했는데.. 최고네요~ 워뇽쿤

37 changes: 37 additions & 0 deletions app/src/main/java/hous/release/android/util/ItemDecorationUtil.kt
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
Copy link
Member

Choose a reason for hiding this comment

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

reapeatOnLifeCycle 함수 자체가 LifecycleonStart 상태일 때 수집을 시작하고, onResume 상태가 되면 알아서 수집을 멈춰주는 함수인 것으로 압니다.

이 확장함수를 만든 의도가 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

The 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
                }
            }
        }
    }
}

reapeatOnLifeCycle함수는 onResume 상태가 아닌 onStop상태일 때 collect가 cancel됩니다!!
사실, collect를 한 번만 하게 된다면 Flow.flowWithLifecycle를 사용해도 되긴합니다만..
추후에 기능이 추가된다면 collect를 또 할 수도 있을 것 같아서 저는 reapeatOnLifeCycle을 선호하는 편입니다!
관련 링크

또한, 이 뷰 뿐만 아니라 다른 뷰(Fragment/Activity)에서도 collect해줄 때마다 더 간결하게 사용하기 위해 확장함수를 만들었습니다!!

 lifecycleScope.launch {
                lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                    // do something~
                }
            }

Loading