【Android】MVVMで、Retrofit + Kotlin CoroutinesでHttp通信をやってみた
概要
MVVMの勉強のため、Retrofit2とKotlin Coroutinesを使って、Http通信をして、qiitaの記事を検索するアプリを作成しました。
ゴール
MVVMでのHttp通信の実装に少しでも参考になればと思います。
実装する機能
Qiita APIを使って、記事一覧を取得する。
文字を入力していくと検索する
今回作ってみたもの
使用するライブラリ
-
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,
)