20201114のAndroidに関する記事は4件です。

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

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

概要

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

ゴール

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

実装する機能 

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

今回作ってみたもの

画像サイズ.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,
)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.gradle
apply 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を生成する
  • setNamenotifyPropertyChanged(BR.name);を記述する

こうすることでsetNameが実行されるタイミングでgetNameがレイアウト側から呼ばれ、nameの値を変更した際にviewに変更が反映されるようになる。

3. データを変更するためのインターフェースを定義

EventHandlers.java
public 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文字列値に応じてデータを変更する処理を書いている。

参考サイト

私のこの記事はこちらの記事をめちゃくちゃ参考にさせていただいております。
本当にわかりやすかったです!ありがとうございました!

Android Databinding 〜超入門〜

最後に

今回は簡単なアプリですが、実際の案件は規模が大きくコードを読み解くのが大変なのが現状です。
さらに学習継続していきます。

誤り、ご指摘などあればコメントいただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 PluginOssLicensesMenuActivity 遷移を定義します。
id は xml に定義しておき、NavGraphBuilderactivity() で遷移先の 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 で定義するのは公式のドキュメントがあるので参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む