HOME > android > basic

안드로이드 - Parcelable을 구현하여 Intent로 데이터를 전달하는 방법

By JS | 09 Oct 2019

Android는 프로세스간 데이터를 전달할 때 바인더를 통해 Parcel이라는 객체로 전달합니다. Parcel은 추상화된 객체로 데이터와 객체를 갖고 있는 컨테이너라고 할 수 있습니다.

그래서 우리는 전달하려는 객체를 Parcel에 저장하고 다른 프로세스로 전달하면 됩니다. Parcelable은 인터페이스이며, Parcel에 객체를 write/read 하도록 만들어줍니다.

만약 내가 정의한 클래스의 객체를 다른 액티비티에 전달하려면 Parcelable을 implements하여 구현해주면 됩니다.

Parcelable 객체 구현

다음 코드는 MyData 라는 클래스를 정의하였고 Parcelable를 구현하였습니다. 안드로이드 스튜디오에서 클래스를 생성하면 다음처럼 기본적인 코드를 자동생성해 줍니다.

import android.os.Parcel
import android.os.Parcelable

class MyData() : Parcelable{

    constructor(parcel: Parcel) : this() {
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<MyData> {
        override fun createFromParcel(parcel: Parcel): MyData {
            return MyData(parcel)
        }

        override fun newArray(size: Int): Array<MyData?> {
            return arrayOfNulls(size)
        }
    }

}
  • constructor: Parcel로부터 데이터를 읽어 객체를 생성할 때 사용합니다.
  • writeToParcel: 객체의 데이터를 Parcel에 wrtie할 때 사용됩니다.
  • describeContents: 데이터가 어떤 종류인지 설명합니다. Parcelable 객체가 file descriptor를 포함하고 있다면 CONTENTS_FILE_DESCRIPTOR를 리턴하고 그 외는 0을 리턴하라고 합니다.
  • Parcelable.Creator: 꼭 구현해야 하는 static class입니다. Parcel으로부터 객체를 만들 때 사용합니다.

describeContents() 결과 값으로 어떤 작업을 한 경험이 없는데요. Android Reference를 보면 Parcelable 객체가 File descriptor를 포함하고 있다면 CONTENTS_FILE_DESCRIPTOR를 리턴하고 그 외는 0을 리턴하라고 되어있습니다.

Parcelable 객체 구현 2

위의 클래스는 멤버 변수가 1개도 없는 클래스입니다. 멤버변수를 추가할 때마다 constructorwriteToParcel에 데이터를 read/write하는 코드를 구현해주어야 합니다.

자바에서 제공하는 Serializable을 이용하면 변수가 추가될 때마다 구현을 할 필요가 없습니다. 하지만 안드로이드의 Parcelable을 이용하면 read/write 코드를 직접 구현해줘야 합니다. Serializable과 Parcelable은 서로 장단점이 있기 때문에 어떤 것을 사용할지는 상황에 따라 결정해야 합니다. 개인적으로 안드로이드는 Parcelable로 구현되어있기 때문에 동일한 방식으로 구현하는 것을 선호합니다.

다음 코드는 위의 클래스에서 name, version, lastModified라는 3개의 변수를 추가하였습니다. 그리고 constructor와 writeToParcel에 read/write 코드를 구현하였습니다.

class MyData() : Parcelable {
    var name : String? = null
    var version : Int = 0
    var lastModified : Int = 0

    constructor(parcel: Parcel) : this() {
        name = parcel.readString()
        version = parcel.readInt()
        lastModified = parcel.readInt()
    }

    constructor(name: String?, version: Int, lastModified: Int) : this() {
        this.name = name
        this.version = version
        this.lastModified = lastModified
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(version)
        parcel.writeInt(lastModified)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<MyData> {
        override fun createFromParcel(parcel: Parcel): MyData {
            return MyData(parcel)
        }

        override fun newArray(size: Int): Array<MyData?> {
            return arrayOfNulls(size)
        }
    }

}

위의 코드를 나눠서 살펴보면, writeToParcel() 메소드에서는 Parcel에 데이터를 write합니다. writeString(), writeInt() 등 자료형마다 메소드가 지원하며 인자를 Parcel에 저장해 줍니다.

override fun writeToParcel(parcel: Parcel, flags: Int) {
    parcel.writeString(name)
    parcel.writeInt(version)
    parcel.writeInt(lastModified)
}

생성자 메소드에서는 Parcel로부터 데이터를 읽습니다. readString()을 호출하면 write했던 순서대로 데이터를 읽어옵니다. 그렇기 때문에 어떤 데이터를 가져오는거지? 라는 생각은 안하셔도 됩니다.

constructor(parcel: Parcel) : this() {
    name = parcel.readString()
    version = parcel.readInt()
    lastModified = parcel.readInt()
}

읽고 쓰는 과정은 모두 추상화되어있습니다. Intent 등의 안드로이드에서 제공하는 객체에 Parcelable 객체를 저장하면, 안드로이드는 바인더를 통해 객체가 다른 프로세스로 전달될 때 Parcel에 객체를 저장하고 전달하고 다시 Parcel에서 객체로 가져오는 과정을 모두 알아서 해 줍니다.

그래서 이 부분에 대해서 우리가 신경써야할 것은 없습니다. 단지, Parcelable 객체에 write/read 코드만 신경써서 구현해주면 됩니다.

객체를 Parcel로, Parcel을 객체로 변환하는 과정

위에서 설명한 것처럼, Parcel에 Parcelable 객체를 write/read 하는 과정은 모두 추상화되어있기 때문에 이 부분에 대해서 생각하지 않아도 됩니다. 하지만 궁금하기 때문에 예제 코드를 만들어보았습니다.

Parcelable이 어떻게 Parcel로 변환되는지 관심없으시다면 이 부분은 건너띄셔도 됩니다.

다음 코드는 MyData 객체를 Parcel로 만들고 다른 프로세스에 전달하였다고 가정하고 다시 Parcel객체를 풀어 MyData로 변환하는 과정입니다.

// MyData객체를 만듬
val myData = MyData("myDatabase", 1, 20191009)
// Parcel 객체를 만들고, MyData 객체를 저장
val p1 = Parcel.obtain()
p1.writeValue(myData)
Log.d(TAG, "origin MyData{${myData?.name}," +
        " ${myData?.version}, ${myData?.lastModified}}")

// Parcel 객체를 다른 프로세스에 전달하기 위해 Byte로 변환
val bytes: ByteArray = p1.marshall()

// 다른 프로세스에 전달되었다고 가정
// bytes 객체를 unmarshall하여 p2라는 Parcel에 저장
val p2 = Parcel.obtain()
p2.unmarshall(bytes, 0, bytes.size)
p2.setDataPosition(0)

// p2에서 데이터를 읽어 MyData로 변환
val delivered: MyData = p2.readValue(MyData::class.java.classLoader) as MyData
Log.d(TAG, "delivered MyData{${delivered?.name}," +
        " ${delivered?.version}, ${delivered?.lastModified}}")

예상한 것처럼 로그는 이렇게 출력됩니다.

com.codechacha.sample D/MainActivity: origin MyData{myDatabase, 1, 20191009}
com.codechacha.sample D/MainActivity: delivered MyData{myDatabase, 1, 20191009}

위의 코드처럼, 객체를 Parcel로 변환하고, 이 Parcel을 Bytes로 변환하여 전달 후, 다시 반대 순서로 객체를 만들 수 있습니다.

다음은 안드로이드 플랫폼에 정의되어있는 Parcel.java의 코드입니다. 코드를 보시면 Parcel.writeValue()에서 Parcelable.writeToParcel()를 호출합니다.

public final void writeValue(@Nullable Object v) {
    if (v == null) {
        writeInt(VAL_NULL);
      ....
    } else if (v instanceof Parcelable) {
        writeInt(VAL_PARCELABLE);
        writeParcelable((Parcelable) v, 0);
    }
    ....
}

public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
    ....
    p.writeToParcel(this, parcelableFlags);
}

다음은 Parcel.readValue() 관련 코드입니다. 코드를 따라가보면 우리가 위에서 정의한 CREATOR.createFromParcel()를 호출하여 객체를 생성해줍니다.

public final Object readValue(@Nullable ClassLoader loader) {
    ...
    case VAL_PARCELABLE:
        return readParcelable(loader);
    ...
}

public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
    Parcelable.Creator<?> creator = readParcelableCreator(loader);
    if (creator == null) {
        return null;
    }
    if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
      Parcelable.ClassLoaderCreator<?> classLoaderCreator =
          (Parcelable.ClassLoaderCreator<?>) creator;
      return (T) classLoaderCreator.createFromParcel(this, loader);
    }
    return (T) creator.createFromParcel(this);
}

이런 과정들을 모두 안드로이드 프레임워크가 알아서 한다고 생각하시면 됩니다. 우리는 Parcelable 클래스에 read/write 코드만 작성하면 됩니다.

사실 제가 Parcel을 bytes로 변환하고 이것을 바인더로 전달하는 코드는 보지 않았습니다. 이 부분은 추측이기 때문에 사실과 다를 수 있습니다.

Intent로 Parcelable 객체 전달하기

Intent에 Parcelable 객체를 넣고 다른 액티비티에 전달할 수 있습니다.

다음은 인텐트에 Parcelable 객체를 추가하는 코드입니다.

val INTENT_EXTRA_MY_DATA = "intent_extra_my_data"
val myData = MyData("myDatabase", 1, 20191009)

val intent = Intent(this, SubActivity::class.java)
intent.putExtra(INTENT_EXTRA_MY_DATA, myData)
startActivity(intent)

Intent.putExtra라는 메소드를 이용하면 Parcelable을 Intent에 추가할 수 있습니다. startActivity를 호출하면 인텐트에 설정한 액티비티가 실행됩니다. 액티비티가 실행될 때 Parcelable 객체도 위에서 설명한 과정을 거쳐 액티비티로 전달됩니다.

다음은 startActivity에 의해 실행된 액티비티에서 Parcelable 객체를 가져오는 코드입니다.

val INTENT_EXTRA_MY_DATA = "intent_extra_my_data"
val myData = intent?.getParcelableExtra<MyData>(MainActivity.INTENT_EXTRA_MY_DATA)
val text = "MyData{${myData?.name}, ${myData?.version}, ${myData?.lastModified}}"
textView.text = text
Log.d(TAG, "Received: $text")

getParcelableExtra<클래스타입>을 이용하면 Parcelable 객체를 전달 받을 수 있습니다. 인텐트는 Parcel을 <클래스타입>에 명시한 클래스로 객체로 변환해 줍니다.

로그는 예상한 대로, 전달된 데이터 정보가 출력되었습니다.

com.codechacha.sample D/SubActivity: Received: MyData{myDatabase, 1, 20191009}

정리

프로세스간 데이터를 전달이 필요하다면 객체의 클래스에 Parcelable 인터페이스를 구현해야 합니다. Parcelable 객체에 구현해야 할 것은 전달하려는 변수에 대한 write/read 코드입니다. Parcelable은 추상화되어있기 때문에 나머지는 안드로이드가 알아서 해 줍니다.

이 글에서 사용한 코드는 GitHub에 있습니다.

참고