- 投稿日:2020-11-14T22:01:41+09:00
【Android】MVVMで、Retrofit2 + Kotlin CoroutinesでHttp通信でqiitaの記事検索するやつ作った
【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.gradleapply 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.ktclass 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.ktclass 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.ktclass 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.ktinterface 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, )
- 投稿日:2020-11-14T17:23:03+09:00
[Android]Material Designを導入したときに初めにつまづいたこと
AndroidにMaterial Designを導入したときに、何度もつまづいたので、メモ。
初めての導入
build.gradleに以下を記載します。
implementation 'com.google.android.material:material:1.3.0-alpha03'Gradle Syncした後にMaterial Buttonを使ってみます。
<com.google.android.material.button.MaterialButton style="@style/Widget.MaterialComponents.Button" ... />実行、そしてクラッシュ
このボタンがあるActivityを起動すると、以下のエラーで落ちました。
Error inflating class com.google.android.material.button.MaterialButton解決方法
res/values/styles.xml
をMaterial Design用に変えないといけない。<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">↓
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">参考
https://qiita.com/susu_susu__/items/67f86582188ff28ad910
https://material.io/develop/android/docs/getting-started
- 投稿日:2020-11-14T12:00:48+09:00
【Android / Java】DataBinding について学んだ
はじめに
開発案件でDataBindingを使っており、学習したことを記事にする。
今回はDataBindingを使って、データクラスオブジェクトのプロパティの変更を監視してView表示に反映させるまでを学習した。
今回作成した学習用アプリ
①と②のテキスト表示を「チェンジ」ボタンを押す毎に動的に行ったりきたり切り替えるというもの
「ドラえもん」⇄「のびた」
①
② 実装ファイル
build.gradle(:app)
activity_main.xml
Character.java
(モデルclass)EventHandlers.java
(インターフェース)MainActivity.java
実装していく
1. DataBinding の導入
build.gradle(:app)
にdataBinding { enabled = true }
を追加build.gradleapply plugin: 'com.android.application' android { compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { applicationId "com.android.databindingjava" minSdkVersion 24 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } // 記述を追加 dataBinding { enabled = true } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }2. 変更を監視するモデルクラスを定義
Character.java// BaseObservableを継承 public class Character extends BaseObservable { private String name; public Character(String name) { this.name = name; } // getNameに@Bindableを付与することにより監視用の定数BR.nameが生成される @Bindable public String getName() { return name; } // setNameにnotifyPropertyChanged(BR.name)を付与することで // レイアウト側からBR.nameに対応するgetName()が呼ばれる(setNameされるタイミングでgetNameがレイアウト側から呼ばれる) public void setName(String name) { this.name = name; notifyPropertyChanged(BR.name); } }viewに変更を反映させるために、
BaseObservable
を継承getName
に@Bindable
をつけ監視用の定数であるBR.name
を生成するsetName
にnotifyPropertyChanged(BR.name);
を記述するこうすることで
setName
が実行されるタイミングでgetName
がレイアウト側から呼ばれ、name
の値を変更した際にviewに変更が反映されるようになる。3. データを変更するためのインターフェースを定義
EventHandlers.javapublic interface EventHandlers { // クリックイベントに対応させたいため、引数はView.OnClickListenerのonClickと同じ(View view)にする void onChangeClick(View view); }レイアウトにセットするイベントハンドラーをインターフェースとして定義
4. 変更を反映させるレイアウトファイルを作成
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <!-- ルートをlayoutにすることでDataBindingに対応したレイアウトとして認識される --> <!-- activity_main.xml => ActivityMainBinding このような形で自動的にxmlファイル名に応じたBindingクラスが作られる--> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <!-- Binding オブジェクト --> <data> <!-- この記述によりcharacterという名前(任意)で、Userクラスオブジェクトとの結びつけがされる --> <variable name="character" type="com.android.databindingjava.Character" /> <!-- この記述によりeventHandlersという名前(任意)で、ハンドラー(インターフェース)が設定される --> <variable name="eventHandlers" type="com.android.databindingjava.EventHandlers" /> </data> <!-- Views--> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/text_view_user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" android:text="@{character.name}" /> <Button android:id="@+id/button_change" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30dp" android:text="チェンジ" android:onClick="@{eventHandlers.onChangeClick}" app:layout_constraintBottom_toTopOf="@id/text_view_user_name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>レイアウトからモデルclassへのアクセス
<variable name="character" type="com.android.databindingjava.Character" />
により、レイアウトファイル内でCharacterクラスオブジェクトをcharacterという名前で定義しており、レイアウトファイル内でオブジェクトを使用できるようになる。
@{character.name}
この記述でCharacterクラスのnameプロパティにアクセスでき、android:text="@{character.name}"
によりtextにプロパティの値がセットされる。※
@{}
の中身はnullを許容するようになっており、nullの場合でもNullPointerExceptionが発生することはない。レイアウト要素にイベントハンドラーをセット
<variable name="eventHandlers" type="com.android.databindingjava.EventHandlers" />
により、レイアウトファイル内でEventHandlersインターフェースをeventHandlersという名前で定義しており、レイアウトファイル内でインターフェースにアクセスできるようになる。
@{eventHandlers.onChangeClick}
この記述でEventHandlersインターフェースのonChangeClickにアクセスでき、Button要素の中でandroid:onClick="@{eventHandlers.onChangeClick}"
を記述することによりクリックした際にonChangeClickが呼ばれるようになる。
(※ 後述するMainActivity.javaへの記述も必要)5. MainActivityでDataBinding処理を定義
MainActivity.java// EventHandlers(インターフェース) を実装 public class MainActivity extends AppCompatActivity implements EventHandlers { private Character chara = new Character("ドラえもん"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // activity_main.xml に対応したクラスの bindingインスタンスを作成 ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // activity_main.xmlのcharacterにcharaをセット binding.setCharacter(chara); // activity_main.xmlのeventHandlersにMainActivityをセット binding.setEventHandlers(this); } // button_changeのクリックイベント処理(インターフェース) @Override public void onChangeClick(View view) { // charaのnameの文字列によって、セットする文字列を変える if (chara.getName().equals("ドラえもん")) { chara.setName("のびた"); } else { chara.setName("ドラえもん"); } } }ルートを
<layout>
にしたレイアウトファイルを作成することで自動的にxmlファイル名に応じたBindingクラスが作成される。
今回であれば、 activity_main.xml => ActivityMainBinding(.java)
onCreate
の中では以下のような処理を行っている
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
でインスタンスを作成binding.setCharacter(chara);
でレイアウトファイルのcharacterにcharaをセットbinding.setEventHandlers(this);
でレイアウトファイルのeventHandlersにMainActivityをセットそしてインターフェースonChangeClickを実装し、メソッドないでTextView文字列値に応じてデータを変更する処理を書いている。
参考サイト
私のこの記事はこちらの記事をめちゃくちゃ参考にさせていただいております。
本当にわかりやすかったです!ありがとうございました!最後に
今回は簡単なアプリですが、実際の案件は規模が大きくコードを読み解くのが大変なのが現状です。
さらに学習継続していきます。誤り、ご指摘などあればコメントいただければ幸いです。
- 投稿日:2020-11-14T11:59:56+09:00
Navigation ComposeでActivityへ遷移させる
Navigation Compose で Activity への遷移をするにはどうすればいいのか調べたのでそれについて書いていこうと思います。
Navigation Compose については以前記事を書いたのでそちらをみてください。⚠️ここでの Navigation Compose のバージョンは 1.0.0-alpha02 を使用しているので、Stable になるまでに破壊的な変更が入る可能性もあります
Navigation Compose に Activity の定義を追加
val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable(route = "home") { HomeScreen(...) } composable(route = "settings") { SettingsScreen(...) } activity(id = R.id.nav_oss_licenses) { activityClass = OssLicensesMenuActivity::class } }今回は OSS Licenses Gradle Plugin の
OssLicensesMenuActivity
遷移を定義します。
id は xml に定義しておき、NavGraphBuilder
の activity() で遷移先の Activity を定義します。遷移させる
Button( onClick = { navController.navigate(R.id.nav_oss_licenses) } ) { Text(text = "Licenses") }遷移させるには
NavController#navigate
に定義したときの id を渡すだけで遷移できます。
Navigation は Kotlin DSL で定義することができ、Navigation Compose でもその方法が使えるので、Activity の遷移も Navigation Compose で行えました。
Navigation を Kotlin DSL で定義するのは公式のドキュメントがあるので参考にしてみてください。