Retrofit + Gson で、 API にアクセス & JSON のパージング処理を簡潔に実装する

# 動作例:

![retrofit_sample.gif](https://qiita-image-store.s3.amazonaws.com/0/131418/ddb8028a-cb54-9f2b-a5ca-7e4cf551fdc2.gif)


- コードは [GitHub](https://github.com/Hachimori/AndroidSamples/tree/master/RetrofitSample) に置きました。
- 以下の手順で動作確認可能です。
  1. AndroidStudio 起動
  2. File > New > Project from Version Control > GitHub を選択。 Git Repository URL を https://github.com/Hachimori/AndroidSamples.git に指定してリポジトリをクローンする
  3. File > Open として、RetrofitSample のプロジェクトを開く


# 概要
- [Retrofit](http://square.github.io/retrofit/): Android の HTTP クライアントライブラリ
- [Gson](https://github.com/google/gson): JSON を Java のオブジェクトに変換するライブラリ
- Retrofit と Gson を組み合わせて、 GitHub API にアクセス & 取得したデータを表示する方法をメモ


# 導入

## 手順
- build.gradle (Module: app) に追加

```gradle
dependencies {
    // ... 中略 ...
    
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    
    // ... 中略 ...
}
```

- 参考
  - [Retrofit公式サイト](http://square.github.io/retrofit/#download)


## ProGuard
- proguard-rules.pro に追加

```
# Retrofit
-dontnote retrofit2.Platform
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-dontwarn retrofit2.Platform$Java8
-keepattributes Signature
-keepattributes Exceptions
```

- 参考
  - [Retrofit公式サイト](http://square.github.io/retrofit/#download)



## GitHub の API
- 今回は、以下の三つを試しに利用する  
  1. ユーザの情報を取得する
      - API: https://api.github.com/users/【ユーザ名】
      - [API ドキュメント](https://developer.github.com/v3/users/#get-a-single-user)
  2. ユーザのリポジトリを列挙する
      - API: https://api.github.com/users/【ユーザ名】/repos
      - [API ドキュメント](https://developer.github.com/v3/repos/#list-user-repositories)
  3. リポジトリのコミットを列挙する
      - API: https://api.github.com/repos/【リポジトリの管理ユーザ名】/【リポジトリ名】/commits
      - [API ドキュメント](https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository)
  
  

# 実装例

## Java 側の実装

### API のエンドポイントを定義
- 以下のような Java Interface を実装

  ```Java
  public interface GitHubService {
      
      /**
       * ユーザの情報を取得する
       * https://developer.github.com/v3/users/#get-a-single-user
       */
      @GET("users/{username}")
      Call<User> getUser(@Path("username") String user);
      
      /**
       * ユーザのリポジトリ一覧を取得する
       * https://developer.github.com/v3/repos/#list-user-repositories
       */
      @GET("users/{user}/repos")
      Call<List<Repos>> listRepos(@Path("user") String user);
  
      /**
       * 
       * リポジトリのコミットを列挙する
       * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
       */
      @GET("repos/{owner}/{repos}/commits")
      Call<List<Commits>> listCommit(@Path("owner") String owner, @Path("repos") String repos);
  }
  ```
  - ポイント
      - アノテーション内の文字列に API エンドポイントを記述
      - "{ }" で囲った文字列は、メソッドの引数として渡した文字列に置き換えられる
  
  - 参考
      - http://square.github.io/retrofit/


### API から取得したデータを Java オブジェクトとして格納するクラスを定義
- 以下のようなクラスを定義
  - ユーザの情報を格納するクラス

  ```Java
  public class User {
      private String name;
      private String company;
      private String email;
      private String bio;
      private String created_at;
      private String updated_at;
      
      public String getName() {
          return name;
      }
      
      public String getCompany() {
          return company;
      }
      
      public String getEmail() {
          return email;
      }
      
      public String getBio() {
          return bio;
      }
      
      public String getCreatedAt() {
          return created_at;
      }
      
      public String getUpdatedAt() {
          return updated_at;
      }
  }
  ```

  - ユーザのリポジトリ情報を格納するクラス

  ```Java
  public class Repos {
      
      private String id;
      private String name;
      private String full_name;
      private String url;
      private String description;
  
      public String getId() {
          return id;
      }
      
      public String getName() {
          return name;
      }
      
      public String getFullName() {
          return full_name;
      }
  
      public String getUrl() {
          return url;
      }
  
      public String getDescription() {
          return description;
      }
  }
  ```

  - リポジトリのコミット情報を格納するクラス

  ```Java
  public class Commits {
      private String url;
      private String sha;
      private Commit commit;
      
      public String getUrl() {
          return url;
      }
  
      public String getSha() {
          return sha;
      }
      
      public String getMessage() {
          return commit.getMessage();
      }
      
      public String getCommiterName() {
          return commit.getCommiterName();
      }
      
      public String getCommiterEmail() {
          return commit.getCommiterEmail();
      }
      
      public String getCommitDate() {
          return commit.getCommitDate();
      }
      
      private class Commit {
          private String message;
          private Committer committer;
          
          public String getMessage() {
              return message;
          }
          
          public String getCommiterName() {
              return committer.getName();
          }
          
          public String getCommiterEmail() {
              return committer.getEmail();
          }
          
          public String getCommitDate() {
              return committer.getDate();
          }
          
          private class Committer {
              private String name;
              private String email;
              private String date;
              
              public String getName() {
                  return name;
              }
              
              public String getEmail() {
                  return email;
              }
              
              public String getDate() {
                  return date;
              }
          }
      }
  }
  ```
  
  - ポイント
      - JSON のパラメータ名と Java クラスの変数名を一緒にすると、JSON の値が該当するクラスの変数に代入される
      - @SerializedName("パラメータ名") のアノテーションをつけると、異なる変数名にした上で値を取得できる
          - [StackOverflow - what is the basic purpose of @SerializedName annotation in android using GSon](http://stackoverflow.com/questions/28957285/what-is-the-basic-purpose-of-serializedname-annotation-in-android-using-gson)
      - JSON の中で JSON がネストしている場合でも、クラスの中でクラスをネストさせてその中で JSON のパラメータと同じ変数名を定義すると、パージングされる
          - [StackOverflow - How can Retrofit 2.0 parse nested JSON object?](http://stackoverflow.com/questions/32942661/how-can-retrofit-2-0-parse-nested-json-object)
      - JSON配列を取得したい場合、クラス内の変数の型を List にする
          - [StackOverflow - Using Retrofit to access JSON arrays](http://stackoverflow.com/questions/21815008/using-retrofit-to-access-json-arrays)



### GitHub API にアクセスし、データを取得する処理の実装
 ```Java
 Retrofit retrofit = new Retrofit.Builder()
         .baseUrl("https://api.github.com")
         .addConverterFactory(GsonConverterFactory.create())
         .build();
 GitHubService service = retrofit.create(GitHubService.class);

 Call<User> userCall = service.getUser("ユーザ名");
 userCall.enqueue(new Callback<User>() {
     @Override
     public void onResponse(Call<User> call, Response<User> response) {
         
         // ユーザ情報の取得 (https://api.github.com/users/"ユーザ名" にアクセスした時のデータが取得される)
         User user = response.body();
         
         // ...任意の処理...
     }
    
     @Override
     public void onFailure(Call<User> call, Throwable t) {

         // ...失敗した時の処理...
     }
 });
 ```

- 参考
  - [Retrofit 公式](http://square.github.io/retrofit/)



## ExpandableListView を利用し、親グループ:リポジトリ一覧, 子グループ:各リポジトリのコミット一覧 の折り畳めるリストを表示する
### Java 側の実装例
- フィールド宣言

  ```Java
    /**
     * リポジトリ/コミットのリスト
     */
    @BindView(R.id.repos_commit_list) ExpandableListView mReposCommitList;
    
    
    /**
     * リポジトリ/コミットのリストの Adapter
     */
    private ReposCommitAdapter mReposCommitAdapter;
    
    private Retrofit mRetrofit;
    private GitHubService mService;
  ```

- リポジトリ一覧, 各リポジトリのコミット一覧を取得し、 ExpandListView にセットする処理の実装
  - ※冒頭に載せた gif によるデモの実装の簡略版です
  
  ```Java
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        
        View view = inflater.inflate(R.layout.retrofit_sample, container, false);
        ButterKnife.bind(this, view);
        
        mRetrofit = new Retrofit.Builder()
                .baseUrl("https://api.github.com")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        mService = mRetrofit.create(GitHubService.class);
        
        // リポジトリ一覧, 各リポジトリのコミット一覧
        final List<Repos> reposList = new ArrayList<>();
        final List<List<Commits>> commitsList = new ArrayList<>();

        // Adapter の生成
        mReposCommitAdapter = new ReposCommitAdapter(getContext(), reposList, commitsList);
        mReposCommitList.setAdapter(mReposCommitAdapter);

        // リポジトリ一覧/各リポジトリのコミット一覧の取得し、データを ExpandListView にセットする
        new AsyncTask<String, Void, Void>() {
    
            @Override
            protected Void doInBackground(String... params) {
                
                // ユーザ名
                String userName = params[0];
                
                try {
                    // GitHub API にアクセスして、ユーザの GitHub リポジトリ一覧の取得
                    Call<List<Repos>> reposCall = mService.listRepos(userName);
                    reposList.addAll(reposCall.execute().body());
                    
                    // GitHub API にアクセスして、各リポジトリのコミット一覧を取得
                    for (Repos repos : reposList) {
                        String reposName = repos.getName();
                        Call<List<Commits>> commitsCall = mService.listCommit(userName, reposName);
                        commitsList.add(commitsCall.execute().body());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                
                return null;
            }
    
            @Override
            protected void onPostExecute(Void aVoid) {
                // データ更新の通知
                mReposCommitAdapter.notifyDataSetChanged();
            }
        }.execute("Hachimori");
        
        return view;
    }
  ```

- ExpandableListView の Adapter の実装

  ```Java
    public class ReposCommitAdapter extends BaseExpandableListAdapter {
        
        private Context mContext;
        private List<Repos> mReposList;
        private List<List<Commits>> mCommitList;
        
        public ReposCommitAdapter(Context context, List<Repos> reposList, List<List<Commits>> commitList) {
            mContext = context;
            mReposList = reposList;
            mCommitList = commitList;
        }
        
        @Override
        public int getGroupCount() {
            return mReposList.size();
        }
        
        @Override
        public int getChildrenCount(int groupPosition) {
            return mCommitList.get(groupPosition).size();
        }
        
        @Override
        public Repos getGroup(int groupPosition) {
            return mReposList.get(groupPosition);
        }
        
        @Override
        public Commits getChild(int groupPosition, int childPosition) {
            return mCommitList.get(groupPosition).get(childPosition);
        }
        
        @Override
        public long getGroupId(int groupPosition) {
            return groupPosition;
        }
        
        @Override
        public long getChildId(int groupPosition, int childPosition) {
            return childPosition;
        }
        
        @Override
        public boolean hasStableIds() {
            return true;
        }
        
        /**
         * 親アイテムの取得
         */
        @Override
        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
            
            View v = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.repository_list_cell, null);
            
            // 親アイテムを取得
            Repos repos = mReposList.get(groupPosition);
            
            // リポジトリ名をセット
            ((TextView) v.findViewById(R.id.repository_name)).setText(repos.getName());
            
            // リポジトリの説明をセット
            ((TextView) v.findViewById(R.id.repository_description)).setText(repos.getDescription());
            
            // URLをセット
            ((TextView) v.findViewById(R.id.repository_url)).setText(repos.getUrl());
            
            return v;
        }
        
        /**
         * 子アイテムの取得
         */
        @Override
        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
                                 View convertView, ViewGroup parent) {
            
            View v = LayoutInflater
                    .from(mContext)
                    .inflate(R.layout.commit_list_cell, null);
            
            // 子アイテムを取得
            Commits commit = mCommitList.get(groupPosition).get(childPosition);
            
            // Commit ID をセット
            ((TextView) v.findViewById(R.id.commit_id)).setText(commit.getSha());
            
            // 名前をセット
            ((TextView) v.findViewById(R.id.committer_name)).setText(commit.getCommiterName());
            
            // Email をセット 
            ((TextView) v.findViewById(R.id.committer_email)).setText("(" + commit.getCommiterEmail() + ")");
            
            // 日付をセット
            ((TextView) v.findViewById(R.id.commit_date)).setText(commit.getCommitDate());
            
            // 説明をセット
            ((TextView) v.findViewById(R.id.commit_description)).setText(commit.getMessage());
            
            return v;
        }
        
        @Override
        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return true;
        }
    }
  ```

### レイアウトファイルの実装
- ExpandableListView のレイアウトファイル (retrofit_sample.xml の一部)

  ```xml
  <ExpandableListView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:id="@+id/repos_commit_list"/>
  ```

- リポジトリ (親グループ要素) のレイアウトファイル (repository_list_cell.xml)

  ```xml
  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="10dp">
  
      <!-- リポジトリ名 -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_marginBottom="10dp"
          android:id="@+id/repository_name"
          android:textColor="@android:color/black"
          android:textSize="24sp"/>
      
      <!-- リポジトリの説明 -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_marginLeft="10dp"
          android:layout_marginBottom="10dp"
          android:id="@+id/repository_description"
          android:textSize="18sp"/>
      
      <!-- リポジトリの URL -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:id="@+id/repository_url"
          android:textSize="12sp"/>
  </LinearLayout>
  ```

- リポジトリのコミット (子グループ要素) のレイアウトファイル (commit_list_cell.xml)

  ```xml
  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:orientation="vertical"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="30dp">
  
      <!-- Commit ID -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:id="@+id/commit_id"/>
      
      <LinearLayout
          android:layout_width="wrap_content"
          android:layout_height="wrap_content">
          
          <!-- 名前 -->
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:id="@+id/committer_name"/>
          
          <!-- Email -->
          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:id="@+id/committer_email"/>
      </LinearLayout>
      
      <!-- 日付 -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:id="@+id/commit_date"/>
      
      <!-- 説明 -->
      <TextView
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_marginTop="20dp"
          android:layout_marginLeft="20dp"
          android:id="@+id/commit_description"/>
  </LinearLayout>
  ```
  
- ExpandableListView の実装について、以下の記事が大変参考になりました
  - [ExpandableListViewクラスで折り畳めるリストを表示する](http://techbooster.jpn.org/andriod/ui/5812/)