通过意图传递枚举或对象(最佳解决方案)

256

我有一个活动,启动时需要访问两个不同的ArrayList。这两个列表是我自己创建的不同对象。

基本上,我需要一种从Intent传递这些对象到活动的方法。我可以使用addExtras(),但这需要一个可Parceable兼容的类。我可以将要传递的类设为可序列化的,但据我所知,这会减缓程序的运行速度。

我有哪些选择?

我能传递一个枚举吗?

另外,有没有办法从Intent传递参数给活动构造函数?


也许我漏掉了什么,但是枚举类型和ArrayList有什么关系呢? - Martin Konecny
15个回答

648
这是一个老问题,但是每个人都没有提到枚举类型其实是可序列化的,因此完全可以作为额外数据添加到Intent中。像这样:

public enum AwesomeEnum {
  SOMETHING, OTHER;
}

intent.putExtra("AwesomeEnum", AwesomeEnum.SOMETHING);

AwesomeEnum result = (AwesomeEnum) intent.getSerializableExtra("AwesomeEnum");

使用静态或应用程序级变量的建议真的很糟糕。这会将您的活动与状态管理系统耦合在一起,难以维护、调试和处理问题。

替代方案:

tedzyc 指出了一个好的观点,即 Oderik 提供的解决方案会给您带来一个错误。然而,提供的替代方案使用起来有点麻烦(即使使用泛型)。

如果您真的担心将枚举添加到 Intent 的性能问题,我建议使用以下替代方案:

选项 1:

public enum AwesomeEnum {
  SOMETHING, OTHER;
  private static final String name = AwesomeEnum.class.getName();
  public void attachTo(Intent intent) {
    intent.putExtra(name, ordinal());
  }
  public static AwesomeEnum detachFrom(Intent intent) {
    if(!intent.hasExtra(name)) throw new IllegalStateException();
    return values()[intent.getIntExtra(name, -1)];
  }
}

使用方法:

// Sender usage
AwesomeEnum.SOMETHING.attachTo(intent);
// Receiver usage
AwesomeEnum result = AwesomeEnum.detachFrom(intent);

选项2:(通用、可重复使用并与枚举解耦)

public final class EnumUtil {
    public static class Serializer<T extends Enum<T>> extends Deserializer<T> {
        private T victim;
        @SuppressWarnings("unchecked") 
        public Serializer(T victim) {
            super((Class<T>) victim.getClass());
            this.victim = victim;
        }
        public void to(Intent intent) {
            intent.putExtra(name, victim.ordinal());
        }
    }
    public static class Deserializer<T extends Enum<T>> {
        protected Class<T> victimType;
        protected String name;
        public Deserializer(Class<T> victimType) {
            this.victimType = victimType;
            this.name = victimType.getName();
        }
        public T from(Intent intent) {
            if (!intent.hasExtra(name)) throw new IllegalStateException();
            return victimType.getEnumConstants()[intent.getIntExtra(name, -1)];
        }
    }
    public static <T extends Enum<T>> Deserializer<T> deserialize(Class<T> victim) {
        return new Deserializer<T>(victim);
    }
    public static <T extends Enum<T>> Serializer<T> serialize(T victim) {
        return new Serializer<T>(victim);
    }
}

使用方法:

// Sender usage
EnumUtil.serialize(AwesomeEnum.Something).to(intent);
// Receiver usage
AwesomeEnum result = 
EnumUtil.deserialize(AwesomeEnum.class).from(intent);

选项三(使用 Kotlin):

虽然已经有一段时间了,但现在我们有了 Kotlin,我认为应该为这种新编程范式添加另一种选择。在这里,我们可以利用扩展函数和 reified 类型(在编译时保留类型)。

inline fun <reified T : Enum<T>> Intent.putExtra(victim: T): Intent =
    putExtra(T::class.java.name, victim.ordinal)

inline fun <reified T: Enum<T>> Intent.getEnumExtra(): T? =
    getIntExtra(T::class.java.name, -1)
        .takeUnless { it == -1 }
        ?.let { T::class.java.enumConstants[it] }

这种方式有几个好处。
  • 我们不需要中间对象的“开销”来进行序列化,因为它是在原地完成的,感谢inline,它将用函数内部的代码替换调用。
  • 这些函数更加熟悉,因为它们类似于SDK中的函数。
  • IDE将自动完成这些函数,这意味着不需要对实用程序类有先前的了解。
其中一个缺点是,如果我们更改Emums的顺序,则任何旧引用都将无法工作。这可能是一些像Intents内的pending intents的问题,因为它们可能在更新后幸存。然而,在其余时间里,应该没问题。
重要的是要注意,其他解决方案,如使用名称而不是位置,如果我们重命名任何值,也会失败。尽管在这些情况下,我们会得到异常,而不是错误的Enum值。
用法:
// Sender usage
intent.putExtra(AwesomeEnum.SOMETHING)
// Receiver usage
val result = intent.getEnumExtra<AwesomeEnum>()

15
感谢您指出将它们应用于整个应用程序是一个“真正糟糕的主意”,给您点赞。 - bugfixr
3
我曾参与过一个项目,其中我不想处理对象的序列化和捆绑(有许多具有大量变量的对象),使用静态全局变量是可以的...直到我的一个队友加入项目。尝试协调使用这些全局变量的代价让我感到很烦躁。于是我决定“算了,我写个代码生成器来生成一些可 Parcelable 的对象”。这样做后 bug 的数量显著减少了。 - Joe Plante
3
@Coeffect 是的,这是一个可以理解的建议,但在大多数情况下,除非你正在解析数千个枚举(它们本质上应该只有很少量,因为它们用于处理状态),否则这可能被视为过早的优化。在 Nexus 4 上,您可以获得 1 毫秒的改进(http://www.developerphil.com/parcelable-vs-serializable/),不确定这是否值得额外的工作,但另一方面,您有我提出的其他选择 ;) - pablisco
1
@rgv 在 Kotlin 的内部,它会将 enum class 类型交叉编译为普通的 Java enum。我猜更简单的解决方法是让 enum class 实现 Serializable 接口: enum class AwesomeEnum : Serializable { A, B, C } 虽然不是最理想的解决方案,但应该可以工作。 - pablisco
1
@Pierre 像所有好的答案一样,“它取决于情况”。没有必要扩展Serializable。初始答案是有效的。那些额外选项是为了防止出现瓶颈的情况,比如您要反序列化数百万条记录(希望不要这样做)。使用您认为合适的方法... - pablisco
显示剩余7条评论

114

你可以让枚举实现Parcelable,这对于枚举来说相当容易:

public enum MyEnum implements Parcelable {
    VALUE;


    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(final Parcel dest, final int flags) {
        dest.writeInt(ordinal());
    }

    public static final Creator<MyEnum> CREATOR = new Creator<MyEnum>() {
        @Override
        public MyEnum createFromParcel(final Parcel source) {
            return MyEnum.values()[source.readInt()];
        }

        @Override
        public MyEnum[] newArray(final int size) {
            return new MyEnum[size];
        }
    };
}

你可以使用 Intent.putExtra(String, Parcelable)。

更新:请注意 wreckgar 的评论,enum.values() 每次调用都会分配一个新数组。

更新:Android Studio 提供了一个名为 ParcelableEnum 的实时模板来实现此解决方案。(在 Windows 上,使用 Ctrl+J


3
可以使用枚举的toString()和valueOf()方法代替序数。 - Natix
2
使用ordinal()可能会出现问题,当开发人员插入新的枚举成员时。当然,重命名枚举成员也会破坏name()。但是,开发人员更有可能插入新成员,而不是重命名,因为重命名需要重新设计整个项目。 - Cheok Yan Cheng
2
我不同意额外的(或重新排序的)枚举值比重命名的更有可能。使用像IntelliJ IDEA这样复杂的IDE进行重构并不是什么大问题。 但你的观点仍然很好:你必须确保序列化在任何共存实现中都是一致的。对于任何类型的序列化都是如此。 我认为在大多数情况下,parcelable对象只在一个应用程序内传递,其中只存在一个实现,因此这不应该是一个问题。 - Oderik
2
values() 每次调用都会创建一个新的数组,因此最好将其缓存到例如私有静态数组中。 - wreckgar23
2
通常情况下(例如数据库存储),ordinal() 不安全的事实对于 Android parcels 并不相关。Parcels 不是用于长期(持久性)存储的。它们随着应用程序的关闭而消失。因此,当您添加/重命名枚举时,将会获得新的 parcels。 - noamtm
显示剩余4条评论

28

你可以将枚举类型作为字符串传递。

public enum CountType {
    ONE,
    TWO,
    THREE
}

private CountType count;
count = ONE;

String countString = count.name();

CountType countToo = CountType.valueOf(countString);

如果支持字符串,则应该可以轻松地传递枚举的值。


3
它们中最简单的实现。 - Avi Cohen

22

要通过Intent传递枚举类型,您可以将其转换为整数。

例如:

public enum Num{A ,B}

发送(从枚举类型到整数):

Num send = Num.A;
intent.putExtra("TEST", send.ordinal());

接收(将整数转换为枚举):

Num rev;
int temp = intent.getIntExtra("TEST", -1);
if(temp >= 0 && temp < Num.values().length)
    rev = Num.values()[temp];

最好的问候。
:)


9
或者您可以使用 Num.A.name() 将其作为字符串发送(以便可读性),然后使用 Num.ValueOf(intent.getStringExtra("TEST")) 获取它。 - Benoit Jadinon
1
我认为Benoit的方式更安全,因为在实践中不倾向于使用temp.ordinal(),因为ordinal()值可能会更改。请参阅此帖子:https://dev59.com/j3E85IYBdhLWcg3wUByz - Shnkc

15

如果你确实需要这样做,你可以将枚举类型序列化为字符串,使用name()valueOf(String)方法,如下所示:

 class Example implements Parcelable { 
   public enum Foo { BAR, BAZ }

   public Foo fooValue;

   public void writeToParcel(Parcel dest, int flags) {
      parcel.writeString(fooValue == null ? null : fooValue.name());
   }

   public static final Creator<Example> CREATOR = new Creator<Example>() {
     public Example createFromParcel(Parcel source) {        
       Example e = new Example();
       String s = source.readString(); 
       if (s != null) e.fooValue = Foo.valueOf(s);
       return e;
     }
   }
 }

如果您的枚举具有可变状态(实际上不应该这样),那么这显然无法正常工作。


4

可能可以使您的枚举实现Serializable,然后通过Intent进行传递,因为有一种方法可以将其作为可序列化对象传递。建议使用int而不是enum是错误的。枚举用于使您的代码更易于阅读和维护。不能使用Enums会是一个倒退到黑暗时代的巨大步骤。


3
任何枚举类型默认都继承自枚举超类Enum,该超类已经实现了Serializable接口。 - Arcao

3

这里大部分使用可序列化Parcelable概念的回答都是Java代码,而在Kotlin中更容易实现。

只需将您的枚举类注释为@Parcelize并实现Parcelable接口即可。

@Parcelize
enum class ViewTypes : Parcelable {
TITLE, PRICES, COLORS, SIZES
}

2
您可以使用枚举构造函数来为枚举类型拥有基本数据类型。
public enum DaysOfWeek {
    MONDAY(1),
    TUESDAY(2),
    WEDNESDAY(3),
    THURSDAY(4),
    FRIDAY(5),
    SATURDAY(6),
    SUNDAY(7);

    private int value;
    private DaysOfWeek(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    private static final SparseArray<DaysOfWeek> map = new SparseArray<DaysOfWeek>();

    static
    {
         for (DaysOfWeek daysOfWeek : DaysOfWeek.values())
              map.put(daysOfWeek.value, daysOfWeek);
    }

    public static DaysOfWeek from(int value) {
        return map.get(value);
    }
}

您可以使用int作为extras传递,然后使用其值从枚举中提取它。


2

关于Oderik的帖子:

你可以让你的枚举实现Parcelable接口,这对枚举来说非常容易:

public enum MyEnum implements Parcelable { ... } 然后你就可以使用Intent.putExtra(String, Parcelable)了。

如果你定义了一个MyEnum变量myEnum,然后执行intent.putExtra("Parcelable1", myEnum),你会得到一个"The method putExtra(String, Parcelable) is ambiguous for the type Intent"错误消息。因为Intent类中也有一个putExtra(String, Parcelable)方法,并且原始的'Enum'类型本身实现了Serializable接口,所以编译器不知道选择哪个方法(intent.putExtra(String, Parcelable/or Serializable))。

建议从MyEnum中删除Parcelable接口,并将核心代码移动到包装类的Parcelable实现中,像这样(Father2是一个Parcelable并包含一个枚举字段):

public class Father2 implements Parcelable {

AnotherEnum mAnotherEnum;
int mField;

public Father2(AnotherEnum myEnum, int field) {
    mAnotherEnum = myEnum;
    mField = field;
}

private Father2(Parcel in) {
    mField = in.readInt();
    mAnotherEnum = AnotherEnum.values()[in.readInt()];
}

public static final Parcelable.Creator<Father2> CREATOR = new Parcelable.Creator<Father2>() {

    public Father2 createFromParcel(Parcel in) {
        return new Father2(in);
    }

    @Override
    public Father2[] newArray(int size) {
        return new Father2[size];
    }

};

@Override
public int describeContents() {
    return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(mField);
    dest.writeInt(mAnotherEnum.ordinal());
}

}

那么我们可以进行以下操作:
AnotherEnum anotherEnum = AnotherEnum.Z;
intent.putExtra("Serializable2", AnotherEnum.X);   
intent.putExtra("Parcelable2", new Father2(AnotherEnum.X, 7));

5
你可以通过将参数强制转换来明确选择正确的签名,例如 intent.putExtra("myEnum", (Parcelable) enumValue); - Oderik
使用序数非常完美! - slott
这是一种非常复杂的表达方式,其实就是 bundle.putExtra("key", AnotherEnum.X.ordinal()) - TWiStErRob

1

我喜欢简单。

  • Fred 活动有两种模式——HAPPYSAD
  • 创建一个静态的 IntentFactory 来为你创建 Intent。将所需的 Mode 传递给它。
  • IntentFactory 使用 Mode 类的名称作为额外信息的名称。
  • IntentFactory 使用 name()Mode 转换为 String
  • 进入 onCreate 时,使用此信息将其转换回 Mode
  • 您也可以使用 ordinal()Mode.values()。我喜欢使用字符串,因为我可以在调试器中看到它们。

    public class Fred extends Activity {
    
        public static enum Mode {
            HAPPY,
            SAD,
            ;
        }
    
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.betting);
            Intent intent = getIntent();
            Mode mode = Mode.valueOf(getIntent().getStringExtra(Mode.class.getName()));
            Toast.makeText(this, "mode="+mode.toString(), Toast.LENGTH_LONG).show();
        }
    
        public static Intent IntentFactory(Context context, Mode mode){
            Intent intent = new Intent();
            intent.setClass(context,Fred.class);
            intent.putExtra(Mode.class.getName(),mode.name());
    
            return intent;
        }
    }
    

好奇心..谁调用了IntentFactory?你能详细说明一下不同的活动如何调用Fred以及Fred如何确保模式被传递吗? - erik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接