20201204のAndroidに関する記事は8件です。

AndroidのIn-App Review�を表示テストする方法

リリース前にIn-App Reviewを表示させてみたい時あるよね?

どうにか表示させようと試行錯誤した結果、どうにか表示できたので共有しておく。
(2020/12/04時点の情報)

In-App Reviewをテスト表示させる方法

表示テストをしたい場合、Play Consoleから 内部テストで配布したときのみ In-App Reviewが表示される。

もう一回言います、「内部テストで配布したときのみ」に表示される。
ローカル環境でビルドしたり、apkファイルを直接インストールしてもIn-App Reviewは表示されなかった。

内部アプリ配布経由で表示できた瞬間のスクショ

自分の個人アプリを実験台にして色々と試してみた!

仕事でのアプリは色々とあって実験に使えないため、こういう時に個人アプリがあると便利だねー
好き勝手に変更できるしw

Play Consoleの内部テストの場所

Play Consoleの左のメニューから選択できる。
初めて使うには若干設定が必要かも?
(何か設定した記憶があるが忘れた)

image.png

参考リンク

Play Core In-App Review API not showing the Review Activity
https://stackoverflow.com/questions/63286540/play-core-in-app-review-api-not-showing-the-review-activity/63289851

公式ドキュメント: Google Play In-App Review API
https://developer.android.com/guide/playcore/in-app-review?hl=ja

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

だからボクはオブジェクト指向が使いこなせない Android/Java編

はじめに

本記事は、Android/Java開発において、オブジェクト指向を使いこなしたい、という方に向けた記事です。
私が10年近くAndroidアプリ開発に関わってきて思うのが、開発現場でオブジェクト指向らしいコードを書けるプログラマーは、残念ながら全体の2割にも満たない、ということです。(あくまで私個人の感想です)。
逆に言えば、オブジェクト指向らしいコードを書けるようになるだけで、開発現場で重宝される人材になれます。Androidの最新技術やKotlinの勉強をすることも大切ですが、開発現場でより即効性があり、かつ、Android以外のプログラミングにも応用できる、潰しのきく技術、それがオブジェクト指向らしいコードを書く技術です。

どうしたらオブジェクト指向らしいコードを書く技術を身につけられる?

※具体的な技術的な話をすぐに読みたい方は「開発現場」の段落までスキップしてください。
昔、先輩社員に「どうしたらオブジェクト指向らしいコードを書く技術を身につけられるか」を聞いたところ「開発経験を積むこと」というあいまいな答えをもらい悩んだことがあります。この「開発経験」は自分にとって余程都合が良いプロジェクトでないと意味がありません。テスターとしてプロジェクトに参画するのはもちろん、他者が書いたコードの拡張や改修する役割で参画してもあまりよい経験にはなりません(AndroidのAPIの使い方を知るという意味では意味がありますが)。自分でソフト構成を設計できるような新規案件や大きな機能ブロックを作成できる案件でないとなかなか「オブジェクト指向らしいコードを書く技術」を磨くチャンスがないのです。しかし、多くの仕事内容は、他者が書いたコードを他者が書いたコードの拡張や改修する作業です。
オブジェクト指向らしいコードを書く技術を磨くには、自分で技術の本や記事を積極的に読み、実際に自分でコードを書き、頭をトレーニングする必要があります。

おすすめのトレーニング方法

「オブジェクト指向らしいコードを書く技術」を身につける為におすすめなのが「デザインパターン」と「リファクタリング」の技術を身につけることです。どちらもオブジェクト指向らしいコードを身につける為の要素がたくさん詰まっています。Android/Javaプログラマーは比較的幸運だと思います。なぜなら、結城 浩さんの良書中の良書があるからです。

私もこれらの本を読んだのがきっかけで、オブジェクト指向らしくコードを書こうとするようになりました。ただ良書ではあるのですが、どちらもそれなりにページ数が多い為、はじめは読むのに尻込みしてしまうかもしれません。そこで、初心者でも取っつきやすい内容を目指して本稿を書きました。

デザインパターン

デザインパターンについて、もう少し掘り下げます。デザインパターンはGoFの23パターンが有名で、上記のデザインパターン本もGoFの23パターンをベースにした本になっています。私個人の意見としては、最初は23種全部覚えるよりも、重要ないくつかのパターンを使いこなすのに時間を掛けた方がよいと思います。次のパターンを優先的にマスターするのがおすすめです。

  • Template Methodパターン
  • Factory Methodパターン
  • Facadeパターン
  • Stateパターン
  • Builderパターン
  • Observerパターン

この中で最初に覚えた方がよいパターンは「Template Method パターン」です。プログラミングのできる方だと、わざわざデザインパターンの名前も付ける必要もないくらいオブジェクト指向の性質そのものだと言う方もいると思います。それくらい「基本でかつ奥義」みたいなパターンです。この「Template Method パターン」について本記事を通して学んでいきましょう。

開発現場

※筆者注:導入部は物語風会話形式で書いてみました。
とあるソフト開発会社「エゥーゴソフト」開発チームの二人の会話から始まります。

登場人物説明

  • ブライト(リーダー):社会人(&開発経験)5年目。新人の頃からAndroid/Java一筋で、厳しい先輩に教わったこともあり、現在では新規アプリ開発プロジェクトの立ち上げを任されている。
  • アムロ(新人女性社員):大学生の頃は主にC言語でプログラミングしていた。Java言語は入門書の7割くらいは読んだが、正直オブジェクト指向と言われてもピンとこない。新人研修のときに初めてAndroidの作り方を学んだ。

ブライトさんの作成依頼

ブライトリーダー:アムロさん、こんな感じのAndroidアプリを作ってほしいんだ。(といって下記のイメージを見せる)

<アプリ仕様>
スライド1.PNG

  • アプリを起動すると背景が青い画面を表示し、中央に「次」ボタンを表示すること
  • 「次」ボタンを押下すると、背景が黄色の画面->赤い画面->青い画面と切り替わること
  • Backキーを押下するとアプリが終了すること(Androidアプリでデフォルトの動作のままでよい)

ブライトリーダー:アムロさんはAndroidを新人研修で教わったみたいだけど、いきなり全部作れって言うのは厳しいと思うから、途中まで(青い画面を表示して、ボタンが押された時のイベント処理をどう書けばよいのかがわかるところまで)作ってあるから、これをベースに作ってみて。
アムロ:わかりました。作ってみます。

ベースとなるコード(Javaファイル以外は省略)

サンプル:ColorSample_base
※サンプルはGithubに格納してあります。

BlueActivity.java
package com.example.colorsample;

import android.graphics.Color;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class BlueActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.layout).setBackgroundColor(Color.BLUE);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
        // 別のActivityに遷移する時の処理イメージ
//                Intent intent = new Intent(BlueActivity.this, YellowActivity.class);
//                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
//                startActivity(intent);
            }
        });
    }
}

。。。。
アムロさんはAndroid Studioの操作に苦戦しつつも、アプリを作り上げました。
アムロ:できました。こんな感じでどうでしょう。(ブライトさんにAndroid端末の画面を見せながら操作する)
ブライトリーダー:いいね。動作は完璧だね。ソースコードの方を見せてもらえる?

アムロさんが作ったコード
サンプル:ColorSample_amuro

BlueActivity.java
package com.example.colorsample;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;

import androidx.appcompat.app.AppCompatActivity;

public class BlueActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.layout).setBackgroundColor(Color.BLUE);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(BlueActivity.this, YellowActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });
    }
}
YellowActivity.java
package com.example.colorsample;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;

import androidx.appcompat.app.AppCompatActivity;

public class YellowActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.layout).setBackgroundColor(Color.YELLOW);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(YellowActivity.this, RedActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });
    }
}

RedActivity.java
package com.example.colorsample;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class RedActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.layout).setBackgroundColor(Color.RED);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(RedActivity.this, BlueActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
            }
        });
    }
}

ブライトリーダー:(そうきたか。。。)うーん。残念だけどお仕事の成果物としては、このままのコードではダメだね。
アムロ:えー。ボクのコードの何がダメなんですか。ちゃんと仕様通り動いているじゃないですか。
ブライトリーダー:BlueActivity.javaとYellowActivity.javaとRedActivity.javaってなんか「とてもよく似ている」よね。
アムロ:まあ、BlueActivity.javaをコピペしてYellowActivityクラスとRedActivityクラスを作って、画面の色とか遷移先のActivityクラスのコードを修正したので。
ブライトリーダー:(コピペしたって言っちゃった。正直というか。。。)似たようなコードがあるってことはもっとコードを少なくして、スッキリさせることができるはずなんだ。コードで説明するとわかりずらいから、日本語に置き換えて説明しよう。


3つのクラスの動作を日本語で表現すると、次のようになります。
スライド2.PNG

この中で、クラス毎に異なる部分を赤字にします。
スライド3.PNG
(1)は3つのクラスとも同じですね。(2)と(3)は赤字以外の部分は同じです。
この同じ部分(重複部分)が「無駄」と言えます。無駄な部分を取り消し線で書きます。
スライド4.PNG
この取り消し線の部分がなくなったら、スッキリしたコードになると思いませんか。そこで次のように、赤字の部分を〇〇と△△に置き換えます。

スライド5.PNG

3つのクラスとも同じ内容になりました。
同じ内容なら1つのクラスで表現すればいいですよね。そこで3つのクラスのスーパークラス(親クラス)というものを考えてみます。
スライド6.PNG

これをソースコードで表現すれば、スッキリしたコードになります(スーパークラスのクラス名をColorActivityとします)。
青色やYelloActivityのような情報を〇〇や△△に置き換えることを「抽象化」と言います。抽象化した情報を具体化するのはサブクラス(子クラス)側に任せ、スーパークラスは決めません。ソースコードを見ていきましょう。

サンプル:ColorSample_final

ColorActivity.java
package com.example.colorsample;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

abstract public class ColorActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); //(1)

        findViewById(R.id.layout).setBackgroundColor(getMyColor()); //(4)

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ColorActivity.this, getNextClass()); //(5)
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);

            }
        });
    }

    /**
     * 自Activityの色を取得する
     * @return 自Activityの色
     */
    abstract protected int getMyColor(); //(2)

    /**
     * 次に遷移するActivity Classを取得する
     * @return 次に遷移するActivity Class
     */
    abstract Class getNextClass(); //(3)
}

「(1)レイアウトを「activity_main.xml」に設定する」については、3つのクラスで同じ内容でした。同じならば、親クラスで1回書けば、子クラスに書く必要はなくなります。
getMyColor()やgetNextClass()はabstractが付いています。ColorActivityはどんな色か、どんなクラスかを具体的に決めません。決めるのはサブクラスに任せます。色や次のクラスを取得し、それを(4)や(5)で使用することだけをスーパークラスで書きます。
サブクラス(子クラス)のソースコードも見ていきましょう。

BlueActivity.java
package com.example.colorsample;

import android.graphics.Color;

public class BlueActivity extends ColorActivity {

    @Override
    protected int getMyColor() {
        return Color.BLUE;
    }

    @Override
    Class getNextClass() {
        return YellowActivity.class;
    }
}

YellowActivity.java
package com.example.colorsample;

import android.graphics.Color;

public class YellowActivity extends ColorActivity {

    @Override
    protected int getMyColor() {
        return Color.YELLOW;
    }

    @Override
    Class getNextClass() {
        return RedActivity.class;
    }
}

RedActivity.java
package com.example.colorsample;

import android.graphics.Color;

public class RedActivity extends ColorActivity {

    @Override
    protected int getMyColor() {
        return Color.RED;
    }

    @Override
    Class getNextClass() {
        return BlueActivity.class;
    }
}

クラス図でクラス関係を整理しておきます。
class.png


アムロ:なるほど。たしかにスッキリした感じがしますね。BlueActivityとか差分の情報しかないって感じがすごくします。サブクラスは、Android固有のメソッドが1つも出てこないんですね。。。でもこのようなコードに改造するのは大変な気がします(というかメンドクサイ)。なんでこんなことをやる必要があるのでしょうか。
ブライトリーダー:簡単に言えば
(1)「バグの量」を減らすこと
(2)クラスの役割分担を明確する
を行うことでメンテナンスしやすくなるんだ。メンテナンスしやすければ「楽に」働ける。
(1)は、ソースコードを人が書く以上、書く量が多ければ多い程ミスが出て、バグが生まれる可能性が高くなる、という考え方から来ている。だから基本的にコード量は少ない方がいいんだ。(Javaのコード量で開発工数の見積もりをしようとお客さんがいるけど、正直コード量で見積もりするのはナンセンスだよな。コピペ文化を増長するし。)
(2)は、日本語の赤字をつけた例を見ればわかると思うけど、本来BlueActivityやYelloActivityは「何色か」、「次のAcitvityクラスは何か」の2つを決めるだけでいいはずなのに黒字と赤字が混ざっている。言い換えれば、Android固有のメソッド(API)を呼ぶコード(setContentView()やstartActivity())と呼ばなくてもよいコードが混ざっている。例えば、AndroidのVersionが上がってAPIの仕様変更があって動かなくなったときにどこを修正すればいいのか調査する必要が出てきたとする。この場合、アムロさんのコードでは、BlueActivity、YelloActivity、RedActivityの3つのクラスの調査する必要がある。
<ColorSample_amuroのソフト構成>
スライド7.PNG

それに比べてColorSample_finalサンプルは、ColorActivityだけを(基本的に)調査すればいいんだ。
<ColorSample_finalのソフト構成>
スライド8.PNG

アムロさんには、ぜひ「ColorSample_final」のようなコードが書けるようになってほしいね。
アムロ:わかりました。がんばります!

Template Method パターンの理解を深める為のQ&A

  • Q.Template Method パターンとは、何かを取得する処理を抽象化するパターンのことである。Yes or No
    • A. No。抽象化の対象は動作(動詞)、つまりメソッドのことです。ただし、〇〇を取得するというメソッドの例が、Template Method パターンを理解する上で一番わかりやすい例だとは思います。
  • Q. 「Parent parent = new Child();」のようなコードが出てこないんだけど、これって本当にTemplate Method パターンのサンプル?
    • A. 今回のサンプルでは、「Parent parent = new Child();」の部分をAndroidが行っています。Template Method パターンの例としては、微妙かもしれませんが、メソッドを抽象化する感覚を感じて頂ければと思います。プログラミング言語の入門書を読んだだけだと、なかなか「abstract」や「interface」を自分で使ってみようという気にならないと思います。Template Method パターンを通じて、「abstract」や「interface」を自分が書いているコードに適用できないか考えてみましょう。

あとがき

ここまで読んでいただきありがとうございます。
本記事はできるだけ「オブジェクト指向の用語の説明」に傾注しないように注意して書きました。もともと数年前に、本屋で見かけたJava言語向けのオブジェクト指向の本が、あまりに現場の感覚と離れていて「使えない」と思ったのがきっかけでした。オブジェクト指向の用語を絵とか書いて(インスタンスとは~をハンコの絵で描いて説明している)伝えようとしているのですが、数学の公式をマスターするのに学校の先生が一生懸命黒板に書いても説明しても生徒が公式をマスターできないように、オブジェクト指向も自分で手と頭を使って初めて習得できるものだと思います。
本記事が少しでもオブジェクト指向の理解と実戦で活かせる助けになれば幸いです。

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

【Kotlin】Navigation Component Architectureで安全に遷移する方法

この記事はKotlin Advent Calendar 2020の4日目の記事です。
Androidアプリ開発初心者ですが、空きがあったので勢いだけで参加してみました。

はじめに

Navigation Architecture Componentを使ってアプリを作成した際、
素早く操作した場合や戻るボタンの連打などで、NavControllerと表示されているFragmentがズレてしまい、エラー(IllegalArgumentException)が頻発しました。
これからライブラリ自体のアップデートで解消されていく可能性はありますが、現時点では、自前で対策する必要があります。

普通の画面遷移

MainFragmentからSubFragmentへの遷移
findNavController().navigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
SubFragmentからMainFragmentへ戻る場合の遷移
findNavController().popBackStack()
// または
findNavController().popBackStack(R.id.MainFragment, false)

特にpopBackStack()を使った場合は、連打するとNavController内で管理されているBackStackがズレやすくなります。(戻りすぎる)

対策済み

Extensionを利用しFragmentに画面遷移用のメソッドを生やします。
(画面遷移前にヒストリとの整合性確認を行う処理を追加しています)

FragmentExtension.kt
package com.sample.extensions

import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.navigation.*
import androidx.navigation.fragment.DialogFragmentNavigator
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.findNavController

/**
 * ==============================================
 *  Fragment Extensions
 * ==============================================
 */
/**
 * 現在のFragmentがヒストリの最新と一致しているか
 */
fun Fragment.isCurrentDestination(): Boolean {
  // ヒストリから現在のFragmentの情報を取得
  val currentDestination: NavDestination = findNavController().currentDestination ?: return false

  when (currentDestination) {
    is DialogFragmentNavigator.Destination -> {
      if (currentDestination.className != this.javaClass.name) {
        // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
        return false
      }
      return true
    }
    is FragmentNavigator.Destination -> {
      if (currentDestination.className != this.javaClass.name) {
        // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
        return false
      }
      return true
    }
    else -> {
      return false
    }
  }
}

/**
 * safeNavigate
 */
fun Fragment.safeNavigate(
  resId: Int,
  args: Bundle? = null,
  navOptions: NavOptions? = null,
  navigatorExtras: Navigator.Extras? = null
) {
  if (!isCurrentDestination()) {
    return
  }
  findNavController().apply {
    navigate(resId, args, navOptions, navigatorExtras)
  }
}

/**
 * safeNavigate
 */
fun Fragment.safeNavigate(
  directions: NavDirections,
  navOptions: NavOptions? = null
) {
  safeNavigate(directions.actionId, directions.arguments, navOptions)
}

/**
 * safePopBackStack
 */
fun Fragment.safePopBackStack() {
  if (!isCurrentDestination()) {
    return
  }
  forcePopBackStack()
}

/**
 * forcePopBackStack
 * note:ヒストリと現在表示されているFragmentの一致チェックなしで戻る処理を行う
 *      DialogFragmentを表示元のFragmentから非表示にする場合などの利用を想定
 */
fun Fragment.forcePopBackStack() {
  findNavController().apply {
    val navBackStackEntry : NavBackStackEntry =  previousBackStackEntry ?: return
    // ヒストリの1つ前の画面に戻る
    // popBackStack()だと、連打により2つ前の画面まで戻ってしまいヒストリがおかしくなることがある
    popBackStack(navBackStackEntry.destination.id, false)
  }
}

/**
 * safePopBackStack
 */
fun Fragment.safePopBackStack(
  destinationId: Int,
  inclusive: Boolean
) {
  if (!isCurrentDestination()) {
    return
  }
  findNavController().apply {
    popBackStack(destinationId, inclusive)
  }
}
MainFragmentからSubFragmentへの遷移
safeNavigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
safeNavigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
SubFragmentからMainFragmentへ戻る場合の遷移
safePopBackStack()
// または
safePopBackStack(R.id.MainFragment, false)

さいごに

NavigationComponentでのIllegalArgumentExceptionを検索してみると、
ボタン連打防止処理や、navigate前のヒストリとの整合性確認がよく出てきますが、
端末の戻るボタンでNavControllerを操作(popBackStack)している場合は、
popBackStackにも対策を行う必要がありました:cold_sweat:

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

【Kotlin】Navigation Architecture Componentで安全に遷移する方法

この記事はKotlin Advent Calendar 2020の4日目の記事です。
Androidアプリ開発初心者ですが、空きがあったので勢いだけで参加してみました。

はじめに

Navigation Architecture Componentを使ってアプリを作成した際、
素早く操作した場合や戻るボタンの連打などで、NavControllerと表示されているFragmentがズレてしまい、エラー(IllegalArgumentException)が頻発しました。
これからライブラリ自体のアップデートで解消されていく可能性はありますが、現時点では、自前で対策する必要があります。

普通の画面遷移

MainFragmentからSubFragmentへの遷移
findNavController().navigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
SubFragmentからMainFragmentへ戻る場合の遷移
findNavController().popBackStack()
// または
findNavController().popBackStack(R.id.MainFragment, false)

特にpopBackStack()を使った場合は、連打するとNavController内で管理されているBackStackがズレやすくなります。(戻りすぎる)

対策済み

Extensionを利用しFragmentに画面遷移用のメソッドを生やします。
(画面遷移前にヒストリとの整合性確認を行う処理を追加しています)

FragmentExtension.kt
package com.sample.extensions

import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.navigation.*
import androidx.navigation.fragment.DialogFragmentNavigator
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.findNavController

/**
 * ==============================================
 *  Fragment Extensions
 * ==============================================
 */
/**
 * 現在のFragmentがヒストリの最新と一致しているか
 */
fun Fragment.isCurrentDestination(): Boolean {
  // ヒストリから現在のFragmentの情報を取得
  val currentDestination: NavDestination = findNavController().currentDestination ?: return false

  when (currentDestination) {
    is DialogFragmentNavigator.Destination -> {
      if (currentDestination.className != this.javaClass.name) {
        // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
        return false
      }
      return true
    }
    is FragmentNavigator.Destination -> {
      if (currentDestination.className != this.javaClass.name) {
        // ヒストリ上の現在のFragmentと画面遷移イベントが発生したFragmentが不一致
        return false
      }
      return true
    }
    else -> {
      return false
    }
  }
}

/**
 * safeNavigate
 */
fun Fragment.safeNavigate(
  resId: Int,
  args: Bundle? = null,
  navOptions: NavOptions? = null,
  navigatorExtras: Navigator.Extras? = null
) {
  if (!isCurrentDestination()) {
    return
  }
  findNavController().apply {
    navigate(resId, args, navOptions, navigatorExtras)
  }
}

/**
 * safeNavigate
 */
fun Fragment.safeNavigate(
  directions: NavDirections,
  navOptions: NavOptions? = null
) {
  safeNavigate(directions.actionId, directions.arguments, navOptions)
}

/**
 * safePopBackStack
 */
fun Fragment.safePopBackStack() {
  if (!isCurrentDestination()) {
    return
  }
  forcePopBackStack()
}

/**
 * forcePopBackStack
 * note:ヒストリと現在表示されているFragmentの一致チェックなしで戻る処理を行う
 *      DialogFragmentを表示元のFragmentから非表示にする場合などの利用を想定
 */
fun Fragment.forcePopBackStack() {
  findNavController().apply {
    val navBackStackEntry : NavBackStackEntry =  previousBackStackEntry ?: return
    // ヒストリの1つ前の画面に戻る
    // popBackStack()だと、連打により2つ前の画面まで戻ってしまいヒストリがおかしくなることがある
    popBackStack(navBackStackEntry.destination.id, false)
  }
}

/**
 * safePopBackStack
 */
fun Fragment.safePopBackStack(
  destinationId: Int,
  inclusive: Boolean
) {
  if (!isCurrentDestination()) {
    return
  }
  findNavController().apply {
    popBackStack(destinationId, inclusive)
  }
}
MainFragmentからSubFragmentへの遷移
safeNavigate(R.id.action_mainFragment_to_subFragment)
// パラメータを渡す場合
safeNavigate(MainFragmentDirections.actionMainFragmentToSubFragment(params = params))
SubFragmentからMainFragmentへ戻る場合の遷移
safePopBackStack()
// または
safePopBackStack(R.id.MainFragment, false)

さいごに

NavigationComponentでのIllegalArgumentExceptionを検索してみると、
ボタン連打防止処理や、navigate前のヒストリとの整合性確認がよく出てきますが、
端末の戻るボタンでNavControllerを操作(popBackStack)している場合は、
popBackStackにも対策を行う必要がありました:cold_sweat:

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

Flow 版ズンドコキヨシを Android で使用する

ACCESS Advent Calendar 2020 5日目の記事です。
と言いつつ、内容は過去に自分の書いた記事の続編です。
昔流行ったズンドコキヨシを通じて Coroutines Flow の良さを考えていきます。

やりたいこと

前回、Coroutines Flow を使用してジェネリックなズンドコキヨシを実装しました。
検証はコンソール環境で行いましたが、せっかく Kotlin で実装したので Android でも使用してみたいと思います。
Android 向け Coroutines にはFlowLiveDataに変換するasLiveData拡張関数が用意されています。
これを使用しつつ、その利点を追求していきます。

再掲 : Flow 版ジェネリック・ズンドコキヨシ

GenericZDK.kt
fun <T> randomFlow(source: List<T>) = flow<T> {
    while (true) {
        emit(source.shuffled().first())
    }
}

fun <T> terminateIfSatisfiedFlow(
    scanned: List<T>,
    pattern: List<T>,
    terminal: T
): Flow<T?> =
    if (scanned == pattern) {
        flowOf(scanned.last(), terminal, null)
    } else {
        flowOf(scanned.last())
    }

@FlowPreview
@ExperimentalCoroutinesApi
fun <T> genericZDK(
    source: List<T>,
    pattern: List<T>,
    terminal: T
): Flow<T> =
    randomFlow(source)
        .scan(emptyList<T>()) { list, e -> (list + e).takeLast(pattern.size) }
        .dropWhile { it.isEmpty() }
        .flatMapConcat { scanned -> terminateIfSatisfiedFlow(scanned, pattern, terminal) }
        .takeWhile { it != null }
        .filterNotNull()

Android でもズンドコしたい

バージョン情報

こちら
  • Android Studio 4.2 Canary 15
  • org.jetbrains.kotlin:kotlin-stdlib:1.4.10
  • org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0
  • androidx.core:core-ktx:1.3.2
  • androidx.fragment:fragment-ktx:1.2.5
  • androidx.lifecycle:lifecycle-livedata-ktx:2.2.0
  • androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0

asLiveDataについて

Flow<T>から収集(collect)された値を保持するLiveData<T>を作成します。
定義を確認しましょう。

fun <T> Flow<T>.asLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T>

asLiveDataで生成されたLiveDataは Cold の性質を持ちます。
つまり、誰かがobserveしてLiveDataがアクティブな状態になった時に上流Flowが起動します。
Flowが完了する前にLiveDataが非アクティブになると、timeoutInMsで指定した時間経過後にFlowをキャンセルします(デフォルトは5秒)。
キャンセル後にLiveDataが再びアクティブになると、Flowが再起動します。

なぜこんなことを説明しているのかというと、この性質がズンドコキヨシをAndroidで利用する上で重要だからです。
ズンドコキヨシはキ・ヨ・シ!が出るまで無限ループでズン ドコを放出し続けますが、キ・ヨ・シ!が出る前にユーザが飽きてしまい、アプリを閉じたりスリープした場合にどうなるかを考える必要があります。
Flow.asLiveDataならこのような問題にも容易に対処することができます。

ではまず、asLiveDataを使用してViewModelを実装します。

ViewModel

genericZDKでズンドコフローを取得したら、onEach内でdelayしてテキストの更新間隔を調整します。
上流Flowはメインスレッドで動作させたくないので、flowOnでデフォルトディスパッチャを指定します。
ViewModelasLiveDataを使用する場合はviewModelScope.coroutineContextを指定するのが良いでしょう。
間違ってもGlobalScopeを指定してはいけません。
ズンドコが止まらなくなる恐れがあります。

ZDKViewModel.kt
@ExperimentalCoroutinesApi
@FlowPreview
class ZDKViewModel : ViewModel() {
    companion object {
        private const val ZUN = "ズン"
        private const val DOKO = "ドコ"
        private const val KI_YO_SHI = "キ・ヨ・シ!"
        private val SOURCE = listOf(ZUN, DOKO)
        private val PATTERN = listOf(ZUN, ZUN, ZUN, ZUN, DOKO)
        private const val INTERVAL_MS = 600L
    }

    val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI)
        .onEach { delay(INTERVAL_MS) }
        .flowOn(Dispatchers.Default)
        .asLiveData(viewModelScope.coroutineContext)
}

View (Fragment)

Data Binding を使っても良いですが、大した規模ではないので今回は採用しません。
やることは単純で、ViewModelで定義したzdkプロパティをTextViewにバインドするだけです。

MainFragment.kt
@FlowPreview
@ExperimentalCoroutinesApi
class MainFragment : Fragment() {

    private val viewModel by viewModels<ZDKViewModel>()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.main_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel.zdk.observe(viewLifecycleOwner) {
            message.text = it
        }
    }
}

レイアウト(ほぼテンプレ通り)
main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainFragment">

    <TextView
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="60dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="MainFragment" />

</androidx.constraintlayout.widget.ConstraintLayout>

zdk0.gif

問題点

見てわかる通り、このままではズン(あるいはドコ)が連続で流れてきた時にテキストが更新されたことを目視で確認できないため、テンポ感を掴みにくいです。
いくつかの解決策を模索していきましょう。
主題に沿うため、すべてFlowのオペレータを使って解決します。

解決策1 : 点滅させる

flatMapConcatを使ってテキストを点滅させてみましょう。
flatMapConcatは流れてきた値を別のFlowに変換し、各Flowを順番通りに連結します(Rx のconcatMapに相当します)。

ZDKViewModel.kt
    val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI)
        .flatMapConcat {
            flow {
                emit(it)
                delay(INTERVAL_MS * 2 / 3)
                if (it != KI_YO_SHI) {
                    emit("")
                    delay(INTERVAL_MS / 3)
                }
            }
        }
        .flowOn(Dispatchers.Default)
        .asLiveData(viewModelScope.coroutineContext)

まず流れてきた値をそのままemitし、一定時間置いた後に空文字列をemitすることで点滅を表現します。
ただし最後のキ・ヨ・シ!は消したくないのでその条件を付与します。

zdk1.gif

解決策2 : 左右に振る

いまいち躍動感に欠けるので別の方法を模索します。
テキストを左右に振ってみましょう。
といってもTextViewの座標をいじるのは面倒なので、前後にスペースを挿入することにより擬似的に揺れを表現します。

ZDKViewModel.kt
    val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI)
        .withIndex()
        .map {
            when {
                it.value == KI_YO_SHI -> it.value
                it.index % 2 == 0 -> "${it.value}    "
                else -> "    ${it.value}"
            }
        }
        .onEach { delay(INTERVAL_MS) }
        .flowOn(Dispatchers.Default)
        .asLiveData(viewModelScope.coroutineContext)

withIndexFlow<T>を、インデックス情報を付与したFlow<IndexedValue<T>>に変換します。
mapを使用して、偶数回なら後ろにスペースを、奇数回なら前にスペースを挿入します。
ただし、最後のキ・ヨ・シ!は常にセンターに表示するものとします。

zdk2.gif

解決策3 : ランダムに文字サイズ(あるいは文字色)を変える

ランダムに文字サイズや文字色を変えることが出来ればより華やかになるでしょう(?)。
これを実現するために、前回のズンドコキヨシで実装したrandomFlow関数を再利用します。
以下は文字サイズを変更する例です。

ZDKViewModel.kt
    // random text size factor (0.5f ~ 1.0f)
    private val randomSizeFlow = randomFlow((50..100 step 10).toList())
        .distinctUntilChanged()
        .map { it / 100f }

    private val zdk = genericZDK(SOURCE, PATTERN, KI_YO_SHI)
        .zip(randomSizeFlow) { text, size ->
            text to if (text == KI_YO_SHI) 1.1f else size
        }
        .onEach { delay(INTERVAL_MS) }
        .flowOn(Dispatchers.Default)
        .asLiveData(viewModelScope.coroutineContext) // LiveData<Pair<String, Float>>

    val zdkText = zdk.map { it.first }
    val zdkSizeFactor = zdk.map { it.second }

サイズ(の倍率)候補からランダムに1つを選び続けるFlowrandomFlowで作成します。
ただし、前回とは必ず異なるサイズを選択したいので、distinctUntilChangedをつけます。

ズンドコフローとサイズフローの2本をzip関数で1本のFlowにまとめます。川が合流するイメージです。
ここでテキストとサイズ倍率のPairをつくります。
ただし、最後のキ・ヨ・シ!は固定の特大サイズ(1.1倍)で表示するものとします。
最後にLiveDatamapでテキストとサイズ倍率に分解します(これは必須ではありません)。
こうすることでテキストと文字サイズの更新タイミングを揃えることができます。

Fragmentを以下のように書き換えたら完成です。

MainFragment.kt
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel.zdkText.observe(viewLifecycleOwner) {
            message.text = it
        }
        val textSize = message.textSize
        viewModel.zdkSizeFactor.observe(viewLifecycleOwner) {
            message.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize * it)
        }
    }

zdk3.gif

ズンドコキヨシはバッテリーに優しいか

ズンドコキヨシは無限ループを使用しています。
冒頭で述べたとおり、もしアプリ終了やスリープしても無限ループが止まらなかったら困りものです。
バッテリーに優しくないズンドコキヨシという烙印を押されてしまいます。
randomFlow関数に以下のようにログを仕込み、この点について検証してみましょう。

GenericZDK.kt
fun <T> randomFlow(source: List<T>) = flow<T> {
    try {
        while (true) {
            emit(source.shuffled().first().also { Log.i("ZUNDOKO", "emit $it") })
        }
    } catch (e: Throwable) {
        Log.i("ZUNDOKO", "Thrown $e")
        throw e
    }
}

「解決策3」の実装で動作を確認します。
アプリ起動したら、キ・ヨ・シ!が出現する前に端末をスリープ状態にします。

Logcat
2020-11-21 22:34:29.657 22142-22176/com.example.zdk I/ZUNDOKO: emit ドコ
2020-11-21 22:34:29.659 22142-22176/com.example.zdk I/ZUNDOKO: emit 70
2020-11-21 22:34:30.301 22142-22176/com.example.zdk I/ZUNDOKO: emit ズン
2020-11-21 22:34:30.303 22142-22176/com.example.zdk I/ZUNDOKO: emit 70
2020-11-21 22:34:30.303 22142-22176/com.example.zdk I/ZUNDOKO: emit 90
2020-11-21 22:34:30.945 22142-22176/com.example.zdk I/ZUNDOKO: emit ズン
2020-11-21 22:34:30.947 22142-22502/com.example.zdk I/ZUNDOKO: emit 60
2020-11-21 22:34:31.561 22142-22176/com.example.zdk I/ZUNDOKO: emit ドコ
2020-11-21 22:34:31.565 22142-22502/com.example.zdk I/ZUNDOKO: emit 50
2020-11-21 22:34:32.171 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン
2020-11-21 22:34:32.173 22142-22502/com.example.zdk I/ZUNDOKO: emit 80
2020-11-21 22:34:32.782 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン
2020-11-21 22:34:32.785 22142-22176/com.example.zdk I/ZUNDOKO: emit 90
2020-11-21 22:34:33.411 22142-22502/com.example.zdk I/ZUNDOKO: emit ズン
2020-11-21 22:34:33.413 22142-22502/com.example.zdk I/ZUNDOKO: emit 60
2020-11-21 22:34:33.965 22142-22502/com.example.zdk I/ZUNDOKO: Thrown kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@53327ff
2020-11-21 22:34:33.965 22142-22176/com.example.zdk I/ZUNDOKO: Thrown kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@53327ff

「解決策3」では2本のランダムフローが動作していますが、そのどちらもキャンセルされていることがわかります。
これはLiveDataが非アクティブになることで未完了の上流Flowがキャンセルされるためです。
なお、アプリ(Activity)を閉じた場合はviewModelScopeが閉じられることによりJobCancellationExceptionがスローされ、やはりキャンセルされます。
これにより、バッテリーに優しいズンドコキヨシであることが示されました。

FlowLiveDataの組み合わせでは、このようにライフサイクルのケアが手厚く行われる点が大きな利点といえます。
また、mapdistinctUntilChanged程度しかオペレータの無かったLiveDataと違い、Flowには豊富なオペレータが用意されているので、両者を組み合わせることで実装の幅が大きく広がると思います。

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

xamarin-forms-tab-badgeでバッジ表示してみる(Xamarin, Android)

はじめに

モバイルアプリケーションを開発するときに、Xamarinを採用することもあると思います。
Xamarinについての説明はここでは割愛し、xamarin-forms-tab-badgeを利用して、通知のバッジを表示することに関して記載します。
ライブラリのドキュメントに使い方があるのでそれで十分なのですが、日本語の情報があまり見当たらなかったので、何かの参考になれば嬉しいです。
(なお、Androidエミュレータでしか確認していないため、iOSでは未確認です。)

■ 参考
Xamarin:Xamarin(ザマリン) とはなんぞや
xamarin-forms-tab-badge:https://github.com/xabre/xamarin-forms-tab-badge

完成イメージ

image.png
通知タブに赤丸でバッジが表示される。
タブの位置やスタイルに関しての実装については割愛します。
参考:TabbedPage

実装

注:タブ機能の簡易な実装方法として、Xamarin.Forms Shellというものがありますが、こちらを利用するとバッジ表示ができないようなので、TabbedPageを利用します。

xamarin-forms-tab-badgeのインストール

VisualStudioのソリューションエクスプローラ上でプロジェクトを右クリック>NuGetパッケージ管理をクリック
plugin.png
Plugin.Badgeを検索し、[インストール]ボタンをクリック

AssemblyInfo.csを修正

[プロジェクト名].Android/Properties/AssemblyInfo.cs を開き、以下を追加

AssemblyInfo.cs
[assembly: ExportRenderer(typeof(TabbedPage), typeof(BadgedTabbedPageRenderer))]

必要に応じて、usingを追加

TabbedPageの実装

TabbedPageのサブクラスをMainTabPage(任意)として実装する。
バッジを付けたい項目に属性としてplugin:TabBadge.BadgeText="{Binding NotificationCount}"を追加

MainTabPage.xaml
    <NavigationPage Title="通知" 
                    IconImageSource="{FontImage FontFamily={StaticResource MaterialFontFamily}, Glyph={Static icons:MaterialIcon.Bell}, Size=50}"
                    plugin:TabBadge.BadgeText="{Binding NotificationCount}">
        <x:Arguments>
            <local:NotificationListPage />
        </x:Arguments>
    </NavigationPage>

コードビハインドにバッジを変更するメソッドを用意

MainTabPage.cs
    public partial class MainTabPage : TabbedPage
    {
        public MainTabPage()
        {
            InitializeComponent();
            BindingContext = new { NotificationCount = "" };
        }

        public void ChangeNotificationBadge(int count)
        {
            if (count == 0)
            {
                BindingContext = new { NotificationCount = "" };
            } else
            {
                BindingContext = new { NotificationCount = "" + count };
            }
        }
    }

バッジ表示の実装

今回はAppから通知をつける場合を実装する。
上で実装したバッジ変更のメソッドを呼び出す

App.cs
   if (Current.MainPage is MainTabPage tabPage)
   {
       tabPage.ChangeNotificationBadge(1);
   }

まとめ

Shellが使えず、自分でTabbedPageを実装しないといけないのは少し面倒ではありますが、ライブラリを利用すると簡単にバッジ表示機能が実装できました。
iOSの場合はLinkerの設定などあるらしいので、もう一手間必要そうです。

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

Jetpack Compose における TalkBack 対応

本記事は Android Advent Calendar 2020 の 6 日目の記事です。

Jetpack Compose 以前はアクセシビリティ対応の一環として、ImageViewcontentDescription を設定したり View のグルーピングで TalkBack の対応をしていました。
大きな一つの View の中でレンダリングされる Jetpack Compose ではどのように TalkBack の対応をするのかについて書いていきます。

※ Jetpack Compose 1.0.0-alpha08 時点の記事で、今後大きな変更もあり得ます

Semantics

Jetpack Compose でアクセシビリティに対応するときは Semantics フレームワークを使用します。
これまでであれば読み上げツールが View のコンポーネントを識別していましたが、View のコンポーネントを操作できない Compose では Semantics を使用して構造化された UI に対して意味を与える必要があります。

また Semantics はアクセシビリティだけでなく、Jetpack Compose で UI テストをするときにも使用されます。

ref : https://developer.android.com/jetpack/compose/testing#semantics

accessibilityLabel

accessibilityLabel では読み上げる文言の設定ができます。

IconButton(
    onClick = {},
    modifier = Modifier.semantics {
        accessibilityLabel = "戻るボタン"
    }
) {
    Icon(imageVector = Icons.Default.ArrowBack)
}

これは ← のアイコンがついたボタンに Modifier#semantics でアクセシビリティに関することを設定します。
accessibilityLabel でボタンに対して読み上げて欲しい文言を設定します。
これまでの contentDescription に近いものがこれになります。

mergeDescendants

mergeDescendants は複数の要素をグループ化します。

Column(
    modifier = Modifier.semantics(mergeDescendants = true) {
    }
) {
    Text(
        text = "タイトル",
        style = MaterialTheme.typography.subtitle1
    )
    Providers(AmbientContentAlpha provides ContentAlpha.medium) {
        Text(
            text = "サブタイトル",
            style = MaterialTheme.typography.caption
        )
    }
}

mergeDescendants を true にすると要素がまとめて読み上げられます。
上記の例では 「タイトル サブタイトル」 と 2 つのテキストがまとめて読み上げられます。

accessibilityValue

accessibilityValue は要素の状態を設定することができます。

var checked by remember { mutableStateOf(false) }
Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        accessibilityValue = if (checked) {
            "チェック済み"
        } else {
            "未チェック"
        }
    }
) {
    Checkbox(checked = checked, onCheckedChange = {
        checked = it
    })
    Text("アイテム")
}

チェックボックスとテキストをグループ化し、チェックボックスの状態を accessibilityValue に設定しています。
上記の例では 「チェック済み アイテム」 と読み上げられます。

clickable

Compose でタップイベントを実装する Modifier#clickable も内部の実装では semantics で読み上げに対応しています。

Box(
    modifier = Modifier.fillMaxWidth()
        .clickable(
            onClick = {
                ...
            },
            onClickLabel = "詳細を開く"
        )
) {
    ...
}

Modifier#clickable には onClickLabelonLongClickLabel を設定でき、それぞれにタップ時に読み上げる文言を設定することができます。

val semanticModifier = Modifier.semantics(mergeDescendants = true) {
    if (enabled) {
        onClick(action = { onClick(); true }, label = onClickLabel)
        if (onLongClick != null) {
            onLongClick(action = { onLongClick(); true }, label = onLongClickLabel)
        }
    } else {
        disabled()
    }
}

Modifier#clickable の内部ではこのような実装になっており、 semantics が設定されています。

customActions

customActions はフォーカスされている項目に対してアクションを設定することができます。

Box(
    modifier = Modifier.semantics(mergeDescendants = true) {
        customActions = listOf(
            CustomAccessibilityAction("アクション") {
                // return a boolean result indicating whether the action is successfully handled
                true
            }
        )
    }
) {
    ...
}

TalkBack では選択された項目に対してジェスチャーでコンテキストメニューを呼び出して操作する場合もあります。
CustomAccessibilityAction でアクションを定義することで、コンテキストメニューでの項目を追加することができます。

Jetpack Compose における基本的な TalkBack の対応について書いてみました。
Jetpack Compose はまだ alpha とはいえアクセシビリティに対応する仕組みが用意されているので、置き換えるにしても新規で作るにしても意識しておきましょう。

Compose のサンプルプロジェクトでも要所要所に semantics の実装がされているので、そちらをみるのも参考になると思います。

ドキュメント

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

DependabotをAndroidプロジェクトで使う方法

Dependabotは、依存しているライブラリのアップデートをチェックし、自動でPRを作ってくれる便利なbotです。

Androidプロジェクトでも、GitHub上からポチポチするだけで簡単にセットアップすることができたので、手順をご紹介したいと思います。

※ 個人のプライベートなAndroidプロジェクトで試しました。 app モジュールのみのオーソドックスなAndroidプロジェクトです。

手順

  • リポジトリを開きます。

  • タブにある Insights を選択します。

  • Dependabot を選択し、Dependabotを有効化します。

  • 有効化した後は、configファイルを作成します。

  • ymlのconfigファイル作成画面になります。

  • 色々と設定を記述することもできますが、一旦無視して package-ecosystem のところに 「gradle」 を記述します。

  • コミットします。

  • dependabot.ymlが作成されました。設定としては以上です。

  • Insights に戻ると gradle のマークがあり、 Checking now と表示されています。

  • プロジェクトの大きさにもよるかもしれませんが、30秒もかからずにPRが作成されていました!

  • PRの差分を見てみると、バージョンのところだけ綺麗にアップデートされているのがわかります。

  • ちなみに、変数を用いてバージョンを共通して書くやり方をしている箇所でも、うまいこと変数のところだけアップデートされるようでした。(賢い!)

まとめ

DependabotをAndroidプロジェクトで使う方法をご紹介しました。
ポチポチするだけで設定できるので、かなり簡単に導入することができるなぁと感じました。

自動でPRを出してくれるのも非常に便利です。

ただ、ライブラリのアップデートのPRは出してくれますが、機械的なアップデートなため、アプリの機能が動かなくなったり、レイアウトが崩れたりなどの問題が起こる可能性もあるので、PR起点のCIでテストを回したり、PRをpullして手元で確認したりなどは必要なのかなと思いました。

Dependabotのドキュメントは こちら を御覧ください。

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