Pokédex
Distribution Code
Download this project’s distribution code.
To open the distribution code, extract the ZIP, open Android Studio, select “Import project”, and select the folder you extracted from the ZIP.
What To Do
- Searching
- Catching
- Saving State
- Sprites
- Description
Searching
Let’s add some new functionality to our Pokédex app! First, let’s give users the ability to search the Pokédex for their favorite Pokémon.
To start, we’re going to use a built-in feature of Adapter
called Filterable
. This interface allows us to apply a filter to the data stored in our Adapter
, which is exactly what we need! We’ll filter out any Pokémon whose names don’t match the search text.
First, make sure that the adapter
variable in MainActivity
has the type PokedexAdapter
, like this:
private PokedexAdapter adapter;
We’ll be calling methods that are specific to our PokedexAdapter
that don’t exist on the base Adapter
class, so we need to use the PokedexAdapter
type.
Next, open up the PokedexAdapter
class. We can specify that our PokedexAdapter
implements Filterable
by changing the class declaration to:
public class PokedexAdapter extends RecyclerView.Adapter<PokedexAdapter.PokedexViewHolder> implements Filterable {
Recall that an interface is just a list of methods that any class can implement. Now that we’ve implemented Filterable
, we can add a new method called getFilter
to the PokedexAdapter
.
@Override
public Filter getFilter() {
return new PokemonFilter();
}
Of course, we don’t have a class called PokemonFilter
yet, so let’s create one! We can create this class inside of PokedexAdapter
, just as we did with PokedexViewHolder
, like this:
private class PokemonFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
// implement your search here!
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
}
}
You can implement your search inside performFiltering
. The argument to this method, constraint
, will be whatever text the user has typed into the search bar, which you can use for your filter. The performFiltering
method should return an instance of FilterResults
. Here’s an example:
@Override
protected FilterResults performFiltering(CharSequence constraint) {
// implement your search here!
FilterResults results = new FilterResults();
results.values = filteredPokemon; // you need to create this variable!
results.count = filteredPokemon.size();
return results
}
The instance of FilterResults
that you return from performFiltering
will then be passed to publishResults
. Inside of publishResults
, you probably want to store the results of the search in another class variable, so you don’t lose your copy of the list containing all Pokémon (i.e., the pokemon
variable). Assuming you call this variable List<Pokemon> filtered
, then your implementation of publishResults
might look like this:
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
filtered = (List<Pokemon>) results.values;
notifyDataSetChanged();
}
Then, rather than using the pokemon
variable inside of methods like onBindViewHolder
and getItemCount
, use your new filtered
variable.
Now that the filtering logic is done, let’s add a search bar above our RecyclerView
. On the left-hand side of Android Studio, expand the app
folder, and you should see a folder called res
. Recall that this is where the XML files for our layouts are stored. Right click on res
, then select New > Android Resource Directory. Enter menu
for both Directory name
and Resource type
, then press OK
. You should now see a new directory called menu
underneath res
.
Next, right click on that menu
directory and select New > Menu resource file. Call this file main_menu.xml
and then click OK
. This new XML file will contain the layout for our menu. Paste the below into that file:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_search"
android:title="Search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always" />
</menu>
As you can see, we’re creating a new menu
element with one item
child. The item
represents a search icon, that when pressed, will open up a SearchView
.
Now, we can wire up that SearchView
to our MainActivity
. First, we need to make MainActivity
implement an interface called SearchView.OnQueryTextListener
. To tell Android that our main activity class implements SearchView.OnQueryTextListener
, change the declaration of the class to the below:
public class MainActivity extends AppCompatActivity implements SearchView.OnQueryTextListener {
Next, to use the layout file we just created, we need to implement a method on our MainActivity
called onCreateOptionsMenu
.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setOnQueryTextListener(this);
return true;
}
As you’d guess, this method is called when an activity is creating a menu. Let’s walk through this code line-by-line. First, we’re specifying that this activity should use R.menu.main_menu
, which is the name of the XML file we created. Then, we’re grabbing a reference to the item
inside our menu using its ID, action_search
. Finally, we’re calling setOnQueryTextListener
on the SearchView
in order to specify that our search code will be specified in our MainActivity
class (which is what this
references).
Now, our SearchView
will automatically call methods on MainActivity
when the user types text into the SearchView
. Specifically, a method called onQueryTextChange
will be called, and the argument passed to that method will be a String
representing the current text of the SearchView
. We then want to pass that along to the PokemonFilter
we created earlier, like this, so our UI will update:
@Override
public boolean onQueryTextChange(String newText) {
adapter.getFilter().filter(newText);
return false;
}
Along the same line, a method called onQueryTextSubmit
will be called when the user presses the “submit” button on the keyboard, which you can handle in the same way:
@Override
public boolean onQueryTextSubmit(String newText) {
adapter.getFilter().filter(newText);
return false;
}
At this point, everything should be wired up, so you can test out your new search functionality!
Catching
Any good Pokédex keeps track of which Pokémon have been caught and which haven’t. Let’s add that functionality to our Pokédex as well.
First, let’s add a new Button
to the PokemonActivity
. Open up the layout XML file, and then add a new <Button>
element. You can set the text of this button to whatever you’d like, but we’ll go with Catch
for simplicity.
To handle taps on the Button
, we can use the attribute android:onClick="toggleCatch"
. Add that to your Button
, and then a method called public void toggleCatch(View view)
will automatically be called whenever the user presses on the button.
Naturally, you’ll want to add that method to your PokemonActivity
, like this:
public void toggleCatch(View view) {
// gotta catch 'em all!
}
Now, we can implement catching. To start, add a new boolean class variable that keeps track of whether or not the Pokémon is caught. If a Pokémon is caught, change the text of the button to something like Release
, and vice-versa when it’s released. The Button
method setText(String text)
method will come in handy.
Saving State
You’ll notice that if you stop running your app and then run it again, your Pokédex will forget which Pokémon are caught and which aren’t! Let’s fix that by saving that state to disk.
As your last task, use the SharedPreferences
class to save which Pokémon are caught. With this class, you can store state that will be remembered each time your app launches, which is just what you need. How you store this state is up to you—you might consider storing a list of all Pokémon that are caught, or you might consider using a map from Pokémon to boolean values.
Here’s an example:
getPreferences(Context.MODE_PRIVATE).edit().putString("course", "cs50").commit();
String course = getPreferences(Context.MODE_PRIVATE).getString("course", "cs50");
// course is equal to "cs50"
To test saving state, you should be able to catch a Pokémon, stop the simulator, start the simulator again, and still see that Pokémon as caught.
Sprites
Every Pokémon aficionado has noticed by now that our Pokédex doesn’t yet have arguably its most important feature: the ability to display what each Pokémon looks like! Luckily for us, the API we chose contains links to images for each Pokémon.
Let’s add that functionality to our app. First, add a new ImageView
to the layout for PokemonActivity
. Give it a unique ID, and then create an ImageView
class variable inside of PokemonActivity
, and use findViewById
to map that variable to your layout.
Next, when parsing the response from the API call, take a look at the key called sprites
. You’ll notice that it’s a dictionary, and the key front_default
contains a URL pointing to an image of a Pokémon. Use the value of that key to load in an image to your ImageView
. You’ll want to follow a similar pattern as before—use methods like getJSONObject
and getString
to parse the JSON strings into Java objects.
Once you have the URL of the image, you’ll need to download it from the Internet. To do so, we’ll use an Android built-in called AsyncTask
. An AsyncTask
executes some code in the background, so your app doesn’t lock up as the image is downloading. To use an AsyncTask
, create a new class that looks like this:
private class DownloadSpriteTask extends AsyncTask<String, Void, Bitmap> {
@Override
protected Bitmap doInBackground(String... strings) {
try {
URL url = new URL(strings[0]);
return BitmapFactory.decodeStream(url.openStream());
}
catch (IOException e) {
Log.e("cs50", "Download sprite error", e);
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// load the bitmap into the ImageView!
}
}
Let’s walk through this. On the first line, we’re specifying that our AsyncTask
takes a String
as input, and will return a Bitmap
. That makes sense, since we’ll be passing in a URL as a String
, and we expect a Bitmap
object, which represents an image, in exchange. The doInBackground
method is where we’ll put the logic to actually download an image. You’ll notice that this method actually takes an array of strings, but we only need to download one, so we’re just taking the first element in that array with strings[0]
.
After doInBackground
completes, the method called onPostExecute
will be called. The Bitmap
argument that’s passed in represents a loaded image, so load that into your ImageView
using the method setImageBitmap
.
Finally, you can use this new class to trigger a download of a string URL with:
new DownloadSpriteTask().execute(url); // you need to get the url!
You can test your code by selecting Pokémon from the list, and you should see images in the ImageView
!
Description
Let’s add one last feature to our Pokédex: a description of each Pokémon. From the API documentation, we can see that we can use /api/v2/pokemon-species/{id} to retrieve a description for a given Pokémon: https://pokeapi.co/docs/v2#pokemon-species. For instance, the URL https://pokeapi.co/api/v2/pokemon-species/133/
will give you the description text for everyone’s favorite Pokémon.
Specifically, what we’re looking for can be found in the key called flavor_text_entries
. This key happens to contain entries for several different languages, but we’re just concerned with English for now. You might need a few additional structs to model the data for these new keys.
After a user selects a Pokémon from the list, make a separate API call to this second endpoint to retrieve the description of the selected Pokémon. Filter for just the first English description, and then display it somewhere on the screen. (Some Pokémon have more than one English description, and it suffices to just display the first one.) You’ll probably want to wire up a new TextView
to display this final piece of data.
You should see a few sentences about each Pokémon after selecting it from the list!
How to Submit
To submit your code with submit50
, you may either: (1) upload your code to CS50 IDE and run submit50
from inside of your IDE, or (2) install submit50
on your own computer by running pip3 install submit50
(assuming you have Python 3 installed).
Execute the below, logging in with your GitHub username and password when prompted. For security, you’ll see asterisks (*
) instead of the actual characters in your password.
submit50 cs50/problems/2020/x/tracks/android/pokedex