2010년 10월 12일 화요일

Parcelable을 사용한 오브젝트 전달 (Object serialization using Parcelable)



앱을 만들다 보면 인텐트를 통해 단순히 String, int, boolean 같은 기본 타입 뿐 아니고 커스텀 클래스나 오브젝트를 다른 컴포넌트에 전달해 줘야 할 경우가 많다. 그 경우 단순히 그냥 인텐트에 putExtra() 로는  넣어줄 수가 없다.
안드로이드에서는 그런 경우를 위해 자바의 Serialization 개념과 유사한 Parcelable이라는 클래스가 있다.

먼저 이런것이 왜 필요한가 살펴보겠다. 예를 들어 다음과 같은 클래스가 있다고 하자.

public class BookData {
  int _id;
  String title;
  String author;
  String publisher;
  int price;
}

도서관리 앱에서 ListView로 화면에 표시하기 위해 ArrayList<BookData>에 책들의 정보를 넣어 인텐트로 넘겨주려고 하면 BookData 클래스를 그대로 사용할수는 없다.

오브젝트를 Parcelable 클래스로 만들어 주려면 android.os.Parcelable 인터페이스를 구현해야 한다. 그러므로 아래와 같이 클래스 정의를 변경한다.

public class BookData implements Parcelable {
  int _id;
  String title;
  String author;
  String publisher;
  int price;
}

그리고 android.os.Parcelable 인터페이스에 있는 2개의 메소드를 오버라이드 해 줘야만 한다.

describeContents() - Parcel 하려는 오브젝트의 종류를 정의한다.
writeToParcel(Parcel dest, int flags) - 실제 오브젝트 serialization/flattening을 하는 메소드. 오브젝트의 각 엘리먼트를 각각 parcel해줘야 한다.

public void writeToParcel(Parcel dest, int flags) {
  dest.writeInt(_id);
  dest.writeString(title);
  dest.writeString(author);
  dest.writeString(publisher);
  dest.writeInt(price);
}

다음으로 해야 할 일은 Parcel에서 데이터를 un-marshal/de-serialize하는 단계를 추가해줘야 한다. 그러기 위해서 Parcelable.Creator 타입의 CREATOR라는 변수를 정의해야 한다. 이 변수를 정의하지 않으면 안드로이드는 다음과 같은 익셉션을 발생한다.

Parcelable protocol requires a Parcelable.Creator object called CREATOR

아래는 위의 예제인 BookData 클래스를 위한 Parcelable.Creator<BookData>의 코드이다.

public class CustomCreator implements Parcelable.Creator<BookData> {
  public BookData createFromParcel(Parcel src) {
    return new BookData(src);
  }

  public BookData[] newArray(int size) {
    return new BookData[size];
  }
}

BookData.java에 모든 parcel된 데이터를 복구하는 생성자를 정의해 줘야만 한다.

  public BookData(Parcel src) {
    _id = src.readInt();
    title = src.readString();
    author = src.readString();
    publisher = src.readString();
    price = src.readInt();
  }

주의할것은 writeToParcel() 메소드에서 기록한 순서와 동일하게 복구해야만 한다.

전체 코드는 다음과 같다.

...
public class BookData implements Parcelable {
    private String title;
    private String author;
    private String publisher;
    private String isbn;
    private String description;
    private int price;
    private String photoUrl;
   
    public BookData() {
    }
   
    public BookData(Parcel in) {
       readFromParcel(in);
    }

    public BookData(String _title, String _author, String _pub, String _isbn, String _desc, int _price, String _photoUrl) {
         this.title = _title;
         this.author = _author;
         this.publisher = _pub;
         this.isbn = _isbn;
         this.description = _desc;
         this.price = _price;
         this.photoUrl = _photoUrl;
    }

// -------------------------------------------------------------------------
// Getters & Setters section - 각 필드에 대한 get/set 메소드들
// 여기서는 생략했음

// ....
// ....
// -------------------------------------------------------------------------
  

   public void writeToParcel(Parcel dest, int flags) {
           dest.writeString(title);
           dest.writeString(author);
           dest.writeString(publisher);
           dest.writeString(isbn);
           dest.writeString(description);
           dest.writeString(photoUrl);
           dest.writeInt(price);
   }

   private void readFromParcel(Parcel in){
           title = in.readString();
           author = in.readString();
           publisher = in.readString();
           isbn = in.readString();
           description = in.readString();
           photoUrl = in.readString();
           price = in.readInt();
   }
   
   public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
        public BookData createFromParcel(Parcel in) {
             return new BookData(in);
       }

       public BookData[] newArray(int size) {
            return new BookData[size];
       }
   };
}

Parcelable 오브젝트를 인텐트로 보내는 경우는 다음과 같이 하면 된다.

BookData book = new BookData();
// 각 필드에 값을 넣어줌

Intent i = new Intent(this, ShowBook.class);
i.putExtra("bookInfo", book);
startActivity(i);

인텐트를 받을 ShowBook.java에서는 다음과 같이 Parcelable 오브젝트를 복구하면 된다.

Bundle bundle = getIntent().getExtras();
BookData book = bundle.getParcelable("bookInfo");

ArrayList<BookData>인 경우는 Intent를 만들어 보내는 쪽에서는 다음과 같이 하면 된다.

ArrayList<BookData> bookList = new ArrayList<BookData>();
...
// bookList.add() 메소드를 사용해서 bookList에 BookData 엔트리를 추가
...

Intent i = new Intent(this, BookList.class);
i.putParcelableArrayListExtra("myBooks", bookList);
startActivity(i);

BookList.java (인텐트에 의해 호출되는 액티비티)에서는 다음과 같이 오브젝트를 복구하면 된다.

ArrayList<BookData> bookList;
...
Intent i = getIntent();
bookList = i.getParcelableArrayListExtra("myBooks");




댓글 37개:

  1. 완전 이해 잘되게 정리해주셨네요.. 너무 잘보고갑니다

    답글삭제
  2. 감사합니다. 많은 도움이됬습니다. :)

    답글삭제
  3. 감사합니다
    정말 많은 도움이 되었어요%^^

    답글삭제
  4. 잘 정리 되었네요!쉽게 이해가 되었습니다.

    답글삭제
  5. 정말 잘 정리 되어 있네요

    답글삭제
  6. 근데받는쪽
    bookList
    = i.getParcelableArrayListExtra("myBooks");

    에서 bookList가 데이터가 전부 null인데
    왜그럴까요ㅠㅠㅠㅠㅠㅠㅠ

    답글삭제
  7. 굿굿 ! 정말 똑뿌러지는 설명 감사해요 > <

    답글삭제
  8. 혹시 인텐트로 넘기려는 Object 안에 또다른 사용자 정의 오브젝트의 ArrayList가 있으면 어떻게 처리를 해줘야 하나요?

    답글삭제
  9. 저렇게는 해 본 적이 없는데 아마 저 경우는 오브젝트 내의 ArrayList는 따로 보내줘야 할 거 같습니다.

    답글삭제
  10. 좋은 내용 잘 보고 갑니다. :)

    답글삭제
  11. 정말 친절한 설명 감사드립니다. 제 블로그에 출처를 명시하고 스크랩해도 괜찬을련지요.

    답글삭제
    답글
    1. 예..출처만 명시해 주시면 상관 없습니다.

      삭제
  12. 감사합니다..잘보고 갑니다..^^*..
    저도 제 블로그에 출처 명시하고 스크랩해갈게요...^^*..

    답글삭제
  13. 감사합니다 ^^ 잘보고 많이 배우고 갑니다~

    답글삭제
  14. 상세한 설명 잘보고 갑니다..*^^*...
    제 블로그에 출처남기고 스크랩 해갑니다..

    답글삭제
  15. 짱입니다.. 덕분에 고민하던 부분이 쉽게 해결되었습니다. 출처남기고 저도 퍼갈께요~~~bb

    답글삭제
  16. 혹시 ArrayList<>를 사용하지 않고 object의 배열로 예를들어서
    BookList[] 를 넘겨줄 수는 없나요?

    답글삭제
  17. 아하 배열을 ArrayList로 대체하면 되네요......
    좋은 정보 정말 감사합니다 ^^

    답글삭제
  18. 대박 글 너무 정리 잘하시네요
    이해가 쏙쏙되네요
    감사합니다

    답글삭제
  19. 객체가 하나가아니라 두개를 넘기려고하니 런타임 에러가나는데 혹시 이유를 아시는지요

    답글삭제
  20. 내용 설명이 정말 잘 되어있어서 알아보기 쉬웠습니다.
    죄송하지만 너무 정리가 잘 되어있어서 그런데 스크랩 해가여도 괜찮다면 출처를 표시하고 가져가겠습니다.
    문제시 삭제조치하겠습니다.
    http://hongry.tistory.com/entry/Android-Parcelable%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%EC%A0%84%EB%8B%AC

    답글삭제
    답글
    1. 출처를 표시해 주시면 상관없습니다.

      삭제
  21. 완전 잘보고 갑니다. 퍼갑니다~

    답글삭제
  22. 만약 사용자 정의 object가 잇으면 어떻게 넘기나요?

    public void writeToParcel(Parcel dest, int flags) {
    ...
    dest.writeValue(Object value) <-- 인가요?

    ...
    }

    답글삭제
  23. 작성자가 댓글을 삭제했습니다.

    답글삭제
  24. arraylist 크기를 고정할 방법을 없나요?

    또, 만약 class로 이루어진 배열을 intent하는 방법은 없는가요?

    지금 크기가 고정된 객체들을 intent하여 사용하려는데, 고민이 되네요.

    답글삭제
  25. 매우매우 도움 되었어요. 감사합니다.

    답글삭제
  26. 좋은 글 널리 알려야겠네요.
    http://blog.daum.net/andro_java/1151
    감사합니다.

    답글삭제
  27. 정말 많은 도움이 되었습니다 ~~. 어렵게 배운 지식 나눠주셔서 감사드립니다^^

    답글삭제
  28. 질문 있습니다.
    순서대로 기록했다가
    순서대로 읽어야되는데

    구체적으로
    기록을 타입별로 순서대로 기록하고 타입별로 읽어야하는건가요?
    그러니까
    writeInt(one)
    writeString(oneString)

    답글삭제
  29. 굿굿 완전 도움됬습니다.

    답글삭제
  30. 정말 좋은글이네요. 한번에 해결했습니다.

    답글삭제
  31. 감사합니다. 잘 이해했습니다.

    답글삭제
  32. 좋은 정보 정말 감사드립니다.
    그런데 만약에 비트맵이 아이템 인자에 포함된다면, 어떻게 처리해야 할까요?

    답글삭제