[TIL][내일배움캠프]

[내일배움캠프][TIL] 24.01.08 (월) - Android 앱개발 숙련 실습과제 1일차

kimlaurant 2024. 1. 8. 20:49
1. Android 앱개발 숙련 실습과제 1일차

 

저번주에는 Android에서 데이터 전달을 효율적으로 할 수 있는 ViewBinding, RecyclerView, Fragment를 학습했다.

 

이번주는 이제 이걸 실제로 써먹을 수 있는 과제를 진행하고자 한다.

 

 

실습과제 - 사과 마켓

 

과제 제목부터 그 어딘가의 당근이 떠오르는데 실제 구동 예시 화면을 보면 그 앱 맞다.

 

 

이번 실습과제의 목표는 본인이 직접 위의 UI처럼 만들면 된다.

 

다음은 세부적인 필수 구현 사항이다.

 

  • 메인페이지
    • 디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
    • RecyclerViewer를 이용해 리스트 화면을 만들어주세요.
    • 상품 이미지는 모서리를 라운드 처리해주세요.
    • 상품 이름은 최대 두 줄이고, 그래도 넘어가면 뒷 부분에 …으로 처리해주세요.
    • 뒤로가기(BACK)버튼 클릭시 종료하시겠습니까? [확인][취소] 다이얼로그를 띄워주세요.
    • 상단 종모양 아이콘을 누르면 Notification을 생성해 주세요.
    • 상품 가격은 1000단위로 콤마(,) 처리해주세요.
    • 상품 아이템들 사이에 회색 라인을 추가해서 구분해주세요.
    • 상품 선택시 아래 상품 상세 페이지로 이동합니다.
    • 상품 상세페이지 이동시 intent로 객체를 전달합니다. (Parcelize 사용)
  • 상세페이지
    • 디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
    • 메인화면에서 전달받은 데이터로 판매자, 주소, 아이템, 글내용, 가격등을 화면에 표시합니다.
    • 하단 가격표시 레이아웃을 제외하고 전체화면은 스크롤이 되어야합니다.
    • 상단 < 버튼을 누르면 상세 화면은 종료되고 메인화면으로 돌아갑니다.

 

이 외에 선택 사항도 몇 가지 있는데 그건 필수 사항부터 구현한 이후에 진행해보도록 하자.

(참고로 밑줄친 부분은 아직 배우지 않은 내용이라 구글링을 통해서 정보를 입수해야 한다.)

 

 

메인페이지 레이아웃 / xml

 

우선 메인페이지 레이아웃부터 만들어보자.

 

일단 맨 위에 내배캠동과 종모양 아이콘이 있는 레이아웃은 고정되어 있고, 나머지 판매 물품 정보들이 있는 곳은 스크롤이 가능하다.

 

고로 위에 레이아웃은 ScrollView에 포함시키지 않고, 나머지 RecyclerView는 ScrollView에 포함시킨다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="40dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dp"
            android:layout_gravity="center"
            android:text="@string/main_myaddress"
            android:textSize="22sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_gravity="center"
            android:paddingStart="10dp"
            android:src="@drawable/ic_arrow"/>

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_gravity="center"
            android:layout_marginRight="30dp"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:srcCompat="@drawable/ic_bell" />


    </LinearLayout>

    <ScrollView
        android:id="@+id/scrollView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="40dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/linearLayout">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />


        </LinearLayout>

    </ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

이는 기본적인 틀만 보여주는 거고, 아직 디테일적인 부분은 조금 더 맞춰가야 한다.

 

 

다음은 RecyclerView에 사용할 레이아웃을 만들어보자.

RecyclerView를 돌리기 위해서 다음과 같이 기본적인 틀을 만들어야 한다.

위의 레이아웃을 구현시키는 xml은 다음과 같다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"
    android:paddingStart="10dp">

    <ImageView
        android:id="@+id/iconItem"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:scaleType="centerCrop"
        android:background="@drawable/rounded_background"
        android:clipToOutline="true"

        app:srcCompat="@drawable/sample1" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:paddingStart="10dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textItem_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/item_name"
            android:textSize="16sp"/>

        <TextView
            android:id="@+id/textItem_address"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/item_address" />

        <TextView
            android:id="@+id/textItem_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/item_price"
            android:textStyle="bold"
            android:textSize="16sp"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:layout_marginEnd="10dp"
        android:paddingEnd="15dp"
        android:orientation="horizontal"
        >

        <ImageView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:src="@drawable/chat"/>

        <TextView
            android:id="@+id/textItem_chat"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingStart="5dp"
            android:text="@string/item_chat" />

        <ImageView
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:paddingStart="2.2dp"
            android:scaleType="centerCrop"
            android:src="@drawable/heart"/>

        <TextView
            android:id="@+id/textItem_good"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingStart="5dp"
            android:text="@string/item_good" />

    </LinearLayout>


</LinearLayout>

역시나 디테일적인 부분은 추후에 더 다듬을 예정이다.

 

코드 짜기

 

기본적인 레이아웃은 만들었으니 이제 이 레이아웃을 구현시킬 코드를 짤 차례다.

 

우선은 MainActivity.kt 쪽이다.

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val dataList = mutableListOf<MyItem>()
        dataList.add(MyItem(R.drawable.sample1, "산진 한달된 선풍기 팝니다", "서울 서대문구 창천동", 1000, 25, 13))
        dataList.add(MyItem(R.drawable.sample2, "김치냉장고", "인천 계양구 귤현동", 20000, 28, 8))
        dataList.add(MyItem(R.drawable.sample3, "샤넬 카드지갑", "수성구 범어동", 10000, 5, 23))
        dataList.add(MyItem(R.drawable.sample4, "금고", "해운대구 우제2동", 10000, 17, 14))
        dataList.add(MyItem(R.drawable.sample5, "갤럭시Z플립3 팝니다", "연제구 연산제8동", 150000, 9, 22))
        dataList.add(MyItem(R.drawable.sample6, "프라다 복조리백", "수원시 영통구 원천동", 50000, 16, 25))
        dataList.add(MyItem(R.drawable.sample7, "울산 동해오션뷰 60평 복층 펜트하우스 1일 숙박권 펜션 힐링 숙소 별장", "남구 옥동", 150000, 54, 142))
        dataList.add(MyItem(R.drawable.sample8, "샤넬 탑핸들 가방", "동래구 온천제2동", 180000, 7 ,31))
        dataList.add(MyItem(R.drawable.sample9, "4행정 엔진분무기 판매합니다.", "원주시 명륜2동", 30000, 28, 7))
        dataList.add(MyItem(R.drawable.sample10, "셀린느 버킷 가방", "중구 동화동", 190000, 6, 40))

        val adapter = MyAdapter(dataList)
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }
}

역시나 이번부터는 ViewBinding을 이용해서 findViewById를 말끔히 생략했다.

(build.gradle.kts에 가서 ViewBinding{enable = true}도 잊지 말자.)

 

다음은 RecyclerView를 가동시키기 위해 필요한 어댑터 부분인 MyAdapter.kt 이다.

class MyAdapter (private val mItems: MutableList<MyItem>) : RecyclerView.Adapter<MyAdapter.Holder>() {

    interface ItemClick {
        fun onClick(view: View, position: Int)
    }

    var itemClick : ItemClick? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        holder.itemView.setOnClickListener{
            itemClick?.onClick(it, position)
        }
        val dec = DecimalFormat("#,###")

        holder.iconImageView.setImageResource(mItems[position].aIcon)
        holder.name.text = mItems[position].aName
        holder.address.text = mItems[position].aAddress
        holder.price.text = "${dec.format(mItems[position].aPrice)}원"
        holder.chat.text = "${mItems[position].aChat}"
        holder.good.text = "${mItems[position].aGood}"
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemCount(): Int {
        return mItems.size
    }

    inner class Holder(private val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
        val iconImageView = binding.iconItem
        val name = binding.textItemName
        val address = binding.textItemAddress
        val price = binding.textItemPrice
        val chat = binding.textItemChat
        val good = binding.textItemGood
    }
}

여기서

iconImageView = 사진
name = 상품명

address = 주소
price = 가격

chat = 댓글수
good = 좋아요수

이다.

 

또, 여기에 필수 구현 사항 중 하나가 들어갔는데

val dec = DecimalFormat("#,###")

이 부분이 바로 천 단위마다 콤마를 찍어주는 명령어다.

이걸 holder.price.text 부분에 "&{dec.format(mItem[position].aPrice)}원"을 넣어서 구현시키면 된다.

 

마지막으로 이들의 데이터들을 담당할 Data Class인 MyItem.kt이다.

@Parcelize
data class MyItem (val aIcon: Int, val aName: String, val aAddress: String, val aPrice: Int, val aChat: Int, val aGood: Int) : Parcelable

 

여기서 Parcelize를 쓰게 되는데 이 때 Parcelize를 쓰려면 build.gradle.kts에 들어가서 plugin 부분에

id("kotlin-parcelize")

 

을 추가해야 한다. 

 

 

이 모든 걸 완성한 뒤 가동시킨 메인페이지 UI는 다음과 같다.

얼추 위의 메인페이지 UI와 비스무리하다.

 

 

트러블슈팅 & 피드백

 

숙련 주차이니만큼 하루동안 개인과제를 하면서 마주한 문제점들과 개선점 등을 한 번 써봤다.

 

 

1. 트러블슈팅

  • LinearLayout 이슈 -> 이제껏 사용했던 ConstraintLayout에 비해서 UI의 위치를 바꾸는 방식이 굉장히 까다롭기 그지 없었다. 이 때문에 디테일적인 측면에서 이리 바꾸고 저리 바꾸느라 개고생했다.
  • RecyclerView 이슈 -> 이게 가장 중요하다고 해서 사전에 수도없이 학습했지만… 역시 어렵다. 뭐가 어떻게 어려운지 설명하려 해도 그냥 어렵다. 물론 그 중에서도 가장 극악은 Adapter 구현 & Activity와의 연계렸다. 그래도 역시 하다보니 결국 앱 구동까지 성공했다. 내일은 상세페이지 UI까지 가즈아!

 

2. 피드백

  • MainActivity에 있는 dataList를 따로 관리할 수 없을까? -> 만들다보니 이걸 굳이 여기다가 적을 필요가 있나 싶었다. 물론 나중에 방대한 양의 자료들은 서버나 다른 사이트에서 받아서 관리할 수 있고, 지금은 양이 얼마 되지 않아서 일일이 다 적었는데 이것도 다른 클래스에 객체 형식으로 받아와서 할 수 있지 않을까?
  • Parcelize를 선언한 것까지는 좋았는데 이걸 가지고 상세페이지(DetailActivity) 쪽으로 데이터를 전달하고 이동하는 부분을 완성하지 못했다. 내일은 상세페이지로 데이터를 전달하는 방법을 연구해서 완성시킬 예정이다.
  • 상세페이지 부분도 현재 UI를 만드는 중인데 문제가 하나 생겼다. 상세페이지 부분을 보면 상품 설명 데이터와 판매자 데이터가 추가되는데 이걸 DetailActivity에 따로 추가해야하는지 아니면 data class에 추가한 다음에 받아와야 하는지 모르겠다. (그런데 이렇게 되면 위에서 말했다시피 굳이 MainActivity에 적을 필요가 있나?)
  • 아직 메인페이지 기능 중에 다이얼로그와 Notification 기능을 구현하지 않았다. 이것도 내일 적용할 예정이다.
  • 상품 아이템들 사이에 회색 라인으로 구분하는 것도 아직 구현하지 못했다.