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

[2주차 과제] 안드로이드 UI 구현 심화 #4

Merged
merged 29 commits into from
Apr 30, 2023
Merged

[2주차 과제] 안드로이드 UI 구현 심화 #4

merged 29 commits into from
Apr 30, 2023

Conversation

leeeha
Copy link
Member

@leeeha leeeha commented Apr 21, 2023

  • [2주차 과제] 안드로이드 UI 구현 심화 #3
  • 커밋 메시지에 이슈 연결하고 싶어서 번호를 적었는데,,, 3번이 아니라 2번을 적어서,,, PR 링크가 연결되었어요😅 다음부턴 이슈 번호 제대로 확인하겠습니닷,,

필수 과제

  • 리사이클러뷰 위에 있는 헤더가 같이 스크롤 되도록
    • Hint: Multi-View Type RecyclerView or ConcatAdapter
    • HomeFragment 리사이클러뷰에 적용했습니다. (rv_home)

심화 과제

  • BottomNavigationView 버튼을 한번 더 누르면, 최상단으로 스크롤 되도록 (Scroll to top)
    • Hint: BottomNavigation.setOnNavigationItemReselectedListener
    • HomeFragment, GalleryFragment에 있는 리사이클러뷰에 적용했습니다.
  • 프래그먼트 생명주기 알아보기
  • 리사이클러뷰 성능 개선하기
    • notifyDataSetChanged 함수의 문제점을 개선한 DiffUtil + ListAdapter에 대해 알아보고 사용해보기
    • GalleryFragment 리사이클러뷰에 적용했습니다. (rv_gallery)
  • RecyclerView Selection 라이브러리 사용해보기
    • Selection 라이브러리를 활용하여 아이템 다중 선택 시, 아이템 뷰에 선택 효과 주기
    • 아이템 뷰의 특정 버튼 클릭 시, 아이템 삭제하기

도전 과제

  • MotionLayout 이용해서 모션 및 위젯 애니메이션 넣어보기
  • (가짜) 서버통신 해보기
    • kotlinx-serialization으로 JSON 데이터 파싱하여 HomeFragment 리스트에 넣기

실행 결과

Screen_Recording_20230421_103731_GO.SOPT.Android.mp4

@leeeha leeeha added Essential 필수 과제 Advanced 심화 과제 labels Apr 21, 2023
@leeeha leeeha requested review from b1urrrr, sxunea, lsakee and a team April 21, 2023 02:01
@leeeha leeeha self-assigned this Apr 21, 2023
Copy link

@lsakee lsakee left a comment

Choose a reason for hiding this comment

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

배우고 갑니다 ㅎㅎ

private fun initRecyclerView(dataSet: ArrayList<MultiViewItem>) {
binding.rvHome.apply {
adapter = MultiViewAdapter(dataSet)
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
Copy link

Choose a reason for hiding this comment

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

xml recycler view 속성에 layoutManager 이용하셔도 될거 같아요!

Choose a reason for hiding this comment

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

만약 XML에서 사용하실 경우 orientation도 반드시 지정해주셔야 한다는 점 !

Copy link
Member Author

Choose a reason for hiding this comment

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

다들 코멘트 감사합니다!!

import org.android.go.sopt.databinding.ItemTextBinding

/** sealed class는 추상 클래스로 자식 클래스의 종류를 제한할 수 있다. */
sealed class MultiViewHolder<E : MultiViewItem>(
Copy link

Choose a reason for hiding this comment

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

sealed class 를 이용해서 하는 방법도 있군요 배워갑니다 ㅎ

Copy link
Member

Choose a reason for hiding this comment

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

정말 코드들하고 네이밍이 너무 좋아서 많이 배웠습니다ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

저도 이번에 구글링으로 처음 배운 코드들이 대부분인 거 같아요!! 온전히 제가 짠 코드는 아니라는 점,, 참고로 알아주세요!

initBnvItemReselectedListener()
}

private fun initBnvItemReselectedListener() {
Copy link

Choose a reason for hiding this comment

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

세미나때 하신 말처럼 네이밍이 길어져도 훨씬 좋은 이름이네요 ! 네이밍하는 방법 배우겠습니다 ㅎ

Copy link
Member

Choose a reason for hiding this comment

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

컴포넌트의 어떤 리스너를 초기화하는 것인지까지 명시해주니까 훨씬 직관적이네요! 센스 있는 네이밍 방식 배워갑니다 🤭

import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment

abstract class BindingFragment<T : ViewDataBinding>(

Choose a reason for hiding this comment

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

바인딩 프래그먼트까지 ,,,,, 👍

Comment on lines 8 to 14
<variable
name="name"
type="String" />

<variable
name="author"
type="String" />
Copy link

Choose a reason for hiding this comment

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

Repo data class 사용하셔도 될거 같습니다 !

Copy link
Member Author

Choose a reason for hiding this comment

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

코멘트 참고해서 item_github_repo.xml은 삭제하고 item_image.xml 파일만 남겨뒀어요!

import org.android.go.sopt.databinding.ItemTextBinding

/** sealed class는 추상 클래스로 자식 클래스의 종류를 제한할 수 있다. */
sealed class MultiViewHolder<E : MultiViewItem>(
Copy link
Member

Choose a reason for hiding this comment

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

정말 코드들하고 네이밍이 너무 좋아서 많이 배웠습니다ㅎㅎ

Comment on lines +32 to +35
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

Choose a reason for hiding this comment

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

큰 문제는 없겠지만, 부모의 onDestroyView가 뷰를 파괴하는 역할이므로, 그 위에 _binding 에 null을 넣어 해제해주는건 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

공식문서 코드를 다시 봤는데 아래처럼 작성되어 있더라구요..!

프래그먼트 뷰가 종료되고 나서도 프래그먼트 객체는 살아있기 때문에, 바인딩으로 인한 메모리 누수를 막으려면 _binding = null 을 설정해줘야 한다는 것으로 알고 있습니다. 그래서 원래대로 작성해도 괜찮지 않을까 하는 게 제 개인적인 의견입니다..!

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

Comment on lines 25 to 32
when(supportFragmentManager.findFragmentById(R.id.fcv_main)){
is HomeFragment -> {
val recyclerView = findViewById<RecyclerView>(R.id.rv_home)
recyclerView.scrollToPosition(0)
}
is GalleryFragment -> {
val recyclerView = findViewById<RecyclerView>(R.id.rv_gallery)
recyclerView.scrollToPosition(0)

Choose a reason for hiding this comment

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

요 부분에서 findViewById를 사용하지 않고도 가능할까요?

private fun initBnvItemReselectedListener() {
binding.bnvMain.setOnItemReselectedListener {
when(supportFragmentManager.findFragmentById(R.id.fcv_main)){
is HomeFragment -> {
Copy link

Choose a reason for hiding this comment

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

여기서 item의 id로 프래그먼트를 접근하면 findById를 쓰지않을수있어서 코드양이 적어질 수 있을것같아요 !

import androidx.databinding.ViewDataBinding
import org.android.go.sopt.R

class MultiViewHolderFactory {
Copy link

Choose a reason for hiding this comment

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

저는 sealed class로 해놓고 이 코드를 adapter에 전부 넣었는데 viewholderfactory를 이렇게 활용할 수 있군요 ! 저도 참고해서 리팩토링 해보겠습니다 !

<item
android:id="@+id/home_menu"
android:icon="@drawable/ic_home"
android:title="Home" />
Copy link

Choose a reason for hiding this comment

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

여기도 string resource 추출하면 더 좋을것같습니다 !

import org.android.go.sopt.ui.main.gallery.adapter.MyItemTouchHelperCallback
import org.android.go.sopt.ui.main.data.DataSources

/** DiffUtil + ListAdapter 사용해서 리사이클러뷰 성능 개선하기 */

Choose a reason for hiding this comment

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

👍

viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
(recyclerView.adapter as MyListAdapter).moveItem(

Choose a reason for hiding this comment

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

위에서 말씀드린 findByViewId 없이 scroll to top 기능을 구현하는것의 힌트는 요것입니다 !

Copy link

@giovannijunseokim giovannijunseokim left a comment

Choose a reason for hiding this comment

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

고수시군요,, 고생하셨습니다 !

binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root) {

data class TextViewHolder(

Choose a reason for hiding this comment

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

이 클래스를 data class로 만들어주신 이유가 무엇인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

구글링해서 나온 코드를 거의 그대로 사용한 거여서,, 저도 무의식적으로 따라서 친 거 같아요😅 뷰홀더는 일반 클래스로 선언하는 게 좋을 거 같아요! 피드백 감사합니다👍👍

Copy link
Member

@b1urrrr b1urrrr left a comment

Choose a reason for hiding this comment

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

직관적이고 효율적인 코드를 작성하려고 노력하시는 게 많이 느껴지네요 👍
이번 주도 과제 하시느라 고생하셨습니다 ~ 👏👏

initBnvItemReselectedListener()
}

private fun initBnvItemReselectedListener() {
Copy link
Member

Choose a reason for hiding this comment

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

컴포넌트의 어떤 리스너를 초기화하는 것인지까지 명시해주니까 훨씬 직관적이네요! 센스 있는 네이밍 방식 배워갑니다 🤭

Comment on lines 60 to 64
if (currentFragment == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fcv_main, HomeFragment())
.commit()
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (currentFragment == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fcv_main, HomeFragment())
.commit()
}
if (currentFragment == null) changeFragment(HomeFragment())

여기서도 changeFragment 함수를 활용할 수 있을 것 같습니다!

Comment on lines 67 to 71
private fun changeFragment(fragment: Fragment) {
supportFragmentManager
.beginTransaction()
.replace(R.id.fcv_main, fragment)
.commit()
Copy link
Member

@b1urrrr b1urrrr Apr 23, 2023

Choose a reason for hiding this comment

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

Android KTX에서 제공되는 Fragment KTX를 사용하면 코드를 경량화하고 불필요한 프래그먼트 객체 생성을 줄일 수 있습니다!
공식문서의 FragmentTransaction 예시에서도 ktx를 활용하고 있습니다. 한번 참고해보시면 좋을 것 같아요 :)

Comment on lines 6 to 14
class MyDiffCallback : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem === newItem
}

override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem == newItem
}
}
Copy link
Member

Choose a reason for hiding this comment

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

제너릭을 활용하여 다른 ListAdapter를 구현할 때에도 재사용할 수 있도록 구현해봐도 좋을 것 같습니다!


import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.android.go.sopt.ui.main.gallery.adapter.MyListAdapter
Copy link
Member

Choose a reason for hiding this comment

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

Optimize imports 단축키와 Reformat code 단축키를 습관화하여 사용하지 않는 import문은 제거하고 코드 가독성을 높이는 것을 추천 드립니다!
Window 기준 Optimize import는 Ctrl + Alt + O, Reformat code는 Ctrl + Alt + L 입니다.

Comment on lines 12 to 26
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
(recyclerView.adapter as MyListAdapter).moveItem(
viewHolder.adapterPosition,
target.adapterPosition
)
return true
}

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
(recyclerView.adapter as MyListAdapter).removeItem(viewHolder.layoutPosition)
}
Copy link
Member

Choose a reason for hiding this comment

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

recyclerView.adapter as MyListAdapter처럼 as를 활용한 강제 형변환은 예기치 못한 예외가 발생할 수 있기 때문에 지양하는 것이 좋다고 생각합니다. 대체할 수 있는 방법이 뭐가 있을지 찾아보시는 것을 추천 드립니다 ㅎㅎ

) : RecyclerView.Adapter<MultiViewHolder<MultiViewItem>>() {
private val multiViewHolderFactory = MultiViewHolderFactory()

@Suppress("UNCHECKED_CAST")
Copy link
Member

Choose a reason for hiding this comment

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

이 어노테이션을 추가하신 이유가 뭘까요?
@Suppress로 컴파일 오류를 무시하기보다는 안전성을 보장하는 다른 방법을 찾아보는 것이 좋다고 생각합니다!

Comment on lines 11 to 18
fun getViewHolder(parent: ViewGroup, viewType: MultiViewType): MultiViewHolder<MultiViewItem> {
return when (viewType) {
MultiViewType.TEXT ->
MultiViewHolder.TextViewHolder(viewBind(parent, R.layout.item_text))
MultiViewType.IMAGE ->
MultiViewHolder.ImageViewHolder(viewBind(parent, R.layout.item_image))
} as MultiViewHolder<MultiViewItem>
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
fun getViewHolder(parent: ViewGroup, viewType: MultiViewType): MultiViewHolder<MultiViewItem> {
return when (viewType) {
MultiViewType.TEXT ->
MultiViewHolder.TextViewHolder(viewBind(parent, R.layout.item_text))
MultiViewType.IMAGE ->
MultiViewHolder.ImageViewHolder(viewBind(parent, R.layout.item_image))
} as MultiViewHolder<MultiViewItem>
}
import org.android.go.sopt.ui.main.home.adapter.MultiViewType.IMAGE
import org.android.go.sopt.ui.main.home.adapter.MultiViewType.TEXT
fun getViewHolder(parent: ViewGroup, viewType: MultiViewType): MultiViewHolder<MultiViewItem> {
return when (viewType) {
TEXT ->
MultiViewHolder.TextViewHolder(viewBind(parent, R.layout.item_text))
IMAGE ->
MultiViewHolder.ImageViewHolder(viewBind(parent, R.layout.item_image))
} as MultiViewHolder<MultiViewItem>
}

사람마다 코드 스타일이 다르지만 저는 상수값 임포트를 통해 코드를 간략화하는 편입니다!

Comment on lines 36 to 54
private fun initBnvItemSelectedListener() {
binding.bnvMain.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.home_menu -> {
changeFragment(HomeFragment())
true
}
R.id.gallery_menu -> {
changeFragment(GalleryFragment())
true
}
R.id.search_menu -> {
changeFragment(SearchFragment())
true
}
else -> false
}
}
}
Copy link
Member

@b1urrrr b1urrrr Apr 23, 2023

Choose a reason for hiding this comment

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

Suggested change
private fun initBnvItemSelectedListener() {
binding.bnvMain.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.home_menu -> {
changeFragment(HomeFragment())
true
}
R.id.gallery_menu -> {
changeFragment(GalleryFragment())
true
}
R.id.search_menu -> {
changeFragment(SearchFragment())
true
}
else -> false
}
}
}
private fun initBnvItemSelectedListener() {
binding.bnvMain.setOnItemSelectedListener { item ->
when (item.itemId) {
R.id.home_menu -> changeFragment(HomeFragment())
R.id.gallery_menu -> changeFragment(GalleryFragment())
R.id.search_menu -> changeFragment(SearchFragment())
else -> return@setOnItemSelectedListener false
}
true
}
}

이렇게 쓸 수도 있을 것 같아요!
아니면 changeFragment를 true를 반환하도록 구현해도 될 것 같네요

@leeeha leeeha requested a review from b1urrrr April 29, 2023 11:29
@leeeha leeeha linked an issue Apr 29, 2023 that may be closed by this pull request
7 tasks
Copy link
Member

@b1urrrr b1urrrr left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@leeeha leeeha merged commit 456330c into develop Apr 30, 2023
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

이 방법이 더 좋아보여 저도 scroll to top 기능 수정했어요 ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

엇 참고가 되었다니 다행이네요!! 제 코드에 관심 가져주셔서 감사합니다🙂

@leeeha leeeha added the Challenge 도전 과제 label Jun 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Advanced 심화 과제 Challenge 도전 과제 Essential 필수 과제
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[2주차 과제] 안드로이드 UI 구현 심화
7 participants