# 완성작
간단한 뮤직플레이어다. 간단하지만 어려웠다. ViewModel의 개념이 들어가면서 깊게 다루지는 않았지만 꽤나 헷갈리는게 많았다. api를 통해 받아온 노래들을 통해 앱을 구성했다.
레트로핏 통신이나, 리사이클러뷰를 구성하는 자세한 방법들은 생략하도록 하겠다.
# 공부한 내용
이번강의에서는 DTO, ENTITY, MODEL, MAPPER 등으로 분리시켰다. DTO는 API호출을 통해 받아온 데이터들, 엔티티는 받아온 데이터들의 1:1 DB와 매칭되는 값, MODEL은 앱내에서 쓰이는 데이터들이라고 했다. 이러한 MODEL을 MAPPER를 통해서 생성해 냈다.
- drawble에서 id값으로 재정의 해주는 방법
- 그룹 만들기
- 모델 맵퍼 만들기
- 리스트 어댑터 만들기
- 플레이어 만들기
- setMusicList만들
- 모델 분리하기
- 플레이어 UI 업데이트하기
- SeekBar
# drawble에서 id 재정의하여 속성 주기
해당 SeekBar영역의 배경과 progress의 색, radius와 같은 속성을 progressDrawble을 통해 한번에 변경 해 줄 수 있다.
다음과 같이 android:id = 에 background와 progress옵션을 각각 적어주고, 그안에 속성을 적어준 후,
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="2dp"/>
<solid android:color="@color/seek_background"/>
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="2dp"/>
<stroke android:width="2dp"
android:color="@color/red"/>
<solid android:color="@color/red"/>
</shape>
</clip>
</item>
</layer-list>
실제 사용되는 seekBar에 progressDrawable에 생성한 xml을 연결해 주면 된다.
android:progressDrawable="@drawable/player_seek_background"
# 그룹(Group)
<androidx.constraintlayout.widget.Group
android:id="@+id/playerViewGroup"
android:layout_width="wrap_content"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="artistTextView,trackTextView,imageCardView,playerSeekBar,currentPlayTimeTextView,totalPlayTimeTextView"
android:layout_height="wrap_content"/>
<androidx.constraintlayout.widget.Group
android:id="@+id/playlistViewGroup"
android:layout_width="wrap_content"
app:constraint_referenced_ids="playListTtleTextView,playListRecyclerView,playListSeekBar"
android:layout_height="wrap_content"/>
다음과 같이 playlist버튼을 누르게 되면 두개의 UI화면이 전환되는 것을 볼 수 있는데, 여기서는 이 두화면을 액티비티 또는 프래그먼트로 분리 시키지 않고, visiable속성을 true/false로 주었다. 그렇게 하기 위해서는 한 xml의 두개의 상황의 UI가 나오게 되는데 하나씩 visiable속성을 제어하기에는 번거롭기 떄문에 그룹(Group)을 통해서 묶어 줄 수 있다. 이후 프래그먼트나 액티비티에서 뷰그룹을 통해 조작하면 코드수를 줄일 수 있다.
# 모델 분리 및 맵퍼 만들기
레트로핏을 통해 받아온 데이터는 Musics로 안에 여러 뮤직들이 리스트형태로 들어있고, 리스트 안에 들어가는 뮤직들을 정의하고자 뮤직 엔티티를 만들어야 한다.
## 서버에서 받아온 데이터
data class MusicsDto(
val musics : List<MusicEntity>
)
data class MusicEntity(
val track : String,
val streamUrl : String,
val artist : String,
val coverUrl : String,
)
위 두개의 데이터 클래스는 서버에서 받아온 데이터들이기 때문에, 앱 내에서 다른 값이 쓰인다 하더라도 따로 정의할 수 있는게 없다. 받아온 그 자체이기 때문이다.
## 앱 내에서 쓰이는 데이터
data class MusicModel(
val id: Long,
val track: String,
val streamUrl : String,
val artist : String,
val coverUrl: String,
val isPlaying : Boolean = false,
)
앱 내에서쓰이는 id라는 값과, 현재 재생중인지를 알 수 있는 isPlaying값을 추가적으로 사용하게 된다. 앱 내에서 쓰이는 데이터 클래스를 별도로 정의 시키고, 이 데이터 모델을 사용해야 한다.
## mapper
// MusicModelMapper.kt
fun MusicEntity.mapper(id : Long) : MusicModel =
MusicModel(
id= id,
track = this.track,
streamUrl = this.streamUrl,
artist = this.artist,
coverUrl = this.coverUrl
)
MusicEntity를 MusicModel로 바꿔주는 맵퍼를 정의한다.
val playMusicList = it.musics.mapIndexed { index, musicEntity ->
musicEntity.mapper(index.toLong())
}
이후 위의 형태처럼, mapper를 통해 musicentitly에서 MusicModel로 변환시켜 사용한다.
사실 mapper를 어떤 기준으로 언제 만들어지는지는 아직 잘 모르겠지만, 이런게 있다는 걸 알고 어떤 기능을 하는지에 초점을 맞추며 공부중이다.
# 리스트 어댑터 만들기
class PlayListAdapter(private val callback : (MusicModel) -> Unit) : ListAdapter<MusicModel, PlayListAdapter.ViewHolder>(diffUtill) {
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: MusicModel) {
val trackTitle = view.findViewById<TextView>(R.id.trackTitleTextView)
val artist = view.findViewById<TextView>(R.id.artistTextView)
val image = view.findViewById<ImageView>(R.id.coverImageView)
trackTitle.text = item.track
artist.text = item.artist
Glide.with(image.context)
.load(item.coverUrl)
.into(image)
itemView.setOnClickListener {
callback(item)
}
// 재생중일시 백그라운드 색 변경
if(item.isPlaying) {
itemView.setBackgroundColor(Color.GRAY)
artist.setTextColor(Color.BLACK)
}else {
itemView.setBackgroundColor(Color.TRANSPARENT)
artist.setTextColor(Color.GRAY)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_music_rc, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object {
val diffUtill = object : DiffUtil.ItemCallback<MusicModel>() {
override fun areItemsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MusicModel, newItem: MusicModel): Boolean {
return oldItem == newItem
}
}
}
}
ListAdapter를 상속하는 어댑터를 만들어 준다. isPlaying여부에 따라서 배경색을 바꿔준다.
# ExoPlayer 구성하기
private fun initPlayer(mBinding: FragmentPlayerBinding) {
// 플레이어 초기화
context?.let {
exoPlayer = SimpleExoPlayer.Builder(it).build()
}
mBinding.playerView.player = exoPlayer
// 플레이어 리스너
binding?.let{ binding ->
exoPlayer?.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if(isPlaying) {
// 재생상태이기에 퍼스 아이콘
binding.playerControlImageView.setImageResource(R.drawable.ic_baseline_pause_48)
}else {
binding.playerControlImageView.setImageResource(R.drawable.ic_baseline_play_48)
}
}
})
}
}
exoPlayer를 초기화 시켜주고, isPlaying에 따라서 Pause아이콘 또는 Play아이콘으로 바꿔준다, 이후 여기 리스너에 2가지를 더 추가해야 한다.
플레이 버튼을 눌렀을때 UI만 변경되면 소용이 없기때문에, exoPlayer가 직접 작동하도록 pause()와 play()기능을 연결시켜준다.
private fun initPlayControlButtons(mBinding: FragmentPlayerBinding) {
mBinding.playerControlImageView.setOnClickListener {
//플레이어 널처리
val player = exoPlayer ?: return@setOnClickListener
if(player.isPlaying) {
player.pause()
}else {
player.play()
}
}
mBinding.skipPreviousImageView.setOnClickListener {}
mBinding.skipNextImageView.setOnClickListener {}
}
# exoPlayer에 Music리스트 등록해놓기
private fun setMusicList(modelList: List<MusicModel>) {
context?.let {
exoPlayer?.addMediaItems(modelList.map{ musicModel ->
MediaItem.Builder()
.setMediaId(musicModel.id.toString())
.setUri(musicModel.streamUrl)
.build()
})
exoPlayer?.prepare()
}
}
exoPlayer에 뮤직들리스트들을 넣어둔다.
# 중간 마무리
현재까지 전체적인 UI구성과 최소한의 기능이 구현 됐다. 그러나 현재는 next, previous버튼이 동작하지 않으며, 현재 재생 되고 있는 노래의 백그라운드가 변경되지도 않는 등 연결되지 않는 동작들이 많다. 그 전까지는 그냥 메인 액티비티에서 이러한 정보들을 쏟아 부었는데 이번에는 아마 ViewModel이라고 불리우는 새로운 모델을 정의해서 데이터들을 관리한다. 사실 아직은 정확히 이해는 안가지만, 내가 아키텍쳐 아키텍처 외쳤던것 처럼 어떠한 구조를 가지게 되어 매우 흥미로웠다. 여기서 끊고 다음편에서 이어서 이 모델들이 어떻게 사용되며 관리되는지 정리하도록 하겠다.
p.s 해커톤 상금 들어왔다! 상장오면 후기 적으려고 했는데..