【Android】MVVMで、Retrofit2 + Kotlin CoroutinesでHttp通信でqiitaの記事検索するやつ作った

# 【Android】MVVMで、Retrofit + Kotlin CoroutinesでHttp通信をやってみた

## 概要
MVVMの勉強のため、Retrofit2とKotlin Coroutinesを使って、Http通信をして、qiitaの記事を検索するアプリを作成しました。

## ゴール
MVVMでのHttp通信の実装に少しでも参考になればと思います。

## 実装する機能　
Qiita APIを使って、記事一覧を取得する。
文字を入力していくと検索する

## 今回作ってみたもの
![画像サイズ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315689/b825aa6a-2dc6-f5e6-10a3-15d465c740a7.png)


## 使用するライブラリ
* AAC
  * LiveData
  * AndroidViewModel

* Retrofit2
  * Retrofitは、ver2.6以降でcoroutinesに対応したので、2.6以降を使用するように注意してください。 

* Moshi
  * サーバからのレスポンスのJSONをjavaで扱えるように変換するライブラリ 

* Kotlin Coroutines

## 実装

### セットアップ
* 依存関係
  * appのbuild.gradleに以下を追加　

``` /app/build.gradle
apply plugin: 'kotlin-kapt'

dependencies {
  // Retrofit2 & Moshi
  def retrofit_version = "2.6.2"
  implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
  implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"

  // RecyclerView
  implementation 'androidx.recyclerview:recyclerview:1.1.0'
}

```

### レイアウトの作成

メイン画面のレイアウト 

``` activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".ui.MainActivity">

    <androidx.appcompat.widget.SearchView
        android:id="@+id/search_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:queryHint="キーワード検索"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/search_view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

```

リストアイテムのレイアウト

``` list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/author_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/qiita_title"
        android:layout_toRightOf="@id/author_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/user_name"
        android:layout_toRightOf="@id/author_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>
```

### 実装

#### View

* MainActivity
* Activityでは、リサイクラービューの設定として、viewModelから取得した記事の情報を渡すなどを主にやっています。

``` MainActivity.kt
class MainActivity : AppCompatActivity() {
  // viewModel
  private val viewModel: MainViewModel by lazy {
    ViewModelProvider.AndroidViewModelFactory().create(MainViewModel::class.java)
  }

  override fun onCreate(savedInstanceState: Bundle?) {
     // searchViewの設定
     var searchView = findViewById(R.id.search_view)
　　　// searchViewに対する入力のリスナーを設定
　　　// viewModelを渡しています。
     searchView.setOnQueryTextListener(SearchViewListener(viewModel))
     searchView.setIconifiedByDefault(false)

     // recyclerViewの設定
     var layoutManager = LinearLayoutManager(activity)
     var viewAdapter = ArticleListViewAdapter()

     // viewModelのQiita記事のリストをオブザーブする
     viewModel.articles.observe(viewLifecycleOwner, Observer { it ->
         // recyclerViewのAdapterに、取得した記事の情報を渡す
         it?.let { viewAdapter.setArticles(it) }
     })

     // recyclerViewをセット
     var recyclerView = findViewById<RecyclerView>(R.id.recyclerView).also {
          it.layoutManager = layoutManager
          it.adapter = viewAdapter
     }
  }

  // searchViewのリスナークラス
  class SearchViewListener(val viewModel: SearchViewModel): OnQueryTextListener {
        // 文字が入力されたタイミングで実行される
        override fun onQueryTextChange(newText: String?): Boolean {
            viewModel.searchArticles(newText)
            return false
        }
        
        // 検索が実行されたタイミングで実行される
        override fun onQueryTextSubmit(query: String?): Boolean {
            viewModel.searchArticles(query)
            return false
        }
    }
}

```

#### ViewModel

* MainViewModel
    * viewModelでは、repositoryに対してQiita記事を取得するメソッドを実行しています。

``` MainViewModel.kt

class SearchViewModel: ViewModel() {

    var articles: LiveData<List<Article>> = MutableLiveData<List<Article>>()
    private val qiitaRepository: QiitaRepository = QiitaRepository()

    fun searchArticles() {
        viewModelScope.launch(Dispatchers.IO) {
            articles = qiitaRepository.getArticles()
        }
    }
}
```

#### Model

* QiitaRepository

``` QiitaRepository.kt

class QiitaRepository() {
    private var service: QiitaApiInterface = Retrofit.Builder()
        .baseUrl("https://qiita.com/api/v2/")
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
        .create(QiitaApiInterface::class.java)

    // Qiita記事を取得するメソッド
    fun getArticles(query: String?): List<Article>? {
        try {
            val response = service.getArticles(query).execute()
　　　　　　　// リクエストが成功した場合
            if (response.isSuccessful) {
                return response.body()
            } else { // 失敗の時は今回は実装していません。
                Log.d("QiitaRepository", "GET ERROR")
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return null
    }
}
```

``` QiitaApiInterface.kt
interface QiitaApiInterface {

    // GET 
    @GET("items")
    suspend fun getArticles(
        @Query("query") query: String?
    ): Call<List<Article>>

}

data class Article (
    val id: String,
    val title: String,
    val user: User,
)

data class User (
    val id: String,
    val name: String,
    val profile_image_url: String,
)

```