I'm using a custom parcelable object called GameSettings to pass a number of settings between Activites within an Android app (developed using MonoDroid). The settings are stored as properties on this GameSettings class, and up until now they've all been simple integers which I've been able to parcel just fine using Parcel.WriteInt() and Parcel.ReadInt().
I've just added a new property to GameSettings called CelebrityNames which is of type List<string>, and I'm trying to pass this in the same way but when ReadStringList() is called the property gets populated with an empty list (despite a non-empty list being written to the parcel prior to this using WriteStringList()). The parcel is being passed from NameEntryActivity to GameRoundActivity.
GameSettings.cs
using System;
using System.Collections.Generic;
using Android.OS;
using Java.Interop;
using Object = Java.Lang.Object;
namespace Celebrities
{
public class GameSettings : Object, IParcelable
{
private static readonly GenericParcelableCreator<GameSettings> _creator
= new GenericParcelableCreator<GameSettings>((parcel) => new GameSettings(parcel));
[ExportField("CREATOR")]
public static GenericParcelableCreator<GameSettings> InitializeCreator()
{
return _creator;
}
public int NumberOfPlayers { get; set; }
public int NumberOfTeams { get; set; }
public int CelebritiesPerPlayer { get; set; }
public int SecondsPerRound { get; set; }
private List<string> _celebrityNames;
public List<string> CelebrityNames {
get
{
_celebrityNames.Shuffle ();
return _celebrityNames;
}
set
{
_celebrityNames = value;
}
}
public GameSettings (int players, int teams, int celebrities, int secondsPerRound)
{
NumberOfPlayers = players;
NumberOfTeams = teams;
CelebritiesPerPlayer = celebrities;
SecondsPerRound = secondsPerRound;
}
private GameSettings(Parcel parcel) : this(parcel.ReadInt (), parcel.ReadInt (), parcel.ReadInt (), parcel.ReadInt ())
{
if (_celebrityNames == null)
{
_celebrityNames = new List<string>();
}
parcel.ReadStringList (_celebrityNames);
}
public void WriteToParcel(Parcel dest, ParcelableWriteFlags flags)
{
dest.WriteInt (NumberOfPlayers);
dest.WriteInt (NumberOfTeams);
dest.WriteInt (CelebritiesPerPlayer);
dest.WriteInt (SecondsPerRound);
dest.WriteStringList (_celebrityNames);
}
public int DescribeContents()
{
return 0;
}
}
}
Note: I'm using the backing variable _celebrityNames for parcelling as I have a custom getter that shuffles the list, which isn't necessary at this point. The problem is the same whether using the property or the variable.
GenericParcelableCreator.cs
using System;
using Android.OS;
using Object = Java.Lang.Object;
namespace Celebrities
{
public sealed class GenericParcelableCreator<T> : Object, IParcelableCreator
where T : Object, new()
{
private readonly Func<Parcel, T> _createFunc;
public GenericParcelableCreator(Func<Parcel, T> createFromParcelFunc)
{
_createFunc = createFromParcelFunc;
}
public Object CreateFromParcel(Parcel source)
{
return _createFunc(source);
}
public Object[] NewArray(int size)
{
return new T[size];
}
}
}
I'm including the relevant code from the Activity classes below (these are not the complete files for brevity, please ask if you think it would be helpful to see the rest too).
NameEntryActivity.cs (where I'm passing the parcel from)
public class NameEntryActivity : Activity
{
...
private GameSettings _gameSettings;
private List<string> _celebrityNames;
protected override void OnCreate (Bundle savedInstanceState)
{
...
_gameSettings = (Intent.Extras.GetParcelable ("GameSettings") as GameSettings);
_celebrityNames = new List<string> ();
...
}
...
private void MoveToNextCelebrity()
{
...
_gameSettings.CelebrityNames = _celebrityNames;
var intent = new Intent (this, typeof(GameRoundActivity));
intent.PutExtra("GameSettings", _gameSettings);
StartActivity (intent);
...
}
}
GameRoundActivity.cs (where I'm passing the parcel to)
public class GameRoundActivity : Activity
{
private GameSettings _gameSettings;
protected override void OnCreate (Bundle savedInstanceState)
{
base.OnCreate (savedInstanceState);
SetContentView (Resource.Layout.GameRound);
_gameSettings = (Intent.Extras.GetParcelable ("GameSettings") as GameSettings);
}
}
This is my first time developing an Android app, so it may well be that I've made a mistake somewhere in implementing the parcelling framework or have misunderstood it. Equally I've been looking at this code for so long that maybe I'm just missing a more general silly mistake :)
Thanks in advance!
I switched to using a string array instead of a list and it's now working using Parcel.WriteStringArray() and Parcel.CreateStringArray().
Obviously this wouldn't be applicable in every situation though so I'm still interested in why this was happening!
Related
In my Activity, I have a Training object member initialized during onCreate(). All the members of this object are set.
private Training mTraining; is a class member
public class Training extends BaseModel {
...
#SerializedName("state")
public TrainingState state;
....
public TrainingPreview() {
}
This object is got from server (JSON), and I had a converter on this state to ensure this enum can't be null (I use GSON engine):
public class TrainingStateConverter extends EnumConverter<TrainingState> {
public static final Type TYPE = new TypeToken<TrainingState>() {}.getType();
#Override
protected TrainingState deserialize(String value) {
return TrainingState.fromString(value);
}
#Override
protected TrainingState getUnknownValue() {
return TrainingState.UNKNOWN;
}
}
During the setup, I've created the exercise list with the listener to show a specific exercise:
private void refreshExercisesList() {
final Runnable showTrainingParts = new Runnable() {
public void run() {
int nbItems = mCardExercises.setExercises(mTraining.training, mTraining.state,
new FlatCardTrainingProfilePartExercisesView.OnClickExerciseListener() {
#Override
public void showPart(String trainingPartId, int index) {
onClickOnExercisesList(trainingPartId, index);
}
});
}
};
}
...
}
My onClickOnExercisesList() method:
private void onClickOnExercisesList(String trainingPartId, int index) {
...
switch (mTraining.state) {
...
This Activity code works perfectly since couple of months, but yesterday there was a NullPointerException on switch (mTraining.state) :
int com.xxx.model.training.TrainingState.ordinal()' on a null object reference
com.xxx.ui.training.TrainingActivity.onClickOnExercisesList
How is possible guys?
Thank you very much for your help!
This would occur if state did not appear in the JSON.
The TypeConverter is only used if there is a value in the JSON to convert. If the value isn't present, then there's nothing to convert, so the value is whatever the default is, which is null, because you didn't set it:
#SerializedName("state")
public TrainingState state;
To fix the issue, initialize the variable to a default value:
#SerializedName("state")
public TrainingState state = TrainingState.UNKNOWN;
I have an app set up using Mortar/Flow and Dagger 2. It seems to work except for when I switch between two views of the same class. The new view ends up with the previous view's presenter.
For example, I have a ConversationScreen that takes a conversationId as a constructor argument. The first time I create a ConversationScreen and add it to Flow it creates the ConversationView which injects itself with a Presenter which is created with the conversationId that was passed to the screen. If I then create a new ConversationScreen with a different conversationId, when the ConversationView asks for a Presenter, Dagger returns the old Presenter, because the scope has not yet closed on the previous ConversationScreen.
Is there a way for me to manually close the scope of the previous screen before I set up the new one? Or have I just set up the scoping wrong to begin with?
ConversationView
public class ConversationView extends RelativeLayout {
#Inject
ConversationScreen.Presenter presenter;
public ConversationView(Context context, AttributeSet attrs) {
super(context, attrs);
DaggerService.<ConversationScreen.Component>getDaggerComponent(context).inject(this);
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
presenter.takeView(this);
}
#Override
protected void onDetachedFromWindow() {
presenter.dropView(this);
super.onDetachedFromWindow();
}
}
ConversationScreen
#Layout(R.layout.screen_conversation)
public class ConversationScreen extends Paths.ConversationPath implements ScreenComponentFactory<SomeComponent> {
public ConversationScreen(String conversationId) {
super(conversationId);
}
#Override
public String getTitle() {
title = Conversation.get(conversationId).getTitle();
}
#Override
public Object createComponent(SomeComponent parent) {
return DaggerConversationScreen_Component.builder()
.someComponent(parent)
.conversationModule(new ConversationModule())
.build();
}
#dagger.Component(
dependencies = SomeComponent.class,
modules = ConversationModule.class
)
#DaggerScope(Component.class)
public interface Component {
void inject(ConversationView conversationView);
}
#DaggerScope(Component.class)
#dagger.Module
public class ConversationModule {
#Provides
#DaggerScope(Component.class)
Presenter providePresenter() {
return new Presenter(conversationId);
}
}
#DaggerScope(Component.class)
static public class Presenter extends BasePresenter<ConversationView> {
private String conversationId;
#Inject
Presenter(String conversationId) {
this.conversationId = conversationId;
}
#Override
protected void onLoad(Bundle savedInstanceState) {
super.onLoad(savedInstanceState);
bindData();
}
void bindData() {
// Show the messages in the conversation
}
}
}
If you use the default ScreenScoper and PathContextFactory classes from Mortar/Flow example project, you will see that the name of the new scope to create is the name of the Screen class.
Because you want to navigate from one instance of ConversationScreen to another instance of ConversationScreen, the name of the new scope will be equal to the name of previous scope. Thus, you won't create a new Mortar scope but just reuse the previous one, which means reusing the same presenter.
What you need is to change the naming policy of the new scope. Rather than using only the name of the new screen class, add something else.
Easiest fix is to use the instance identifier: myScreen.toString().
Another better fix is to have a tracking of the screen/scope names.
Following example extracted from https://github.com/lukaspili/Mortar-architect
class EntryCounter {
private final SimpleArrayMap<Class, Integer> ids = new SimpleArrayMap<>();
int get(History.Entry entry) {
Class cls = entry.path.getClass();
return ids.containsKey(cls) ? ids.get(cls) : 0;
}
void increment(History.Entry entry) {
update(entry, true);
}
void decrement(History.Entry entry) {
update(entry, false);
}
private void update(History.Entry entry, boolean increment) {
Class cls = entry.path.getClass();
int id = ids.containsKey(cls) ? ids.get(cls) : 0;
ids.put(cls, id + (increment ? 1 : -1));
}
}
And then use this counter when creating new scope:
private ScopedEntry buildScopedEntry(History.Entry entry) {
String scopeName = String.format("ARCHITECT_SCOPE_%s_%d", entry.path.getClass().getName(), entryCounter.get(entry));
return new ScopedEntry(entry, MortarFactory.createScope(navigator.getScope(), entry.path, scopeName));
}
And in some other place, i'm incrementing/decrementing the counter if new scope is pushed or scope is detroyed.
The scope in ScreenScoper is based on a string, which if you create the same path, it will use the same name as it bases it on the class name of your path.
I solved this by removing some noise from the ScreenScoper, considering I'm not using #ModuleFactory in my Dagger2-driven project anyways.
public abstract class BasePath
extends Path {
public abstract int getLayout();
public abstract Object createComponent();
public abstract String getScopeName();
}
public class ScreenScoper {
public MortarScope getScreenScope(Context context, String name, Object screen) {
MortarScope parentScope = MortarScope.getScope(context);
return getScreenScope(parentScope, name, screen);
}
/**
* Finds or creates the scope for the given screen.
*/
public MortarScope getScreenScope(MortarScope parentScope, final String name, final Object screen) {
MortarScope childScope = parentScope.findChild(name);
if (childScope == null) {
BasePath basePath = (BasePath) screen;
childScope = parentScope.buildChild()
.withService(DaggerService.TAG, basePath.createComponent())
.build(name);
}
return childScope;
}
}
I just noticed the Property class http://developer.android.com/reference/android/util/Property.html . I can see some explanation of it here http://developer.android.com/about/versions/android-4.0.html#api but dont really understand the use cases of it. Would be great if someone can point me to some code snippets where I can understand this more.
Property is a wrapper to a Reflection.
For example, you have an object
public class A {
private int fieldOfA;
private int fieldTwo;
private int fieldThree;
public void setFieldOfA(int a) {
fieldOfA = a;
}
public int getFieldOfA() {
return fieldOfA;
}
public void setFieldTwo(int a) {
fieldTwo = a;
}
public int getFieldTwo() {
return fieldTwo;
}
public void setFieldThree(int a) {
fieldThree = a;
}
public int getFieldThree() {
return fieldThree;
}
}
If you need to update phew fields, you have to know all their names in the update method without Properties
private void updateValues(final A a, final int value) {
a.setFieldOfA(value);
a.setFieldTwo(value);
a.setFieldThree(value);
}
With Properties you can update only the properties.
Property aProperty = Property.of(A.class, int.class, "fieldOfA");
Property bProperty = Property.of(A.class, int.class, "fieldTwo");
Property cProperty = Property.of(A.class, int.class, "fieldThree");
Collection<Property<A, Integer>> properties = new HashSet<>();
properties.add(aProperty);
properties.add(bProperty);
properties.add(cProperty);
updateValues(a, 10, properties);
And the method would be
private void updateValues(final A a, final int value, final Collection<Property<A, Integer>> properties) {
for (final Property<A, Integer> property : properties) {
property.set(a, value);
}
}
As laalto memtioned, property animations use a similar mechanism.
One example would be property animations. The Property class provides an abstraction for attributes that can be changed over time to perform an animation.
I have searched a few topics but not found a solution to my problem.
public class Series implements Parcelable {
private String name;
private int numOfSeason;
private int numOfEpisode;
/** Constructors and Getters/Setters have been removed to make reading easier **/
public Series(Parcel in) {
String[] data = new String[3];
in.readStringArray(data);
this.name = data[0];
this.numOfSeason = Integer.parseInt(data[1]);
this.numOfEpisode = Integer.parseInt(data[2]);
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeStringArray(new String[] { this.name,
String.valueOf(this.numOfSeason),
String.valueOf(this.numOfEpisode) });
}
private void readFromParcel(Parcel in) {
name = in.readString();
numOfSeason = in.readInt();
numOfEpisode = in.readInt();
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
#Override
public Series createFromParcel(Parcel in) {
return new Series(in);
}
#Override
public Series[] newArray(int size) {
return new Series[size];
}
};
}
In my MainActivity I have an ArrayList. To make the list dynamically editeable I need to pass it to another activity where I can edit it.
ArrayList<Series> listOfSeries = new ArrayList<Series>();
public void openAddActivity() {
Intent intent = new Intent(this, AddActivity.class);
intent.putParcelableArrayListExtra(
"com.example.episodetracker.listofseries",
(ArrayList<? extends Parcelable>) listOfSeries);
startActivity(intent);
}
I need to cast the list, otherwise Eclipse gives me the following Error message.
The method putParcelableArrayListExtra(String, ArrayList) in the type Intent is not applicable for the arguments (String, List)
Is this the correct way to do it?
ArrayList<Series> list = savedInstanceState
.getParcelableArrayList("com.example.episodetracker.listofseries");
This is the way I try to read the data in another activity.
It's crashing on the line above. namely the getParcelableArrayList part.
The problem is in writing out to the parcel and reading in from the parcel ...
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(numOfSeason);
dest.writeInt(numOfEpisode);
}
private void readFromParcel(Parcel in) {
name = in.readString();
numOfSeason = in.readInt();
numOfEpisode = in.readInt();
}
What you write out has to match what you read in...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent i = new Intent(this,SecondActivity.class);
ArrayList<testparcel> testing = new ArrayList<testparcel>();
i.putParcelableArrayListExtra("extraextra", testing);
startActivity(i);
}
/**********************************************/
public class SecondActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayList<testparcel> testing = this.getIntent().getParcelableArrayListExtra("extraextra");
}
}
The above code is having onCreate() from two different activities. The first one launches the second one; and it works fine I was able to pull the parcelable without issue.
You should use the putParcelableArrayListExtra() method on the Intent class.
I've used putParcelableArrayList(<? extends Parcelable>) from a Bundle Object. Not directly from an Intent Object.(I don't really know what's the difference). but i use to use in this way:
ArrayList<ParcelableRow> resultSet = new ArrayList<ParcelableRow>();
resultSet = loadData();
Bundle data = new Bundle();
data.putParcelableArrayList("search.resultSet", resultSet);
yourIntent.putExtra("result.content", data);
startActivity(yourIntent);
Later on your new activity you can populate the data recently inserted on the Bundle object like this:
Bundle data = this.getIntent().getBundleExtra("result.content");
ArrayList<ParcelableRow> result = data.getParcelableArrayList("search.resultset");
Just remember that your ArrayList<> must contain only parcelable objects. and just to make sure that your have passed the data you may check if the data received is null or not, just to avoid issues.
Maybe this helps someone.. else my problem was that I used write and readValue but it should match type like writeInt, readInt writeString, readString and so forth
I am doing it in this way:
var intent = Intent(this#McqActivity,ResultActivity::class.java)
intent.putParcelableArrayListExtra("keyResults", ArrayList(resultList))
// resultList is of type mutableListOf<ResultBO>()
startActivity(intent)
And for making the class Parcelable . I simply do two operation first I used #Parcelize Annotation above my data class and secondly I inherit it with Parcelable like below..
import kotlinx.android.parcel.Parcelize
import android.os.Parcelable
#Parcelize // Include Annotion
data class ResultBO(val questionBO: QuestionBO) : Parcelable {
constructor() : this(QuestionBO())
}
And at receiving end
if (intent != null) {
var results = intent.getParcelableArrayListExtra<Parcelable>("keyResults")
}
You can pass Parcelable ArrayList
Sender Activity ->
startActivity(Intent(this, SenderActivity::class.java).apply { putExtra("getList",list)})
Receiver Activity ->
private lateinit var list: ArrayList<List>
list = this.intent.extras?.getParcelableArrayList("getList")!!
And you will get all Arraylist in list.
I am having a class EmployeeInfo as the following:
public class EmployeeInfo {
private int id; // Employee ID
private String name; // Employee Name
private int age;// Employee Age
public int getEmployeeID() {
return id;
}
public void setEmployeeID(int id) {
this.id = id;
}
public String getEmployeeName() {
return name;
}
public void setEmployeeName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age= age;
}
}
ArrayList<EmployeeInfo> employeeInfo object contains the emplyoyee info data for multiple employees.
I want to transfer the data( ArrayList employeeInfo ) from Activity1 to Activity2.
Is using Parcelable the only way to transfer the data from Activity1 to Activity2?
If not , what are the alternatives.
If yes ,kindly provide the prototype code of Parcelable along with the sample code on how to transfer the object data from Activity1 to Activity2.
Here is my implementation of Parceleble:
public class ProfileData implements Parcelable {
private int gender;
private String name;
private String birthDate;
public ProfileData(Parcel source) {
gender = source.readInt();
name = source.readString();
birthDate = source.readString();
}
public ProfileData(int dataGender, String dataName, String dataBDate) {
gender = dataGender;
name = dataName;
birthDate = dataBDate;
}
// Getters and Setters are here
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(gender);
out.writeString(name);
out.writeString(birthDate);
}
public static final Parcelable.Creator<ProfileData> CREATOR
= new Parcelable.Creator<ProfileData>() {
public ProfileData createFromParcel(Parcel in) {
return new ProfileData(in);
}
public ProfileData[] newArray(int size) {
return new ProfileData[size];
}
};
}
and how I transfer data:
Intent parcelIntent = new Intent().setClass(ActivityA.this, ActivityB.class);
ProfileData data = new ProfileData(profile.gender, profile.getFullName(), profile.birthDate);
parcelIntent.putExtra("profile_details", data);
startActivity(parcelIntent);
and take data:
Bundle data = getIntent().getExtras();
ProfileData profile = data.getParcelable("profile_details");
You can simply let your EmployeeInfo class implement Serializable. Or you can send data like this
intent.putExtra("id", employInfo.getEmployeeID());
intent.putExtra("name", employInfo.getEmployeeName());
intent.putExtra("age", employInfo.getAge());
If you need to transfer a list of your custom classes, i'd use the first approach. So you would be able to put entire list as Serializable.
However they said that everyone should use Parcelable instead because it's "way faster". Tbh, I'd never used it, because it needs more effort and I doubt somebody can realize the difference in speed in a regular application w/o a load of data sending via intent
Good question. Looking at the docs and doing armchair coding:
It may be possible to pass an object between Activities by calling putExtras(Bundle) and myBundle.putSerializable. The object and the entire object tree would need to implement serializable.
JAL
EDIT: The answer is yes:
It is possible to pass an immutable object between Activities by calling putExtras(Bundle) and myBundle.putSerializable. The object and the entire object tree would need to implement serializable. This is a basic tenet of Object Oriented Programming, passing of stateful messages.
First we create the immutable object by declaring a new class:
package jalcomputing.confusetext;
import java.io.Serializable;
/*
* Immutable messaging object to pass state from Activity Main to Activity ManageKeys
* No error checking
*/
public final class MainManageKeysMessage implements Serializable {
private static final long serialVersionUID = 1L;
public final int lengthPassword;
public final long timeExpire;
public final boolean isValidKey;
public final int timeoutType;
public MainManageKeysMessage(int lengthPassword, long timeExpire, boolean isValidKey, int timeoutType){
this.lengthPassword= lengthPassword;
this.timeExpire= timeExpire;
this.isValidKey= isValidKey;
this.timeoutType= timeoutType;
}
}
Then we create an immutable stateful instance of the class, a message, in the parent activity, and send it in an intent as in:
private void LaunchManageKeys() {
Intent i= new Intent(this, ManageKeys.class); // no param constructor
// push data (4)
MainManageKeysMessage message= new MainManageKeysMessage(lengthPassword,timeExpire,isValidKey,timeoutType);
Bundle b= new Bundle();
b.putSerializable("jalcomputing.confusetext.MainManageKeysMessage", message);
i.putExtras(b);
startActivityForResult(i,REQUEST_MANAGE_KEYS); // used for callback
}
Finally, we retrieve the object in the child activity.
try {
inMessage= (MainManageKeysMessage) getIntent().getSerializableExtra("jalcomputing.confusetext.MainManageKeysMessage");
lengthPassword= inMessage.lengthPassword;
timeoutType= inMessage.timeoutType;
isValidKey= inMessage.isValidKey;
timeExpire= inMessage.timeExpire;
} catch(Exception e){
lengthPassword= -1;
timeoutType= TIMEOUT_NEVER;
isValidKey= true;
timeExpire= LONG_YEAR_MILLIS;
}
Well there is another way to transfer an object.We can use application to transfer object and this is way is far better way in my opinion.
First of all create your custom application in your main package.
public class TestApplication extends Application {
private Object transferObj;
#Override
public void onCreate() {
super.onCreate();
// ACRA.init(this);
}
public Object getTransferObj() {
return transferObj;
}
public void setTransferObj(Object transferObj) {
this.transferObj = transferObj;
}
}
Now use setTransfer and get transfer methods to move abjects from one activity to other like:
To Transfer:
((TestApplication) activity.getApplication()).setTransferObj(Yous object);
ToRecieve:
Object obj=((TestApplication) activity.getApplication()).getTransferObj();
NOTE
Always remember to make entry of this application in manifest application tag:
<application
android:name=".TestApplication">
</application>
You can convert your object to jsonstring using Gson or Jakson and pass using intent as string and read the json in another activity.