MP3: Refactoring and More JSON
In MP2, you connected the app to our game server to get and respond to invitations. In Checkpoint 3, you’ll make it possible to create multiplayer games of the two game modes and refactor some of your old code to improve it.
When you first started working on the MP, we hadn’t covered object-oriented programming yet, so the target mode functionality had to be implemented using only arrays and imperative programming. As you discovered, that was not ideal. Programmers often learn or realize better approaches to problems already solved and revisit old code to improve it. This process is called refactoring. In this checkpoint we’ll refactor some of the code you wrote in MP0 and organize some relevant logic into a class that can more easily be used in the next checkpoint.
Checkpoint 3 is a one week MP, so you only have one deadline, which varies by your deadline group. MP3 is due at:
-
11:59 PM on Sunday, 03/29/2020 for the Blue Group: all labs starting at 3 PM or earlier
-
11:59 PM on Monday, 03/30/2020 for the Orange Group: all labs starting at 4 PM or later
Late submissions are subject to the MP late submission policy.
However, note that we will adjust the deadline as needed depending on the success of our efforts to provide virtual support.
1. Learning Objectives
On MP3 you will:
-
Work with and improve older code
-
Build JSON objects to deliver additional information to the web API
We will continue to exercise your class design like in previous checkpoints.
2. Assignment Structure
For information on all classes present after the completion of Checkpoint 3, refer to our official Javadoc.
2.1. Obtaining MP3
To update your project with the remote and get started on MP3, follow these instructions:
-
Get the remote code. Select VCS | Git | Fetch from the menu to download the remote commit. However, it still needs to be merged with your work.
-
Merge the changes! Now we need to merge the changes from your computer with our remote repository, so go back to the menu and choose VCS | Git | Merge Changes. Check the
remotes/release/mp3
branch you just fetched and press Merge! The Version Control pane will appear to inform you of items updated from the server. -
Update
grade.yaml
. Open the file in the root of the project, change thecheckpoint
setting to3
, and do File | Sync Project with Gradle Files.
2.2. Late Submissions
Like in previous checkpoints, setting useProvided
in grade.yaml
to true
causes the app to use our provided components rather than your implementations.
For Checkpoint 3, we provide TargetVisitChecker
(with both MP0-style and
refactored signatures), AreaDivider
, the launch activity, the main activity,
and the gameplay logic in GameActivity
.
As usual, you can always go back and make submissions to previous checkpoints by
changing checkpoint
in grade.yaml
.
However, merging MP3 will produce compilation errors until you add the classes
expected by the new test suite.
3. Tools and Resources
This section provides you with tools and information that you will need to complete the features in Your Goal.
3.1. Lists
Java arrays are useful for storing multiple values of the same type, but an array’s length cannot be changed after its creation. Often it’s not known in advance how many entries a collection will have, or the collection has items added to or removed from it. In those situations, arrays are inconvenient.
Fortunately, Java provides lists: objects that hold dynamically resizable
lists of things.
All lists are instances of
List
,
but List
is an interface rather than a concrete type.
To create a list you create an instance of a List
implementation, the most
common of which is
ArrayList
.
This is an example of polymorphism.
When you declared arrays, you put the element type before square brackets, like
String[]
for an array of strings.
List variables are declared like List<String>
, with the element type in angle
brackets.
To declare a list-of-strings variable and initialize it to an empty ArrayList
,
you would use code like this:
List<String> names = new ArrayList<>();
The angle brackets on the right side of the assignment can be empty, which means that the actual list object created holds the same type of values as the variable is declared to contain.
The List
type defines many functions that are usable on all lists.
The most useful ones are:
-
add
, which adds one element to the end of the list (increasing its size by one) -
size
, which returns the current length of the list -
get
, which gets the element at the specified position in the list -
remove
, which removes an element from the list (decreasing its size by one)
For example:
System.out.println(names.size()); // Prints 0 - the list is empty initially
names.add("Chuchu");
System.out.println(names.size()); // Prints 1 - now the list has one element
names.add("Xyz");
System.out.println(names.size()); // Prints 2 - now the list has two elements
System.out.println(names.get(0)); // Prints "Chuchu", the first element in the list
names.remove("Chuchu");
System.out.println(names.get(0)); // Prints "Xyz", the first element now that Chuchu was removed
System.out.println(names.size()); // Prints 1 - the list has one element anymore
Lists can be iterated over with the enhanced for loop:
for (String name : names) {
// Do something with name?
}
Types like List
and ArrayList
will need to be imported before you can use
them.
Android Studio can help with this.
If you press Tab to autocomplete a type name, any needed import statement will
automatically be added at the top of the file.
3.2. Writing JSON with Gson
In Checkpoint 2, you used Gson to read data from parsed JSON. In this checkpoint, you’ll need to create JSON objects to send to the server.
Gson can help with this too.
To create a new JSON object, use new JsonObject()
.
To add a single, simple value like a string or number as a property on an
object, call the object’s addProperty
function, passing the property name and
value.
For example, this code builds a JsonObject
corresponding to
the first MP2 JSON example:
JsonObject point = new JsonObject();
point.addProperty("latitude", 40.109187);
point.addProperty("longitude", -88.227213);
To add a more complicated value like an array or other object as a property of
an object, use add
instead.
Likewise, to create a JSON array, use new JsonArray()
.
Its add
function will add an entry to the end of the array.
This code reconstitutes the more complicated JSON object from the MP2 writeup:
JsonObject cs125 = new JsonObject();
cs125.addProperty("name", "CS 125");
cs125.addProperty("enrollment", 800);
JsonObject location = new JsonObject();
location.addProperty("name", "Foellinger Auditorium");
location.addProperty("allows_food", false);
location.addProperty("latitude", 40.105952);
location.addProperty("longitude", -88.227204);
cs125.add("location", location);
JsonArray lectureDays = new JsonArray();
lectureDays.add("Monday");
lectureDays.add("Wednesday");
lectureDays.add("Friday");
cs125.add("lecture_days", lectureDays);
Gson objects stringify to the JSON text they represent, so you can pass them to
System.out.println
to see what JSON you’ve built.
It will be condensed onto one line and difficult to read, so you may find it
helpful to paste that into
a JSON formatter
to see its structure more easily.
3.3. Our API Documentation
To create a multiplayer game, the app makes a POST request to our
/games/create
endpoint.
Since there is a lot of game information rather than just a game ID, the game
configuration will need to be uploaded to the server as the body (payload) of
the request.
The body will be a JSON object (Gson JsonObject
instance) with these
properties:
-
mode
(string) is the game mode, either "target" or "area" -
invitees
(array of objects) is the list of players invited to the game, including the user. Each object should have these properties:-
email
(string) is the invitee’s email address -
team
(integer) is theTeamID
code for the role/team the user is invited to
-
-
For target mode only,
proximityThreshold
(integer) is the proximity threshold in meters -
For target mode only,
targets
(array of objects) is the list of targets in the game. Each object should have these properties:-
latitude
(double) is the latitude of the target -
longitude
(double) is the longitude of the target
-
-
For area mode only,
cellSize
(integer) is the cell size in meters -
For area mode only,
areaNorth
,areaEast
,areaSouth
, andareaWest
(all doubles) are the latitude/longitude bounds of the area
You may find the example target mode body and example area mode body helpful.
If the game is created successfully, the server’s response will be a JSON object
with a single game
property whose value is the (string) game ID.
Our provided code uses this to launch GameActivity
.
3.4. Extra Credit API Documentation
If you are attempting the extra credit feature to allow the user to load a
predefined set of targets, your app will need to be able to fetch the preset
targets lists from the server.
Those are accessible by a GET request to the /presets
endpoint.
The server’s response will be a JSON object containing this property:
-
presets
(array of objects) is the list of preset options. Each object has these properties:-
name
(string) is the human-readable name of the preset -
targets
(array of objects) is the list of targets in the preset. Each has at least these properties:-
latitude
(double) is the target’s latitude -
longitude
(double) is the target’s longitude
-
-
You may find
this example response
helpful.
Do not assume that the note
property will always be present on target objects,
but feel free to do anything you like with it if it’s there.
You can always ignore it completely.
3.5. Reverting Changes with Git
Version control systems like Git make it possible to retrieve older versions of your code, which is very useful if you accidentally damage a file. Android Studio integrates with Git to allow you to undo (revert) changes with its UI.
If you would like to put a file back to how it was at the last commit, right-click it in the Project pane and choose Git | Revert. This brings up the Revert Changes dialog, where you can select any additional files you would like to revert. Reverting a file throws away all changes to it since the last commit and is usually not reversible.
For a more surgical approach, Android Studio highlights changed regions of files with colored bars or gray triangles in the left margin of the code editor. Clicking one of these decorations produces a toolbar with a back arrow (Rollback Lines) button that reverts just the highlighted lines to how they were in the last commit. This rollback method may sometimes be reversible with Ctrl+Z, but you should still be certain that you want to throw away your changes.
4. Your Goal
When you’re done with Checkpoint 3, creating a game will upload its configuration to the server and make it visible to the invitees, who can then accept or decline the invitation using their app. The game setup activity will show only the settings for the selected game mode.
4.1. The Target
Class
The new test suite, Checkpoint3Test
, is initially unable to compile because it
refers to a Target
class in the logic
subdirectory that does not yet
exist, so this must be fixed first.
We will be using the Target
class primarily in the next checkpoint to help
manage a target marker on the map, since the Checkpoint 0 approach of passing
coordinates to a changeMarkerColor
function is unwieldy
1.
Create the class by right-clicking the logic
subfolder within the package,
choosing New | Java Class, entering Target
in the Name field, and clicking
OK.
We also expect a GameSetup
logic class, described in the next section, that
you will need to similarly create before your code can compile.
You don’t need to implement all the functionality at once, but you should
create functions to match the Javadoc so you can get the tests running.
Be sure that the files were created inside the logic
subpackage and that they
were added to Git.
If they are not, your code may not be seen during official grading.
To see the needed public members of this class, refer to
our official Javadoc.
You will need to store a Google Maps
Marker
object in a private instance variable.
To place a marker on a Google map, use the map’s addMarker
function
2:
// Suppose position is a LatLng variable
MarkerOptions options = new MarkerOptions().position(position);
// Set any other options you like?
Marker marker = map.addMarker(options);
To change the color of a marker after it has been created, use its setIcon
function
3:
// Suppose hue is a hue value like the constants defined on BitmapDescriptorFactory
BitmapDescriptor icon = BitmapDescriptorFactory.defaultMarker(hue);
marker.setIcon(icon);
You can refer to
this Android article
to check out the different HUE_
constants to use in Target.java
.
After completing this task, testTargetClass
will pass.
You may optionally rework your target mode logic in GameActivity
to take
advantage of this new class, but otherwise you will not need it again in this
checkpoint.
4.2. LatLng
Refactor
Functions that take eight parameters, especially all of the same type, can be
difficult to use.
This is even more unfortunate when some of the parameters really belong
together, packaged up into objects.
Now that you know how to use objects like the Google Maps SDK’s LatLng
, we’ve
rewritten LinesCrossDetector.linesCross
to accept the lines' endpoints as
LatLng
objects
4.
You need to copy the new version from
this GitHub
Gist
over your current LineCrossDetector
so that linesCross
can be called with
four LatLng
positions.
You will also need to update the places in your code that call it to match.
Similarly refactor the addLine
function in GameActivity
to take two LatLng
endpoints rather than four double
coordinates.
You will need to update the function’s callers to be compatible with its new
signature.
Once you complete these tasks, testLatLngRefactor
will pass.
If you make a mistake while refactoring and want to put a file back to how it was at the last commit, see the section on reverting changes.
4.2.1. Optional: Refactoring TargetVisitChecker
If you would like to, you may refactor your TargetVisitChecker
methods to take
a LatLng[]
in place of the two double[]
s.
Updated Javadoc is available.
The Checkpoint 0 tests are forward-compatible with this change.
After doing that, you’ll probably want to use the getPositions
function of
DefaultTargets
rather than getLatitudes
and getLongitudes
in your
GameActivity
target mode setup.
Better yet, you may take advantage of your new list skills to keep track of the
target mode game state entirely inside GameActivity
.
If TargetVisitChecker
is removed, the Checkpoint 0 test results will be
all-or-nothing based on the result of testTargetModeGameplay
.
TargetVisitChecker
will be removed entirely in the next checkpoint and
GameActivity
will be significantly remodeled then, so don’t get too attached
to either.
This section of refactoring is not required and will not be graded, but it is encouraged to practice changing your code, as it is a necessary real-world skill!
4.3. The GameSetup
Class
You will need to create a GameSetup
class in the logic
subfolder.
This class will contain two static helper methods that take the app’s current
game information and convert all the data into a JSON payload that can be sent
in a POST request.
Remember to follow the documentation for our API so
that when you write properties and values into your JsonObject
, they match the
naming and type conventions that we’ve specified.
Some of the parameters passed to your functions are List
s.
You will need to read from them according to our list introduction
but should not need to modify them or create new lists here.
You can refer to the Javadoc for the class here. Be sure to implement both listed functions, but you are encouraged to add helper functions as you design your logic.
Once the functions are fully implemented, GameSetup
will be able to create
JSON objects representing the configuration of a multiplayer target game or area
game.
testJsonTargetMode
and testJsonAreaMode
respectively will then pass.
4.4. Game Setup UI
The game configuration screen allows the user to select their desired game mode
(area or target) and set other parameters like the cell size or proximity
threshold.
This screen’s layout is activity_new_game.xml
and its Java class is
NewGameActivity
.
Our layout contains a RadioGroup
with ID gameModeGroup
.
Inside this RadioGroup
are two RadioButton
s.
One has ID targetModeOption
and the other has ID areaModeOption
.
The user will use these to pick the game mode.
Some settings only make sense for one game mode, so they shouldn’t be shown all
the time.
For example, the user shouldn’t see a setting for proximity threshold when
setting up an area mode game.
To allow showing and hiding the different game-mode-specific settings as a unit,
we’ve organized the views into containers.
There is a LinearLayout
with the ID areaSettings
.
If the user chooses to play a game in target mode, this LinearLayout
should
disappear.
Otherwise, the user will use this settings container to configure their game.
For target mode settings, we’ve added another container with ID
targetSettings
.
To make the radio buttons change the containers' visibility, we need to add code
to NewGameActivity
.
In onCreate
, attach a handler that will be run when the selected radio button
in the RadioGroup
is changed:
// Suppose modeGroup is a RadioGroup variable (maybe an instance variable?)
modeGroup = findViewById(R.id.gameModeGroup);
modeGroup.setOnCheckedChangeListener((unused, checkedId) -> {
// checkedId is the R.id constant of the currently checked RadioButton
// Your code here: make only the selected mode's settings group visible
});
Each mode’s settings group should be shown only when its option is selected.
Each settings group should be View.GONE
initially and when its mode is not
selected.
After you make this so, testSettingsGroupVisibility
will pass.
4.5. Extra Credit: Target Presets
Challenge problem!
This is extra credit because it takes a bit more work and tinkering.
It can be done before the game creation API request and before you complete the
RadioGroup
visibility modifications, so feel free to tackle this early on!
Many users won’t want to spend a lot of time picking out enough targets for an interesting target mode game. To make it easier to add a set of targets, the app could have several suggested lists of targets and allow the user to add an entire suggested list at once.
Inside the target mode settings group, we have added a "Load Preset" with ID
loadPresetTargets
.
When it is clicked, you need to fetch the list of presets from
the server.
When the request completes, create and show an AlertDialog
to list the
options.
Refer to Android’s
AlertDialog
guide
for details.
We have provided a chunk_presets_list.xml
layout resource which you can inflate
5
with a null parent
6
and pass to the dialog builder’s setView
function.
For each preset option, add a RadioButton
inside the provided RadioGroup
(ID presetOptions
), with the radio button’s text set to the
preset’s name.
This is the one place in the MP where you should create an individual view
dynamically using new
.
The constructors for most Android views take a context, which can be
the activity: this
7.
The alert dialog’s positive button should be labeled "Load". Its negative button should be labeled "Cancel." The dialog might look like this:
If the positive button (Load) is pressed with a preset selected, all existing
targets should be removed and all the targets from the selected preset should be
added.
There are multiple ways to associate a preset with a radio button—you may
find getTag
and setTag
helpful.
If the user presses Cancel or presses Load without selecting a preset, do
nothing and the dialog will be dismissed by default.
If you complete this task, testTargetPresets_extraCredit
will pass and you’ll
have earned 20% extra credit!
5. Grading
As always, 100 points is full credit on the checkpoint. But on MP3 there are 120 points available, broken down as follows:
-
15 points for the
Target
class -
15 points for refactoring
addLine
andLineCrossDetector
-
10 points for making the radio buttons in
NewGameActivity
control settings group visibility -
25 points for
areaMode
inGameSetup
-
25 points for
targetMode
inGameSetup
-
20 points of extra credit for the optional Load Preset feature
-
10 points for passing
checkstyle
inspection
If you missed a deadline in a previous checkpoint, doing the extra credit here is a great way to earn some of those points back!
Your app will be tested by Checkpoint3Test
.
Feel free to look through that class’s code to see what the test suite tries to
do with your app.
Post on the forum for clarifications about what exactly is expected.
6. Cliffhanger
Because the game setup screen submits the game configuration to the server instead of passing it to the game activity, gameplay is probably pretty broken at the moment. In the next and final checkpoint, we’ll finish the app by connecting the game activity to the server!
7. Cheating
By now you should be familiar with the cheating policies from the syllabus. Collaborating in a human language about how to approach the problems is encouraged, but sharing your code with anyone not currently on the course staff constitutes cheating.