[Android / Kotlin] AppleMarket (1)

Subeen·2024년 1월 5일
0

Android

목록 보기
31/71

과제 목표 (메인 페이지)

  • [v] 디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
  • [v] 상품 데이터는 아래 dummy data 를 사용합니다. (더미 데이터는 자유롭게 추가 및 수정 가능)
  • [v] 데미데이터 : 이미지 링크 상품 리스트 링크 (←링크 권한 없으면 여기 클릭)
  • [v] RecyclerViewer를 이용해 리스트 화면을 만들어주세요.
  • [v] 상단 툴바를 제거하고 풀스크린 화면으로 세팅해주세요. (statusbar는 남기고)
  • [v] 상품 이미지는 모서리를 라운드 처리해주세요.
  • [v] 상품 이름은 최대 두 줄이고, 그래도 넘어가면 뒷 부분에 …으로 처리해주세요.
  • [v] 뒤로가기(BACK)버튼 클릭시 종료하시겠습니까? [확인][취소] 다이얼로그를 띄워주세요. (예시 비디오 참고)
  • [v] 상단 종모양 아이콘을 누르면 Notification을 생성해 주세요. (예시 비디오 참고)
  • [v] 상품 가격은 1000단위로 콤마(,) 처리해주세요.
  • [v] 상품 아이템들 사이에 회색 라인을 추가해서 구분해주세요.
  • 상품 선택시 아래 상품 상세 페이지로 이동합니다.
  • 상품 상세페이지 이동시 intent로 객체를 전달합니다. (Parcelize 사용)

결과 화면

MainActivity

  • 상품 아이템들 사이에 회색 라인을 추가해서 구분
binding.productRecyclerView.addItemDecoration(
	DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
)
  • 'onBackPressed(): Unit' is deprecated. Deprecated in Java 해결
override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	setContentView(binding.root)
	this.onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
	override fun handleOnBackPressed() {
		showAlertDialog()
	}
}

class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        this.onBackPressedDispatcher.addCallback(this, onBackPressedCallback)

        initView()
    }

    private val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            showAlertDialog()
        }
    }

    private fun showAlertDialog() {
        val builder = AlertDialog.Builder(this@MainActivity)
        builder.apply {
            setTitle(getString(R.string.dialog_title))
            setMessage(getString(R.string.dialog_message))
            setIcon(R.drawable.img_main_chat_16dp)
        }

        val listener = DialogInterface.OnClickListener { _, which ->
            when (which) {
                DialogInterface.BUTTON_POSITIVE -> finish()
                DialogInterface.BUTTON_NEGATIVE -> Unit
                else -> Unit
            }
        }
        builder.setPositiveButton(getString(R.string.dialog_button_positive), listener)
        builder.setNegativeButton(getString(R.string.dialog_button_negative), listener)
        builder.show()
    }

    private fun setSpinnerLocation() {
        binding.spinnerLocation.adapter = ArrayAdapter(
            this@MainActivity,
            android.R.layout.simple_spinner_dropdown_item,
            listOf(
                getString(R.string.location_0)
            )
        )
    }

    private fun initView() {
        setSpinnerLocation()

        val items = loadList()

        val adapter = ProductAdapter(items)
        binding.productRecyclerView.adapter = adapter
        binding.productRecyclerView.layoutManager = LinearLayoutManager(this)
        binding.productRecyclerView.addItemDecoration(
            DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
        )

        adapter.itemClick = object : ProductAdapter.ItemClick {
            override fun onItemClick(view: View, position: Int) {
                Log.d("MainActivity", items[position].name)
            }

        }

        binding.ivNotification.setOnClickListener {
            notification()
        }
    }

	// 알림 설정 
    fun notification() {
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager

        val builder: NotificationCompat.Builder
		// 33 버전 이상
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) {
                // 알림 권한이 없다면, 사용자에게 권한 요청
                val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
                    putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
                }
                startActivity(intent)
            }
        }
        // 26 버전 이상
        val channelId = "one-channel"
        val channelName = "My Channel One"
        val channel = NotificationChannel(
            channelId,
            channelName,
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            // 채널에 다양한 정보 설정
            description = "My Channel One Description"
            setShowBadge(true)
            val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
            val audioAttributes = AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_ALARM)
                .build()
            setSound(uri, audioAttributes)
            enableVibration(true)
        }
        // 채널을 NotificationManager에 등록
        manager.createNotificationChannel(channel)

        // 채널을 이용하여 builder 생성
        builder = NotificationCompat.Builder(this, channelId)

        // 알림의 기본 정보
        builder.run {
            setSmallIcon(R.mipmap.ic_launcher)
            setWhen(System.currentTimeMillis())
            setContentTitle(getString(R.string.notification_title))
            setContentText(getString(R.string.notification_message))
        }

        manager.notify(11, builder.build())
    }
}

Product

data class Product(
    val id: Int, // 고유값
    val resId: Int, // 이미지
    val name: String, // 상품 제목
    val explain: String, // 상품 설명
    val seller: String, // 판매자
    val price: Int, // 상품 가격
    val location: String, // 위치
    val like: Int, // 좋아요 수 
    val chat: Int // 채팅 수 
)

ProductManager

  • xlsx 파일 Load

Project ➡️ app ➡️ New ➡️ Assets Folder 생성 후 하위에 xlsx 파일을 .tsv로 변환 후 가져오기
데이터가 탭으로 구분되어 있어서 .tsv로 가져온다.

object ProductManager {
    private var items: MutableList<Product> = arrayListOf()

	// assets 폴더에서 파일 불러오는 방법 
    fun Context.loadList(): MutableList<Product> {
        val assetManager = assets
        val inputStream = assetManager.open("dummy_data.tsv")
        // 줄 단위로 입력을 받아 문자열 형식으로 반환 
        val bufferedReader = BufferedReader(InputStreamReader(inputStream))
        bufferedReader.forEachLine {
            val tokens = it.split("\t")
            val resource = resources.getIdentifier(tokens[1], "drawable", packageName)
            val post = Product(
                tokens[0].toInt(),
                resource,
                tokens[2],
                tokens[3],
                tokens[4],
                tokens[5].toInt(),
                tokens[6],
                tokens[7].toInt(),
                tokens[8].toInt()
            )
            items.add(post)
        }
        return items
    }
}

읽기 전용 파일 저장 폴더

assetsres/raw
파일명자유롭게 설정 가능a-z, 0-9, _ 만 사용가능
서브 폴더사용 가능사용 불가
파일 리스팅list()로 가능불가능
컴파일 타임 파일 체크없음있음
XML에서 접근불가능가능
환경별 설정 분리불가능가능

XML과 연계하여 다루어야 할 때는 res/raw 폴더를 사용하고, 그렇지 않을 경우에는 assets 폴더를 사용하면 된다.

ProductAdapter

  • 숫자 세 자리마다 쉼표 출력
private fun decimalFormat(price: Int): String {
	val dec = DecimalFormat("#,###")
	return dec.format(price)
}

class ProductAdapter(private val items: MutableList<Product>) :
    RecyclerView.Adapter<ProductAdapter.Holder>() {

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

    var itemClick: ItemClick? = null

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

    override fun onBindViewHolder(holder: ProductAdapter.Holder, position: Int) {
        holder.itemView.setOnClickListener {
            itemClick?.onItemClick(it, position)
        }
        holder.productImageView.setImageResource(items[position].resId)
        holder.productImageView.clipToOutline = true

        holder.productName.text = items[position].name
        holder.productLocation.text = items[position].location
        val price = "${decimalFormat(items[position].price)}원"
        holder.productPrice.text = price

        val chatCount = "${items[position].chat}"
        val likeCount = "${items[position].like}"
        holder.productChatCount.text = chatCount
        holder.productLikeCount.text = likeCount
    }

    override fun getItemCount(): Int = items.size

    inner class Holder(private val binding: ItemProductRecyclerBinding) :
        RecyclerView.ViewHolder(binding.root) {
        val productImageView = binding.ivProduct
        val productName = binding.tvProductName
        val productLocation = binding.tvProductLocation
        val productPrice = binding.tvProductPrice
        val productChatCount = binding.tvChatCount
        val productLikeCount = binding.tvLikeCount

    }

    private fun decimalFormat(price: Int): String {  // 숫자 세 자리마다 쉼표 출력
        val dec = DecimalFormat("#,###")
        return dec.format(price)
    }
}

activity_main

<?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">

    <Spinner
        android:id="@+id/spinnerLocation"
        android:layout_width="wrap_content"
        android:layout_height="32dp"
        android:layout_margin="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/ivNotification"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:layout_margin="16dp"
        android:src="@drawable/selecter_main_notice_32dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/productRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/spinnerLocation" />

</androidx.constraintlayout.widget.ConstraintLayout>

item_product_recycler

android:maxLines="2" // 두 줄로 고정
android:ellipsize="end" // 줄이 넘어가면 마지막에 ...표시
<?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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <ImageView
        android:id="@+id/ivProduct"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:background="@drawable/iv_corner_radius"
        android:src="@drawable/sample1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvProductName"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:ellipsize="end"
        android:maxLines="2"
        android:text="프라다 복조리백"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ivProduct"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tvProductLocation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="5dp"
        android:text="수원시 영통구 원천동"
        android:textSize="14sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ivProduct"
        app:layout_constraintTop_toBottomOf="@id/tvProductName" />

    <TextView
        android:id="@+id/tvProductPrice"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="5dp"
        android:text="50,000원"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/ivProduct"
        app:layout_constraintTop_toBottomOf="@id/tvProductLocation" />

    <ImageView
        android:id="@+id/ivChat"
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:layout_marginEnd="5dp"
        android:src="@drawable/img_main_chat_16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/tvChatCount" />

    <TextView
        android:id="@+id/tvChatCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:text="16"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/ivLike" />

    <ImageView
        android:id="@+id/ivLike"
        android:layout_width="16dp"
        android:layout_height="16dp"
        android:layout_marginEnd="5dp"
        android:src="@drawable/img_all_emptylike"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/tvLikeCount" />

    <TextView
        android:id="@+id/tvLikeCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="25"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
profile
개발 공부 기록 🌱

0개의 댓글