본문 바로가기

Android Project

[Android/Kotlin] FLO앱 클론코딩(4) - 생명주기

원래는 SongActivity에서 노래가 재생된다고 가정하고 코딩을 진행했었는데, 이번시간에는 실제로 음악이 재생될수있도록 해볼것이다 

 

그리고 생명주기를 활용해서 Activity가 onPause() 상태가 됐을때 음악을 중지하는것과 

음악을 재생하고 있다가 앱을 종료시켜도 이전에 재생되고있었던 음악(Song데이터)을 미니플레이어에 반영해주는 작업도 생명주기를 활용해서 해볼것이다

 

https://coding-juuwon2.tistory.com/84

 

[Android/Kotlin] 생명주기(Life Cycle)

생명주기의 사전적 정의를 살펴보면 "어떤것이 태어나고 죽기까지의 기간"을 말한다고 나와있다 사람으로 예를들어보면, 사람은 태어나고 죽기까지의 생명주기를 가지며 이 생명주기동안 사람

coding-juuwon2.tistory.com

생명주기 개념관련해서는 여기에 기록해두었다

 

 

 

실제 플로앱과 살짝 다른점이 있는데 실제 플로앱에서는 홈버튼을 눌러도 음악이 백그라운드에서 계속 재생이 되지만,

지금 만드는 앱에서는 생명주기를 활용해서 onPause일때 음악이 중지되도록 할것이다

 

 

# mp3 파일 재생하기 

 

먼저 음악을 재생시키기 위해서는 mp3파일의 리소스를 추가해줘야한다

res 디렉토리 하위로 raw 디렉토리를 생성해준다 

app 우클릭 -> new -> Folder -> Raw Resource Folder -> Change Folder Location 체크박스를 해제하고 Finish버튼을 눌러주면 raw 디렉토리가 추가된다

여기에 mp3음악파일을 다운로드 받아서 넣어준다. 나는 이 무료음악 사이트에서 다운받았다 (https://www.mp3juices.cc/)

 

 

이렇게 파일을 추가시켜준다. 파일이름은 꼭 소문자로 작성해주도록한다!! 안그러면 오류가 나타날수있다

 

 

 

 

이제 음악을 재생시켜줄 무언가가 필요하기때문에 미디어플레이어라는 객체를 사용해서 음악을 재생시켜줄것이다

MediaPlayer는 미디어파일을 쉽게 재생시켜주는 클래스라고 생각해주면 된다

SongActivity class 내에 아래와 같은 코드를 작성해준다

 

이 코드에서 ?의 의미는 nullable 즉, null값이 들어올수있다는 의미이다

MediaPlayer을 nullable로 설정해준 이유는 Activity가 소멸될때 MediaPlayer를 해제시켜주어야하기 때문이다

// 음악을 재생시켜줄 mediaPlayer 객체 추가
private var mediaPlayer : MediaPlayer? = null

SongActivity.kt

 

 

그리고 지금 현재 Song 데이터 클래스를 통해서 어떤음악인지에 대한 데이터를 받아오고있다

그래서 Song 데이터 클래스에 실제로 어떤 음악이 재생되는지를 알려주는 music 변수를 추가해줄것이다

package com.example.flo


//데이터 클래스
data class Song(
    val title : String = "",         //노래제목
    val singer : String = "",        //가수
    var second : Int = 0,            //현재 재생시간
    var playTime : Int = 60,          //총 재생시간
    var isPlaying : Boolean = false,     //노래가 현재 재생되고 있는지
    var music : String = ""             // 어떤 음악이 재생되는지

)

Song.kt

 

 

Song데이터 클래스에 데이터를 추가해줬으니깐 MainActivity와 SongActivity에서도 데이터를 변경해줘야한다

먼저 MainActivity로가서 music관련 데이터를 추가해준다 

// text값들을 string값으로 바꿔서 가져오기 (title,singer,second,playTime,isPlaying,music 에 해당하는 값들)
val song = Song(binding.mainMiniplayerTitleTv.text.toString(), binding.mainMiniplayerSingerTv.text.toString(),0,60,false,"music_lilac")
//binding을 사용해서 id값 가져오기
//mainPlayerCl눌렀을때 SongActivity로 이동
binding.mainPlayerCl.setOnClickListener {
    //startActivity(Intent(this, SongActivity::class.java))
    val intent = Intent(this, SongActivity::class.java)

    //putExtra를 사용해서 데이터값들을 보내줌
    intent.putExtra("title", song.title)    //putExtra를 사용해서 title(제목)데이터를 보내줌
    intent.putExtra("singer", song.singer)  //putExtra를 사용해서 singer(가수)데이터를 보내줌 (보낸데이터는 SongActivity에서 받음)
    intent.putExtra("second", song.second)
    intent.putExtra("playTime", song.playTime)
    intent.putExtra("isPlaying", song.isPlaying)
    intent.putExtra("music", song.music)

    startActivity(intent)
}

 

MainActivity.kt

 

 

MainActivity에서 music값을 넘겨주었으니깐 SongActivity에서도 똑같이 받아오는 작업을 수행해야한다

private fun initSong(){
    //MainActivity에서 보낸데이터 받음
    //만약 title값과 singer값이 넘어왔으면, getExtra를 사용해서 받아옴
    if(intent.hasExtra("title") && intent.hasExtra("singer")){
        song = Song(
            intent.getStringExtra("title")!!,
            intent.getStringExtra("singer")!!,
            intent.getIntExtra("second", 0),
            intent.getIntExtra("playTime",0),
            intent.getBooleanExtra("isPlaying", false),
            intent.getStringExtra("music")!!
        )

SongActivity.kt

 

 

그런데 지금 Song데이터 클래스에는 mp3파일 이름이 String값으로 되어있기 때문에 이것을 실제로 실행시킬려면 리소스 파일에서 해당 String값을 찾아서 리소스를 반환해주는 무언가가 필요하다. 따라서 이 부분은 resource.getIdentifier를 사용해서 반환해줄수있다. 즉, 패키지안에 있는 raw(음악파일) 디렉토리에서 song.music의 이름을 가진 mp3파일의 식별자를 반환한다.

이제 리소스를 반환받았으니 이 리소스를 미디어플레이어한테 올려줘야된다. 미디어플레이어한테 이 음악을 재생할거야 라고 알려준다. MediaPlayer.create(this, music) 코드는 context와 음악 리소스 ID를 입력값으로 갖는 MediaPlayer객체를 생성한다는 의미이다.  SongActivity에서 setPlayer함수 내에 아래 코드를 추가해준다

// String값인 muscic 리소스를 반환
val music = resources.getIdentifier(song.music,"raw", this.packageName)
// 리소스를 MediaPlayer한테 올림 (MediaPlayer한테 이음악 재생한다고 알림)
mediaPlayer = MediaPlayer.create(this,music)

SongActivity.kt

 

※ resource.getIndentifier

- 앱 리소스의 식별자를 알아내기위해 사용

- 검색할 리소스의 이름, 디렉토리명, 리소스가 속한 패키지 이름을 입력인자로 갖는다

 

 

 

이제 재생버튼과 정지버튼을 눌렀을때 음악이 재생되고 정지되도록 만들어보겠다

//재생버튼 눌렀을때 이미지 바뀌는 함수
fun setPlayerStatus(isPlaying : Boolean){
    //재생버튼을 눌렀을때 song.isPlaying값과 timer.isPlaying을 초기화
    song.isPlaying = isPlaying
    timer.isPlaying = isPlaying

    if(isPlaying){
        binding.songMiniplayerIv.visibility = View.GONE
        binding.songPauseIv.visibility = View.VISIBLE
        //음악재생
        mediaPlayer?.start()
    }
    else{
        binding.songMiniplayerIv.visibility = View.VISIBLE
        binding.songPauseIv.visibility = View.GONE
        //음악중지
        if (mediaPlayer?.isPlaying == true){
            mediaPlayer?.pause()
        }
    }
}

SongActivity.kt

 

 

 

여기까지하면 실제 내가 넣은 노래가 재생되고 정지되는것을 확인할수있다

 

 

 

 

 

# 음악이 재생되고 있다가 사용자가 포커스를 잃었을때 중지되도록 

 

이번엔 음악이 재생되고 있다가 사용자가 포커스를 잃었을때 중지되도록 해볼것이다 

 

현재는 SongActivity가 종료되거나 홈버튼을 눌러서 앱이 보이지 않게 되더라도 음악이 계속해서 재생된다. SongActivity가 완전히 종료될때는 LifeCycle의 onDestroy()가 호출되고, 홈버튼을 눌렀을때는 onPause()가 호출되는데 문제는 onDestroy가 호출될때 발생한다. onDestroy가 호출되면 Activity에 할당된 Thread가 정리되기때문에 Timer가 동작하지 않아서, 음악은 재생되고있는데 시간과 SeekBar는 움직이지 않게되는 상황이 발생한다.

따라서 LifeCycle(생명주기)를 사용해서 이러한 문제를 해결할것이다.

 

Activity 생명주기 함수인 onPause()함수를 사용해준다

// 사용자가 포커스 잃었을때 음악이 중지
override fun onPause() {
    super.onPause()
    setPlayerStatus(false)
}

SongActivity.kt

 

그리고 저번에 추가해줬던 onDestroy()함수에 mediaPlayer?.release() 라는 코드를 추가해서 미디어플레이어가 갖고있던 리소스를 해제시켜주는것이 좋다. 또한  mediaPlayer = null 이라는 코드도 추가하여 미디어플레이어 또한 해제시켜준다

//activity파괴될때 호출되는 onDestroy함수(불필요한 리소스방지)
override fun onDestroy() {
    super.onDestroy()
    //thread 종료
    timer.interrupt()
    mediaPlayer?.release()  //미디어플레이어가 갖고있던 리소스 해제
    mediaPlayer = null     //미디어플레이어 해제
}

SongActivity.kt

 

 

 

여기까지하면 사용자가 홈화면을 눌러서 포커스를 잃은경우에 재생되던 음악이 중지되고, 다시 돌아왔을때도 SeekBar와 시간도 잘 반영되어 있는것을 확인할수있다

 

 

 

 

 

# SharedPreference 사용하기

1) SongActivity에서 Song데이터 저장하기

 

SharedPreference를 사용해서 SongActivity에서 재생되고있던 Song데이터를 저장해서 앱을 종료했다가 다시 메인화면으로 돌아왔을때, 미니플레이어에 이전에 재생되고있던 Song데이터를 반영해주는 작업을 해볼것이다

 

그러면 음악이 몇초까지 재생되었는지를 Song데이터 클래스에 반영을 해줘야한다

그리고 이 Song데이터가 앱이 종료되어도 계속 남아있을수있게 SharedPreference를 사용해서 데이터를 저장해준다

// 사용자가 포커스 잃었을때 음악이 중지
override fun onPause() {
    super.onPause()
    setPlayerStatus(false)
    // 음악이 몇초까지 재생되었는지 Song데이터 클래스에 반영
    song.second = ((binding.songProgressSb.progress * song.playTime)/100)/1000
    val sharedPreferences = getSharedPreferences("song", MODE_PRIVATE)

}

SongActivity.kt

SharedPreference란 내부저장소에 데이터를 저장할수있게 해주는것으로, 앱을 종료했다가 다시 실행해도 저장된 데이터를 꺼내서 다시 사용할수있도록 해주는것이다. 

이는 간단한 설정값같은 데이터를 저장해줄때 용이하다. 당연히 중요한 데이터나 무거운 데이터를 저장할때는 DB나 서버,파일의 형태로 저장하겠지만,  로그인할때 비밀번호 기억하기와 같은 간단한값은 SharedPreference 를 사용하는것이 매우 용이하다. 

위 sharedPreference의 코드를 살펴보면, song은 sharedPreference의 name을 말하며

MODE_PRIVATE는 sharedPreference의 모드를 private하게 사용하는것으로 자신의 앱에서만 사용가능한다는 뜻이다

MODE_PRIVATE말고 다른 모드를 사용하게되면 다른 앱에서도 접근 가능하게 해줄수도 있다(그치만 이런경우는 거의 없다)

 

sharedPreference를 만들어줬으니깐 이제 실제로 데이터를 저장해야되기때문에 editor를 만들어서 데이터를 조작해보겠다

// 사용자가 포커스 잃었을때 음악이 중지
override fun onPause() {
    super.onPause()
    setPlayerStatus(false)
    // 음악이 몇초까지 재생되었는지 Song데이터 클래스에 반영
    song.second = ((binding.songProgressSb.progress * song.playTime)/100)/1000
    val sharedPreferences = getSharedPreferences("song", MODE_PRIVATE)

    val editor = sharedPreferences.edit()
    // song의 데이터들을 sharedPreferences에 putString
    editor.putString("title", song.title)
    editor.putString("singer", song.singer)
    editor.putString("second", song.second)
    editor.putString("PlayTime", song.playTime)
    ...

}

 

그런데 이 방식데로 하다보면 song의 title, singer, second, playTime... 등의 정보를 일일이 SharedPreferences에 putString을 사용해서 보내줘야하기 때문에 번거롭다

따라서 이런 방식이 아닌 song객체를 Json포멧으로 변환하여, Song이 있는 자바객체를 통째로 SharedPreferences에 putString하는 방식을 사용해보도록 하겠다. 

 

Json = 일종의 데이터표현 표준 

자바객체를 다른곳으로 전송할때 Json포멧으로 보냄

 

따라서 방금 적었던 번거롭게 putString하는 코드들을 삭제하고 Json포멧으로 넣어줄것이다

그러면 Song객체를 Json으로 변환시켜주는 작업이 필요한데 , 이 작업을 해주는것이 Gson이다

※ Gson
Gson은 자바 객체를 Json으로, Json을 자바객체로 변환시켜주는것으로 
Json과 자바 객체간의 변환을 간단하게 처리할수있도록 도와주는 라이브러리다.

 

 

gson을 사용하기위해서 먼저 라이브러리 추가를 해준다

Module 수준의 build.gradle 파일에 gson 관련 의존성을 추가해준뒤 sync now를 눌러 동기화를 시켜준다

implementation 'com.google.code.gson:gson:2.8.7'

 

 

 

다시 SongActivity로 돌아가서 Gson선언을 해주고,

Gson을 통해서 Song객체를 SharedPreferences에 넣어주겠다

그러면 gson이 song객체를 json포멧으로 변환시켜주게되고 이것을 editor에 넣어주면된다

그리고 마지막에 editor.apply() 함수까지 호출해줘야지 저장작업까지 완료가 되기때문에 이 코드까지 적어줘야한다

class SongActivity : AppCompatActivity(){

    lateinit var binding : ActivitySongBinding
    //Song 데이터클래스를 초기화하기위해 전역변수 선언
    lateinit var song: Song
    lateinit var timer : Timer
    // 음악을 재생시켜줄 mediaPlayer 객체 추가
    private var mediaPlayer : MediaPlayer? = null
    // Gson 선언
    private var gson : Gson = Gson()
// 사용자가 포커스 잃었을때 음악이 중지
override fun onPause() {
    super.onPause()
    setPlayerStatus(false)
    // 음악이 몇초까지 재생되었는지 Song데이터 클래스에 반영
    song.second = ((binding.songProgressSb.progress * song.playTime)/100)/1000
    val sharedPreferences = getSharedPreferences("song", MODE_PRIVATE)

    val editor = sharedPreferences.edit()
    val songJson = gson.toJson(song)
    editor.putString("songData", songJson)

    editor.apply()
}

SongActivity.kt

 

여기까지 해주면 SongActivity에서 Song데이터를 내부저장소에 저장하는 작업까지 완료가 되는 것을 확인할수있다

 

 

 

 

2) MainActivity에서 Song데이터 가져오기

 

이제 SongActivity에서 저장되어있었던 Song데이터를 가져와서 MainActivity에 있는 미니플레이어에 반영을 해보도록 하겠다.  미니플레이어에도 프로그레스바를 나타내줄것이기 때문에 main_activity.xml로 돌아가서 미니플레이어 위에 SeekBar를 추가해준다

<SeekBar
    android:id="@+id/main_miniplayer_progress_sb"
    android:layout_width="match_parent"
    android:layout_height="10dp"
    android:background="@null"
    android:paddingEnd="0dp"
    android:paddingStart="0dp"
    android:layout_marginBottom="-4dp"
    android:progressBackgroundTint="@color/gray_color"
    android:progressTint="@color/select_color"
    android:progress="0"
    android:max="100000"
    android:thumb="@color/transparent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintBottom_toTopOf="@id/main_player_cl" />

main_activity.xml

 

 

 

SongActivity에 있던 Song데이터를 MainActivity에 있는 미니플레이어에 반영하는 작업은 onStart()함수 내에서 해줄것이다.  MainActivity에서 SongActivity로 갔다가 SongActivity에서 창을 닫고 다시 MainActivity로 돌아오게되면 onStart()함수 부터 시작되기 때문이다. 즉, 엑티비티 전환이 될때 onStart()함수부터 시작되기 때문이다.

따라서 SongActivity에서의 데이터를 반영해주기 위해서는 onStart()함수에서 해준다

 

그래서 onStart()함수에서 SharedPreferences에 있던 값을 가져오고,

SharedPreferences에 저장되어있던 songData를 가져온다

그 후에 가져온값을 songData에 담아줘야되는데, song변수와 gson변수가 아직 선언이 안되어있기 때문에 전역변수로 선언해준다

그리고 가져온 값을 song데이터에 넣어주는 작업을 할건데 처음에는 SharedPreferences에 저장된 값이 아무것도 없을것이기 때문에 오류가 나지 않도록  songJson이 null일때는 Song데이터를 직접넣어주는 작업을해주고,

데이터가 존재할때는 SharedPreferences에 저장된 값을 가져오도록 gson을 사용해서 songjson을 Song::class.java객체로 변환해준다

 

이제 Song데이터에 반영을 다 해줬으면 미니플레이어에 반영되도록 함수를 생성해준다

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    // song,gson 전역변수 선언
    private var song : Song = Song()
    private var gson : Gson = Gson()

	...

    
    // 미니플레이어에 반영하는 함수
    // song데이터를 가져오니깐 매개변수로 song선언
    private fun setMiniPlayer(song: Song){
        binding.mainMiniplayerTitleTv.text = song.title
        binding.mainMiniplayerSingerTv.text = song.singer
        binding.mainMiniplayerProgressSb.progress = (song.second * 100000)/song.playTime

    }


    override fun onStart() {
        super.onStart()
        // SharedPreferences에 있는 값을 가져옴
        val sharedPreferences = getSharedPreferences("song", MODE_PRIVATE)
        val songJson = sharedPreferences.getString("songData",null)

        // 가져온값을 song에 넣기
        song = if (songJson == null){
            Song("라일락","아이유(IU)",0,60,false,"music_lilac")
        }else{
            gson.fromJson(songJson, Song::class.java)
        }

        // setMiniPlayer함수 호츌
        setMiniPlayer(song)

    }
}

MainActivity.kt

 

 

// text값들을 string값으로 바꿔서 가져오기 (title,singer,second,playTime,isPlaying,music 에 해당하는 값들)
val song = Song(binding.mainMiniplayerTitleTv.text.toString(), binding.mainMiniplayerSingerTv.text.toString(),0,60,false,"music_lilac")

그리고 이제는 Song을 직접 생성하는 대신에, SharedPreferences에서 저장된 값을 가져오기 때문에 위의 코드는 이제 필요하지 않아서 삭제해준다

 

 

 

이렇게 코드를 다 짜주면, SongActivity에서 노래를 재생하고 앱을 종료하고 들어오면 미니플레이어 진행바에 진행율이 올바르게 표시되어있는것을 확인할수있을것이다

 

[실행 에뮬레이터 첨부]