Reference projects: The award-winning Morse-Coder skill heavily relies on the States SDK. To learn more about this SDK use the open source of Morse Coder as a reference.
#Alexa States SDK for Java
This SDK is an extension to the Amazon Alexa Skills Kit for Java which gives you a really convenient alternative of persisting session state in a growing number of persistence stores like DynamoDB, AWS S3 and AWS IoT. It is an abstraction layer for reading state from and (permanently) saving state to either an Alexa session or one of the aforementioned data stores. This also is your framework for building your own state handlers for any possible data store.
Don't be scared by the complexity of that schema. Most of it is hidden for you when using the SDK.
Add below Maven dependency to your project.
<dependencies>
...
<dependency>
<groupId>io.klerch</groupId>
<artifactId>alexa-skills-kit-states-java</artifactId>
<version>1.1.0</version>
</dependency>
...
</dependencies>
Depending on what features you use from this SDK you also need to add dependencies to certain AWS SDKs dedicated to S3, DynamoDb or IoT.
This SDK can save you hundreds of lines of code. See following examples where you can see how to manage state of your prepared POJO model (referred as AlexaStateModel) or just single values.
State is persisted throughout one client session.
final AlexaStateHandler handler = new AlexaSessionStateHandler(session);
final User abby = handler.readModel(User.class, "Abby").orElse(handler.createModel(User.class, "Abby"));
abby.setPersonalHighscore(80);
abby.saveState();
handler.writeValue("overallHighscore", abby.getPersonalHighscore());
State is persisted permanently per user.
final AlexaStateHandler handler = new AWSDynamoStateHandler(session);
final User john = handler.readModel(User.class, "John").orElse(handler.createModel(User.class, "John"));
john.setPersonalHighscore(90);
john.saveState();
handler.writeValue("overallHighscore", john.getPersonalHighscore());
If you like to administer state objects in files why not using an S3 bucket?
final AlexaStateHandler handler = new AWSS3StateHandler(session, "bucketName");
final User bob = handler.readModel(User.class, "Bob").orElse(handler.createModel(User.class, "Bob"));
bob.setPersonalHighscore(100);
bob.saveState();
handler.writeValue("overallHighscore", bob.getPersonalHighscore());
You can not only use handlers to persist states but also to propagate it. By propagating state to an AWS IoT thing shadow you interact with physical things easily
final AlexaStateHandler handler = new AWSIoTStateHandler(session);
final User tim = handler.readModel(User.class, "Tim").orElse(handler.createModel(User.class, "Tim"));
tim.setPersonalHighscore(110);
tim.saveState();
handler.writeValue("overallHighscore", tim.getPersonalHighscore());
It is easy to implement your own AlexaStateHandler so you can save state in whatever you want to use.
Each model declares on its own what is saved and even can decide on what scope is used to read and write attributes. That is how you can not only save state per user but also per application for e.g. managing the highscore of your game skill.
Now you will learn how to pimp your Alexa skill with permanent state capability in minutes.
This step is optional. If you just want to read/write single values to the store go on with step 4).
For complex information schema in your skill you better start organizing your state in objects. The above sample had the User-object. Think of a POJO with some member fields.
- Let your POJO derive from AlexaStateModel and you are ready to go.
- Tag members of your POJO whose state you want to save.
public class User extends AlexaStateModel {
@AlexaStateSave(Scope = AlexaScope.USER)
private Integer personalHighscore;
@AlexaStateSave(Scope = AlexaScope.SESSION)
private Integer currentScore;
// ...
}
Optionally you can give each member a scope so you can decide on the context the value is saved. Where personalHighscore is persisted per USER on a permanent basis, currentScore will only be saved throughout one Alexa session. Instead of white-listing members of your model you can also black-list them if tagging the whole model as AlexaStateSave
@AlexaStateSave(Scope = AlexaScope.APPLICATION)
public class QuizGame extends AlexaStateModel {
private Integer highscore;
private String highscorer;
@AlexaStateIgnore
private Integer level;
// ...
}
Wow, there is the third scope APPLICATION you can use to let state of your models be valid throughout all users in all sessions. The highscore value will be shared amongst all users of your skill whereas the level is ignored and will not persist in your session.
When serializing and deserializing models, the States SDK relies on Jackson's ObjectMapper. That being said, you can use Json-properties, reference your own (de)serialization-logic and more in the AlexaStateModels.
Depending on where you want to save your model's states you can pick from one of the following handlers:
The AlexaSessionStateHandler persists state in the Alexa session JSON and and is not capable of saving state in USER- or APPLICATION-scope.
final AlexaStateHandler sh1 = new AlexaSessionStateHandler(session);
The AWSS3StateHandler persists state in files in an AWS S3 bucket. It can be constructed in different ways. All you have to provide is an S3 bucket. You maybe want to hand in an AWS client from the AWS SDK in order to have set up your own credentials and AWS region. As the handler also gets the Alexa session object, whatever is read from or written to S3 will be in your Alexa session as well. So you won't need to read out your model state over and over again within one session.
final AlexaStateHandler s3h1 = new AWSS3StateHandler(session, "bucketName");
final AlexaStateHandler s3h2 = new AWSS3StateHandler(session, new AmazonS3Client().withRegion(Regions.US_EAST_1), "bucketName");
The AWSDynamoStateHandler persists state in items in a DynamoDB table. If you don't give it a table to work with, the handler creates one for you. Once more you could hand in an AWS client with custom configuration. As the handler gets the Alexa session object, whatever is read from or written to the table will be in your Alexa session as well. So you won't need to read out your model state over and over again within one session.
final AlexaStateHandler dyh1 = new AWSDynamoStateHandler(session);
final AlexaStateHandler dyh2 = new AWSDynamoStateHandler(session, "tableName");
final AlexaStateHandler dyh3 = new AWSDynamoStateHandler(session, new AmazonDynamoDBClient(), "tableName");
The AWSIoTStateHandler persists state in a virtual representation of a physical thing - in AWS IoT this is called a thing shadow. AWS IoT manages state of that thing and automatically propagates state updates to the connected thing. It also receives state updates from the thing which will persist in the shadow as well. This handler can also read out that updated data and serialize it in your model.
final AlexaStateHandler ioth1 = new AWSIoTStateHandler(session);
final AlexaStateHandler ioth1 = new AWSIoTStateHandler(session, new AWSIotClient(), new AWSIotDataClient());
So you got your POJO model and also your preferred state handler. They now need to get introduced to each other. The most convenient way is to instantiate your model with help of the state handler. Of course you can construct your model as you like and set the handler later on.
final User bob = handler.createModel(User.class, "Bob");
final QuizGame game = handler.createModel(QuizGame.class);
There's a big difference between both lines because the first one gives the model being created an identifier. This is how you can have multiple models per scope and can address their state with the same Id later on. The second line does not provide an Id causing this model to be a scope-wide singleton. What in this case makes total sense as the QuizGame is scoped as APPLICATION and is shared with all users of your skill. Moreover, the second approach won't let you deal with identifiers.
Continuing from above lines we now assign some values to bob and set a new highscore in the game. But nothing will be persisted until you tell your model to save its state. There are two alternatives of doing so:
// save state from within your model
bob.setPersonalHighscore(100);
bob.saveState();
// save state with handler
game.setHighscore(100);
handler.writeModel(game);
The first approach does work because we introduced bob to the handler on construction. The second approach would even work for models which were constructed without the handler. Feel free to introduce your model to another handler with its setHandler(AlexaStateHandler handler) at any time. Let's say you want to save bob's state in S3 and in DynamoDB.
bob.withHandler(s3Handler).saveState();
// or like this
dynamoHandler.writeModel(bob);
You are not limited to POJO models. Since version 1.0.0 you can also write single values to a store. You need to provide an id and a serializable value and optionally the scope you want that value to be saved in (by default the scope is SESSION)
dynamoHandler.writeValue("mySessionStateKey", "myValue");
dynamoHandler.writeValue("myUserStateKey", 123, AlexaScope.USER);
Moreover, you can write multiple models or values at once. Depending on the used handler you can leverage batch processing capabilities to enhance write performance to e.g. DynamoDB. If you´re using handlers which are not able to batch-process the following calls result in multiple single write transactions behind the scenes (the S3 handler does that for example):
final AlexaStateObject obj1 = new AlexaStateObject("mySessionStateKey", "myValue");
final AlexaStateObject obj2 = new AlexaStateObject("myUserStateKey", 123, AlexaScope.USER);
dynamoHandler.writeValues(Arrays.asList(obj1, obj2));
// or write multiple models
dynamoHandler.writeModels(bob, john, abby);
So real Bob is leaving his Echo for a week. After some days he's asking your skill again what's his personal highscore. As your skill is pimped with the State SDK it just needs to read out bob over the same handler it was saved back then.
final Optional<User> bob = handler.readModel(User.class, "Bob");
if (bob.isPresent()) {
final Integer bobsHighscore = bob.get().getPersonalHighscore();
}
He also wants to know the current highscore amongst all users as this could have changed meanwhile. Remember the QuizGame is persisted in APPLICATION-scope.
final QuizGame game = handler.readModel(QuizGame.class).orElse(handler.createModel(QuizGame.class));
final Integer highscore = game.getHighscore();
}
Thanks to Optional's of Java8 we can react with creating a new QuizGame on not-existing in a fancy one-liner. We should have already done so when we constructed the models in chapter 3. Constructing and saving a model with or without and Id potentially overwrites an existing model in the store. Take the last code-lines as a best-practice of constructing your models.
Assume we used a AWSDynamoStateHandler or AWSS3StateHandler for reading out bob's state and we want use it throughout the current session without getting back to S3 or DynamoDB a second time. Well, ...
new AlexaSessionStateHandler(session).writeModel(bob);
Next time you can read out bob with AlexaSessionStateHandler and not with the handler of DynamoDB or S3.
final AlexaStateHandler sh = new AlexaSessionStateHandler(session);
final AlexaStateHandler dyh = new AWSDynamoStateHandler(session);
final User bob = sh.readModel(User.class, "bob").orElse(dyh.readModel(User.class, "bob").orElse(dyh.createModel(User.class, "bob")));
Once again you can also read to single values from the store by giving its id. By default the value is read from SESSION scope unless you provide the desired scope to read from.
final Optional<AlexaStateObject> obj1 = sh.readValue("mySessionStateKey");
final Optional<AlexaStateObject> obj2 = sh.readValue("myUserStateKey", AlexaScope.USER);
You can also read multiple values at once by providing a list of ids the handler should look after in the store. It returns a list of AlexaStateObjects found in the store. If you'd like to read multiple models of the same type at once a map is returned instead. For each model found in the persistence store an entry is added to the map where its key value is the model-id and the value is the model itself.
final List<AlexaStateObject> obj = sh.readValues("mySessionStateKey", "myUserStateKey");
final Map<String, User> users = sh.readModels(User.class, "Bob", "Abby", "John", "Julia");
Also check out what the exists methods can do for you. These methods check existence of models or single values in a store.
if (sh.exists("mySessionStateKey") || sh.exists(User.class, "Bob")) {
// ...
};
Of course you can delete the state of your model. Let's keep it short as I think you already got it.
// first alternative
bob.removeState();
// second alternative
handler.removeModel(bob);
// or if we haven't read out bob so far
handler.readModel(User.class, "Bob").ifPresent(bob -> bob.removeState());
Same goes for single values by using removeValue. Once again, you can also remove more than one model or value wiht removeValues or removeModels.
Putting it together, there's a lot you can do with these extensions in regards to state management in your Alexa skill. Get detailed information for this SDK in the Javadocs.
One last example. Running userScored("Bob", 100)
void userScored(String player, Integer score) throws AlexaStateErrorException {
final AlexaStateHandler handler = new AWSDynamoStateHandler(this.session);
final User user = handler.readModel(User.class, player).orElse(handler.createModel(User.class, player));
// check if last score is player's personal highscore
if (user.getPersonalHighscore() < score) {
user.setPersonalHighscore(score);
user.saveState();
}
// check if last score is all-time highscore of the game
final QuizGame game = handler.readModel(QuizGame.class).orElse(handler.createModel(QuizGame.class));
if (game.getHighscore() < score) {
game.setHighscore(score);
game.setHighscorer(player);
game.saveState();
}
}
with nothing you to bring except for some credentials to your AWS account gets you to
Let's congrat Bob for beating the highscore ;)
You may have noticed that by default Alexa's userId is used to as key when storing user-scoped model-state. This userId that comes in with every request to your skill, will change when a user re-enables your skill. That's not ideal as he would lose his state when enabling your skill with another Amazon account or disables and re-enables your skill with the same account. If you're using account-linking you may have your own userId which is independant from the skill enablement. You can assign custom userIds to a handler (setUserId(String), withUserId(String)) and it will use this one when saving user-scoped model-state.