Uncategorized

How to build Android apps and not crash in the attempt (Part II)


In the previous post, we showed you how to set up our project. If you missed it, you can check it out here. This time we will show you how to make an API request, receive, and parse it to a java object using one of the most popular library called retrofit. You can check the detailed documentation here.

 

Adding retrofit to the project

Let’s start by adding the necessary dependencies in gradle using the latest stable version.

Go to project view (1), make sure that the view type is Android (2), and open Gradle Scripts -> build.gradle. (3)

Add the following lines as shown in the image.

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

After any change on the gradle file (1), you have to sync everything by clicking on Sync now (2).

If the sync was successful, you will get the following message in the build section:

To check if you have done everything correctly, go to the Run menu and click Run App.

You will find something like this on the emulator or your device.

Now we are ready to start coding. Let’s start by understanding what we are going to do and what we need.

 

Using a solidus app as the data source

Same as last time, we will show you a product list from a solidus store. I have one ready here. Use the URL below to ask for these products.

https://myapptest-magmalabs.herokuapp.com/api/products.

Note: This is a demo store mounted on Heroku. You can and should mount yours for this project by following this link – it will only take five minutes. Add your own store name, and you are ready to go.

On Heroku: When you open the app, you should see something like this.

Now, we have our own store and already some data to play with. However, if we are trying to access this data from our API URL we will get the following error:

Let’s get this API key for our store.

Go to https://{your-store}.herokuapp.com/admin/login (remember your store name will be different from this one). The default solidus credentials are admin@example: test123.

Click on the admin email (1), copy and save the key value (2).

 

Testing the endpoint

Let’s see if we can access the product’s data from our store.

Go to https://www.hurl.it/, an API tester page and paste the API URL (1) make sure the HTTP method is GET. The API key will be added as a header named X-Spree-Token (2). Do the captcha and launch the request. You can find more info about http request and response here.

If the data is correct, you should get a response that is composed of a header and a body.

The body is the field we are interested in. It contains a JSON object with the following products:

Creating the java models

In this project, retrofit will handle the JSON response and parse it to a java object. However, it is your job to create the matching java model. Another tool is http://www.jsonschema2pojo.org/ and, as the name already suggests, it uses a JSON file to generate the java classes for us.

Copy from the top to the second product; make sure you don’t select the comma. Copy and paste it.

Remember to cut the JSON file; you will need it to complete the file by adding one closing square bracket and one closing curly bracket ‘ ] } ’.

Check the options Java as the target language, Gson as annotation style, use double numbers, and include getters and setters. Optionally, you can add the package and class name. Click on preview.

You only need the Example and Product classes. Create a new package called models and add these two classes here. Download or paste them into Android Studio. Your project structure should look like this:

If you get error messages, don’t worry, just add the imports. Note: You don’t need all the variables; you must delete some of them in both classes in order to continue. The final code for Example and Product should look like the following:

Example.java

package com.example.myfirtsapp.models;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.util.List;
public class Example {
   @SerializedName("count")
   @Expose
   private Integer count;
   @SerializedName("total_count")
   @Expose
   private Integer totalCount;
   @SerializedName("current_page")
   @Expose
   private Integer currentPage;
   @SerializedName("pages")
   @Expose
   private Integer pages;
   @SerializedName("per_page")
   @Expose
   private Integer perPage;
   @SerializedName("products")
   @Expose
   private List<Product> products = null;

   public Integer getCount() {
       return count;
   }

   public void setCount(Integer count) {
       this.count = count;
   }

   public Integer getTotalCount() {
       return totalCount;
   }

   public void setTotalCount(Integer totalCount) {
       this.totalCount = totalCount;
   }

   public Integer getCurrentPage() {
       return currentPage;
   }

   public void setCurrentPage(Integer currentPage) {
       this.currentPage = currentPage;
   }

   public Integer getPages() {
       return pages;
   }

   public void setPages(Integer pages) {
       this.pages = pages;
   }

   public Integer getPerPage() {
       return perPage;
   }

   public void setPerPage(Integer perPage) {
       this.perPage = perPage;
   }

   public List<Product> getProducts() {
       return products;
   }

   public void setProducts(List<Product> products) {
       this.products = products;
   }
}

Product.java

package com.example.myfirtsapp.models;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.util.List;

public class Product {
   @SerializedName("id")
   @Expose
   private Integer id;
   @SerializedName("name")
   @Expose
   private String name;
   @SerializedName("description")
   @Expose
   private String description;
   @SerializedName("price")
   @Expose
   private String price;
   @SerializedName("display_price")
   @Expose
   private String displayPrice;
   @SerializedName("available_on")
   @Expose
   private String availableOn;
   @SerializedName("slug")
   @Expose
   private String slug;
   @SerializedName("meta_description")
   @Expose
   private Object metaDescription;
   @SerializedName("meta_keywords")
   @Expose
   private Object metaKeywords;
   @SerializedName("shipping_category_id")
   @Expose
   private Integer shippingCategoryId;
   @SerializedName("taxon_ids")
   @Expose
   private List<Integer> taxonIds = null;
   @SerializedName("total_on_hand")
   @Expose
   private Integer totalOnHand;
   @SerializedName("meta_title")
   @Expose
   private Object metaTitle;
   @SerializedName("has_variants")
   @Expose
   private Boolean hasVariants;

   public Integer getId() {
       return id;
   }

   public void setId(Integer id) {
       this.id = id;
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public String getDescription() {
       return description;
   }

   public void setDescription(String description) {
       this.description = description;
   }

   public String getPrice() {
       return price;
   }

   public void setPrice(String price) {
       this.price = price;
   }

   public String getDisplayPrice() {
       return displayPrice;
   }

   public void setDisplayPrice(String displayPrice) {
       this.displayPrice = displayPrice;
   }

   public String getAvailableOn() {
       return availableOn;
   }

   public void setAvailableOn(String availableOn) {
       this.availableOn = availableOn;
   }

   public String getSlug() {
       return slug;
   }

   public void setSlug(String slug) {
       this.slug = slug;
   }

   public Object getMetaDescription() {
       return metaDescription;
   }

   public void setMetaDescription(Object metaDescription) {
       this.metaDescription = metaDescription;
   }

   public Object getMetaKeywords() {
       return metaKeywords;
   }

   public void setMetaKeywords(Object metaKeywords) {
       this.metaKeywords = metaKeywords;
   }

   public Integer getShippingCategoryId() {
       return shippingCategoryId;
   }

   public void setShippingCategoryId(Integer shippingCategoryId) {
       this.shippingCategoryId = shippingCategoryId;
   }

   public List<Integer> getTaxonIds() {
       return taxonIds;
   }

   public void setTaxonIds(List<Integer> taxonIds) {
       this.taxonIds = taxonIds;
   }

   public Integer getTotalOnHand() {
       return totalOnHand;
   }

   public void setTotalOnHand(Integer totalOnHand) {
       this.totalOnHand = totalOnHand;
   }

   public Object getMetaTitle() {
       return metaTitle;
   }

   public void setMetaTitle(Object metaTitle) {
       this.metaTitle = metaTitle;
   }

   public Boolean getHasVariants() {
       return hasVariants;
   }

   public void setHasVariants(Boolean hasVariants) {
       this.hasVariants = hasVariants;
   }
}

Adding retrofit helpers

Let’s continue with retrofit. First of all, we need to add an interface for the API calls.

 

package com.example.myfirtsapp;

import com.example.myfirtsapp.models.Example;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
public interface ApiService {
   @GET("api/products")
   Call<Example> callProducts(@Header("X-Spree-Token") String apiKey);
}


The annotation @GET defines the HTTP method and the path.

Call defines the model that retrofit will parse to. Remember that Example class contains an array of products – the information we need. We call this method callProducts (you can choose any name you like).

As arguments, we can add headers, body content, and more information to our request. For now, we only need to add the API key as the argument for the header. Remember, you can get more information about how to compose this method in the official documentation.

 

Now we will add another helper class called ApiClient. This class helps us to make and maintain an instance of retrofit.

package com.example.myfirtsapp;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ApiClient {
   private Retrofit retrofit;
   private final static String BASE_URL = "https://myapptest-magmalabs.herokuapp.com/";

   ApiClient() {
       this.retrofit = new Retrofit.Builder()
               .baseUrl(BASE_URL)
               .addConverterFactory(GsonConverterFactory.create())
               .build();
   }

   public ApiService getApiService(){
       return this.retrofit.create(ApiService.class);
   }
}

Here, we build our retrofit instance and define the base URL (the store URL).

Now, we have all we need to make our first request.

 

Adding the call to MainActivity

For the layout we will add only one button to the layout – nothing else.

<Button
   android:id="@+id/btn_request"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="Request"
   tools:layout_editor_absoluteX="148dp"
   tools:layout_editor_absoluteY="148dp" />

 

The MainActivity looks like this:

public class MainActivity extends AppCompatActivity {

   ApiService apiService;
   ApiClient apiClient = new ApiClient();
   private final String API_KEY = "916cbf7ef11713ec678b889cb913e9dd6051fbeeb6e8a730";

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       apiService = apiClient.getApiService();

       Button btnSendRequest = findViewById(R.id.btn_request);
       btnSendRequest.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               Call<Example> callproducts = apiService.callProducts(API_KEY);
               callproducts.enqueue(new Callback<Example>() {
                   @Override
                   public void onResponse(Call<Example> call, Response<Example> response) {
                       if (response.body().getProducts() != null)
                       iterateProducts(response.body().getProducts());
                   }

                   @Override
                   public void onFailure(Call<Example> call, Throwable t) {

                   }
               });
           }
       });
   }

   private void iterateProducts(List<Product> productList) {
       for(int i = 0; i < productList.size() ; i++){
           Log.v("Products", productList.get(i).getName());
       }
   }
}

 

From ApiClient we get an instance of retrofit – ready to use.

Now, we need to define the call we will make (the callProducts one). We need to send the API key as the argument.

In the callback, we will either have a positive or negative outcome. The negative response will be indicated as followed: no internet, connection error, timeout error, or anything that doesn’t allow the request to continue.

A positive response will lead to a successful connection, and you will receive a response from the server. (Note that the “401: unauthorized error response” is also a positive response from the server.)

You can see that all this is inside of the click listener method for the send request button, so we can make all the request we want by clicking the button.

 

Running the app

It is time to make our final test. Go to the Run menu and run the app. At the top, you are going to see a single button that we will remove later.

Click the button. Now, if you are asking yourself where the data are, go back to the iterateProducts method, and you will see that we can iterate the response and print them in Logcat. Next, go to android studio’s log and find something like this:

Looks familiar? These are the names of the products from the solidus store! Now we have all the products in the app, displayed as a list, and we can do anything with them.

 

What’s next?

In the next episode, we will work in the user interface and add this list of products to a RecyclerView to show them.

If you want to know more about Magmalabs, visit us here!

Beginner
Importing fonts the right way
Best Practices
A nice way to handle SASS color variables
MagmaHackers
MagmaHackers