과제 목표 (메인 페이지)
- [v] 디자인 및 화면 구성을 최대한 동일하게 해주세요. (사이즈 및 여백도 최대한 맞춰주세요.) ✨
- [v] 상품 데이터는 아래 dummy data 를 사용합니다. (더미 데이터는 자유롭게 추가 및 수정 가능)
- [v] 데미데이터 : 이미지 링크 상품 리스트 링크 (←링크 권한 없으면 여기 클릭)
- [v] RecyclerViewer를 이용해 리스트 화면을 만들어주세요.
- [v] 상단 툴바를 제거하고 풀스크린 화면으로 세팅해주세요. (statusbar는 남기고)
- [v] 상품 이미지는 모서리를 라운드 처리해주세요.
- [v] 상품 이름은 최대 두 줄이고, 그래도 넘어가면 뒷 부분에 …으로 처리해주세요.
- [v] 뒤로가기(BACK)버튼 클릭시 종료하시겠습니까? [확인][취소] 다이얼로그를 띄워주세요. (예시 비디오 참고)
- [v] 상단 종모양 아이콘을 누르면 Notification을 생성해 주세요. (예시 비디오 참고)
- [v] 상품 가격은 1000단위로 콤마(,) 처리해주세요.
- [v] 상품 아이템들 사이에 회색 라인을 추가해서 구분해주세요.
- 상품 선택시 아래 상품 상세 페이지로 이동합니다.
- 상품 상세페이지 이동시 intent로 객체를 전달합니다. (Parcelize 사용)
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())
}
}
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 // 채팅 수
)
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
}
}
값 | assets | res/raw |
---|---|---|
파일명 | 자유롭게 설정 가능 | a-z, 0-9, _ 만 사용가능 |
서브 폴더 | 사용 가능 | 사용 불가 |
파일 리스팅 | list()로 가능 | 불가능 |
컴파일 타임 파일 체크 | 없음 | 있음 |
XML에서 접근 | 불가능 | 가능 |
환경별 설정 분리 | 불가능 | 가능 |
XML과 연계하여 다루어야 할 때는
res/raw
폴더를 사용하고, 그렇지 않을 경우에는assets
폴더를 사용하면 된다.
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)
}
}
<?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>
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>