Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

【Android】MediatorLiveData で他の LiveData を監視する

今回は、MediatorLiveData で他の LiveData を監視する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Bumblebee | 2021.1.1 Patch 2
  • JDK 11.0.11
  • Android Gradle Plugin 7.1.2
  • Kotlin 1.6.10
  • Gradle 7.4.1
  • androidx.lifecycle 2.4.1

MediatorLiveData とは?

MediatorLiveData は LiveData のサブクラスであり、他の LiveData の値を監視して自身の値を変更することができます。

以下が、監視対象を追加するメソッドです。

@MainThread
public <S> void addSource(@NonNull LiveData<S> source,
                          @NonNull Observer<? super S> onChanged)

引数 source に監視対象の LiveData を設定します。そして、引数 onChanged に引数 source が変更されたときに呼び出されるコールバックを設定します。

なお、1 つの MediatorLiveData で、2 つ以上の LiveData を監視することが可能です。

// OK
mediator.apply {
    addSource(source1) { mediator = it }
    addSource(source2) { mediator = it }
}

しかし、MediatorLiveData は 1 つの LiveData に対して 2 つ以上のコールバックを設定することができません。

以下のように、すでに監視を開始している LiveData に対して、異なるコールバックを設定した場合、MediatorLiveData#addSource は IllegalArgumentException を投げます。

// NG
mediator.apply {
    addSource(source1) { mediator = it }
    addSource(source1) { mediator = "source1: $it" }
}

監視を停止したい場合には、以下の MediatorLiveData#removeSource を使います。

@MainThread
public void <S> removeSource(@NonNull LiveData<S> toRemote)

MediatorLiveData を使う

実際に MediatorLiveData を試してみます。

今回は、Activity と ViewModel、そして、String 型の値を持つ LiveData を 2 つとそれらを結合する MediatorLiveData を 1 つ用意します。MediatorLiveData は、2 つの LiveData を監視し、それらが変更されるたびに 2 つの LiveData を結合して自身に格納します。

上記の 3 つの LiveData を ViewModel に保持し、Activity から結合元となる 2 つの LiveData の変更と MediatorLiveData の監視を行います。

ライブラリのインポート

MediatorLiveData を使えるようにするために、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
}

lifecycle-livedata-ktx をインポートすることにより、MediatorLiveData が使えるようになります。また、以降のサンプルでは LiveData を ViewModel に保持するため、lifecycle-viewmodel-ktx もインポートします。

ViewModel で値の保持と結合を行う

まず、ViewModel を以下のように実装します。

class MainViewModel : ViewModel() {

    // editText1 の文字列
    val str1 = MutableLiveData("")

    // editText2 の文字列
    val str2 = MutableLiveData("")

    // editText1 と editText2 の結合
    val linkedStr: MediatorLiveData<String> = MediatorLiveData()

    init {
        linkedStr.apply {
            // str1 の変更を監視する。
            addSource(str1) {
                linkedStr.value = "$it ${str2.value!!}"
            }
            // str2 の変更を監視する。
            addSource(str2) {
                linkedStr.value = "${str1.value!!} $it"
            }
        }
    }
}

監視の開始を init で行います。2 つの LiveData ごとに addSource を呼び出します。監視対象が変更されたら、もう一方の LiveData と結合して、自身に格納します。

Activity で LiveData の変更と MediatorLiveData の監視を行う

そして、Activity を以下のように実装します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val viewModel =
            ViewModelProvider(this)[MainViewModel::class.java]

        // editText1 の文字列変更を str1 に反映する。
        findViewById<EditText>(R.id.editText1)
            .doOnTextChanged { text, _, _, _ ->
                viewModel.str1.value = (text ?: "").toString()
            }

        // editText2 の文字列変更を str2 に反映する。
        findViewById<EditText>(R.id.editText2)
            .doOnTextChanged { text, _, _, _ ->
                viewModel.str2.value = (text ?: "").toString()
            }

        viewModel.linkedStr.observe(this) {
            findViewById<TextView>(R.id.text).text = it
        }
    }
}

Activity のレイアウトには、2 つの LiveData ごとに EditText を用意し、それらのテキストの変更を監視し、テキストを ViewModel の LiveData に格納します。

そして、2 つの LiveData を結合した linkedStr を監視し、その値を TextView に表示します。

こうすることにより、2 つの EditText に入力したテキストを結合したものが TextView に表示されるようになります。

参考

MediatorLiveData

【Android】アプリのインストールを検知する

今回は、Androidでアプリのインストールを検知する方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

アプリのインストールを検知する方法

Android OSは、アプリがインストールされると android.intent.action.PACKAGE_ADDED というインテントを発信します。よって、このインテントをBroadcastReceiverにて受信することにより、アプリのインストールを検知することができます。

マニフェストファイルの変更

まず、マニフェストファイル AndroidManifest.xml に以下の設定を追加します。

<receiver
    android:name=".PackageChangedReceiver"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_ADDED" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="package" />
    </intent-filter>
</receiver>

BroadcastReceiverの実装

そして、以下のようにBroadcastReceiverを実装します。

public final class PackageChangedReceiver extends BroadcastReceiver {

    @NonNull
    private static final String TAG = "PackageChangedReceiver";

    @Override
    public void onReceive(final Context context, final Intent intent) {
        // intent#getDataStringからインストールされたアプリのパッケージ名を取得できる。
        final String packageName = intent.getDataString();
        Log.d("PackageChanged", packageName);
    }
}

以上の実装により、アプリのインストールを検知できるようになります。

また、intent#getDataStringを使ってインストールされたアプリのパッケージ名を取得することができるため、期待するアプリがインストールされたのか判断することも可能です。

【Android】WebView の JavaScript からネイティブコードを呼び出す

今回は、WebView の JavaScript からネイティブコードを呼び出す方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

JavaScript からネイティブコードを呼び出すまでの流れ

WebView の JavaScript からネイティブコードを呼び出すには、以下を行う必要があります。

  1. JavaScript とネイティブコードをつなぐインターフェースを作成する
  2. JavaScript とネイティブコードのインターフェースを WebView に追加する
  3. WebView の JavaScript を有効にする

今回は、WebView に表示する Web ページにボタンを用意し、そのボタンを押すと、Android ネイティブの Toast を表示するようにします。また、ネイティブコードは JavaScript に対して戻り値を返すようにし、JavaScript はその戻り値を Web ページに表示するようにします。

JavaScript とネイティブコードをつなぐインターフェースを作成する

まず、JavaScript とネイティブコードをつなぐインターフェースを作成します。

class WebAppInterface(private val context: Context) {

    @JavascriptInterface
    fun showMessage(message: String): String {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
        return message
    }
}

インターフェースには、JavaScript から呼び出すネイティブコードをメソッドとして作成します。このメソッドには、@JavascriptInterface を付与し、public にする必要があります。

今回作成するメソッドでは、JavaScript から文字列を受け取るようにし、その文字列を Toast に表示します。また、受け取った文字列をそのまま戻り値として呼び出し元に返します。

WebView の設定

次に、WebView にさきほど作成したインターフェースを追加し、JavaScript を有効にします。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WebView(this).apply {
            setContentView(this)
            loadUrl("file:///android_asset/index.htm")
            val webAppInterface = WebAppInterface(this@MainActivity)
            addJavascriptInterface(webAppInterface, "Android")

            settings.apply {
                allowFileAccess = true
                javaScriptEnabled = true
            }
        }
    }
}

まず、以下のように WebView にさきほど作成したインターフェースを追加します。

WebView#addJavascriptInterface の第 1 引数にインターフェースのインスタントを渡します。そして、第 2 引数には、JavaScript から呼び出す際の名前を指定します。例えば、この名前に "Android" を指定した場合、JavaScript から呼び出す際は Android.showMessage() となります。

        WebView(this).apply {
            ︙
            val webAppInterface = WebAppInterface(this@MainActivity)
            addJavascriptInterface(webAppInterface, "Android")
            ︙

そして、WebView の JavaScript を有効にします。デフォルトでは JavaScript は無効になっているので、有効にする必要があります。

        WebView(this).apply {
            ︙
            settings.apply {
                ︙
                javaScriptEnabled = true
            }
        }

以上で、ネイティブの実装は完了です。これで、JavaScript からネイティブコードを呼び出すことができるようになります。

JavaScript からネイティブコードを呼び出す

JavaScript からネイティブコードを呼び出してみます。Web ページの HTML を以下のようにします。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <script type="text/javascript">
            function showMessage(message) {
                const result = Android.showMessage(message);
                document.getElementById("text").innerHTML = result;
            }
        </script>
        <input type="button" value="Button"
               onClick="showMessage('Hello Android!');" />
        <p id="text"></p>
    </body>
</html>

ボタンが押されたら、JavaScript とネイティブコードのインターフェースの showMessage メソッドを呼びます。この呼び出しにより、Android ネイティブの Toast が表示されます。また、showMessage メソッドの戻り値を p タグに表示します。

参考

WebView での JavaScript の使用

【Android】LiveDataで値を監視する

LiveData は監視可能なデータホルダークラスです。通常の監視とは異なり、LiveDataAndroid のライフサイクルに応じた監視が可能です。つまり、アクティビティ、フラグメント、サービスなどの他のアプリコンポーネントのライフサイクルが考慮されます。

build.gradledependencies の設定は以下の通り。

// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")

ここでは、以下の2種類の LiveData を解説します。 - LiveData - MutableLiveData

LiveData

データが更新されたとき、監視者にそれを通知します。データを直接更新することはできません。

LiveData#observe

public void observe(LifecycleOwner owner, Observer<? super T> observer)

アクティビティやフラグメントなどの監視者が監視を開始します。引数 owner には監視者のライフサイクルオーナーを指定します。監視者がアクティビティのときは this を指定します。監視者がフラグメントのときは viewLifecycleOwner を指定します。これは、フラグメントのライフサイクルはアクティビティのそれとは異なり、不要な更新通知などを避けるためです。

以下は、フラグメントが LiveData を監視する場合の observe の使用方法です。

viewModel.count.observe(viewLifecycleOwner, {
    binding.countText.text = it.toString()
})

LiveData#getValue

public T getValue()

LiveData に格納されている値を取得します。

MutableLiveData

データが更新されたとき、観察者にそれを通知します。データを直接更新することができます。LiveData を継承しています。

一般的に、クラスのカプセル化を意識して、MutableLiveDataプロパティを非公開にし、LiveDataプロパティのみを公開します。

class MainViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> get() = _count

MutableLiveData#setValue

public void setValue(T value)

MutableLiveData に値を設定します。メインスレッドから値を設定する場合に使用します。

MutableLiveData#postValue

public void postValue(T value)

MutableLiveData に値を設定します。ワーカースレッドから値を設定する場合に使用します。

参考

LiveData の概要

Pythonのf文字列による文字列補完

Pythonで文字列補完をするとき、今までは文字列メソッドformat()を使用していたのですが、Python3.6から追加されたf文字列(フォーマット済み文字列リテラル)を用いることで、より簡潔に記述出来ることがわかったので、ここにその方法を記載します。

サンプルソース

以下にformat()による文字列補完とf文字列による文字列補完を記載します。

w = 'World'
msg = 'Hello,{}!'.format(w)
print(msg)
# Hello,World!
w = 'World'
msg = f'Hello,{w}!'
print(msg)
# Hello,World!

上記の通り、冗長なformat()呼び出しの記述がなくなるため、f文字列による文字列補完のほうがソースコードが簡潔になります。

書式指定

文字列メソッドformat()と同様に、f文字列でも置換フィールドでコロン:のあとに書式指定文字列を指定することで様々な書式を指定出来ます。

s = 'abc'
right  = f'right : {s:_>8}'
center = f'center: {s:_^8}'
left   = f'left  : {s:_<8}'
print(right)
print(center)
print(left)
# right : _____abc
# center: __abc___
# left  : abc_____
i = 1234
s = f'{i:08}'
print(s)
# 00001234

参考

フォーマット済み文字列リテラル

Pythonのvenvでパッケージ管理する

今回は、venvを用いたPythonのパッケージ管理方法を解説します。

パッケージ管理とは?

Pythonでは、pipコマンドによりパッケージをインストールすることが出来ます。このときにそのままパッケージをインストールすると、ユーザーディレクトリなどにインストールされ、各プロジェクト共通になります。

しかし、プロジェクト毎にパッケージを隔離したい場合があります。例えば、特定のプロジェクトのパッケージのバージョンを古いバージョンのままにしたいなどです。そのような場合に、パッケージ管理を利用します。

前準備

venv自体はPython3.3から標準搭載であるため、Pythonのバージョンが3.3以上であれば、venvのインストール作業は必要ないです。

まず、プロジェクトディレクトリを以下の構成にします。

[Project Directory]
  ├src
  │ ├__init__.py
  │ └hoge.py
  └venv   

上記のvenvディレクトリは仮想環境ディレクトリなどと呼ばれ、このディレクトリ以下にインストールしたパッケージが保存されていきます。最初、venvディレクトリの中は空にします。ディレクトリ名はvenvでなくてもよいですが、一般的にvenvと命名することが多いので、そのままとします。

なお、srcディレクトリ以下にソースコードを配置します。このディレクトリも制限はないですが、srcディレクトリ以下にソースコードを配置するのが一般的なので、こちらもそれに倣います。

利用方法

仮想環境の作成

カレントディレクトリをプロジェクトディレクトリ直下にした状態で、コマンドプロンプトなどで以下のコマンドを打ちます。

>python -m venv [venv_dir_name]

[venv_dir_name]には仮想環境ディレクトリのディレクトリ名を入力します。なので、今回はvenvとなります。

仮想環境の作成に成功すると、以下のように仮想環境ディレクトリの配下に新たにディレクトリやファイルが生成されます。

[Project Directory]
  ├src
  │ ├__init__.py
  │ └hoge.py
  └venv
     ├Include
     ├Lib
     ├Scripts
     └pyvenv.cfg

Activate

以下のようなコマンドを入力して仮想環境を有効にします。

>.\[venv_dir_name]\Scripts\activate
([venv_dir_name]) >

コマンド入力後、コマンドプロンプト上で仮想環境ディレクトリ名が表示されるようになったと思います。

次に、インストール済みパッケージを確認するために、pip freezeコマンドを入力します。

([venv_dir_name]) >pip freeze
([venv_dir_name]) >

入力の結果、パッケージのインストール状態がまっさらになっていることがわかります。

パッケージのインストール

pip installコマンドを入力して、パッケージをインストールします。インストール方法は、venvを使用していないときと同じです。

([venv_dir_name]) >pip install [package name]

次に、再度pip freezeコマンドを入力します。

([venv_dir_name]) >pip freeze
[package name1]==x.x.x
[package name2]==x.x.x
・
・
・

入力の結果、仮想環境にパッケージがインストールされたことがわかります。

Deactivate

仮想環境を無効にするには、deactivateコマンドを入力します。

([venv_dir_name]) >deactivate
>

入力の結果、コマンドプロンプトから仮想環境ディレクトリ名の表示が消えて、仮想環境を有効にする前の状態に戻ったことがわかります。

仮想環境の破棄と作り直し

仮想環境を1から作り直したい場合など、作成済みの仮想環境を破棄したい場合は、仮想環境ディレクトリ配下のディレクトリとファイルをすべて削除してください。

削除後、再度venvコマンドを入力することにより、仮想環境を作り直すことが出来ます。

【Android】SavedStateHandle解説

今回は、AndroidのSavedStateHandleを解説します。

SavedStateHandle クラスは、set() メソッドおよび get() メソッドを介して、SavedState との間でデータの書き込みや取得を行えるようにする Key-Value マップです。また、getLiveData() を使用して LiveData オブザーバブルにラップされている値を SavedStateHandle から取得できます。キーの値が更新されると、LiveData が新しい値を受け取ります。

build.gradledependencies では、以下を必要とします。

implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")

使用例

例えば、以下のように使用します。

class MainViewModel(
    // ViewModelの第1引数にSavedStateHandleを指定します。
    private val handle: SavedStateHandle
) : ViewModel() {

    companion object {
        // 値の出し入れに使用するキーを定数宣言します。
        private const val KEY_COUNT = "count"
    }

    // SavedStateHandleからキーKEY_COUNTにひも付くMutableLiveDataを探して取り出します。
    // 存在しない場合は新しく作ります。
    // そして、SavedStateHandle内でキーKEY_COUNTに対してひも付けます。
    // 初期値0。
    private val _count = handle.getLiveData(KEY_COUNT, 0)
    val count: LiveData<Int> get() = _count

    fun countUp() {
        // _count.valueはInt?型ですが、SavedStateHandle#getLiveData呼び出し時に初期値を設定しており、
        // nullが入り込むことがありません。よって、!!によって強制的にアンラップしています。
        val currentCount = _count.value!!

        // SavedStateHandle#getLiveDataにて値を取り出している場合、
        // SavedStateHandle#setで値の格納と同時に監視者への更新通知が行われます。
        // なお、SavedStateHandle#getにて値を取り出している場合には、
        // 監視者への更新通知が行われません。
        // また、内部的にはMutableLiveData#setValueが呼ばれているため、
        // ワーカースレッドから呼び出してはいけません。
        handle.set(KEY_COUNT, currentCount + 1)
    }

また、ViewModel の取得でby viewModels() を使用している場合、viewModels()ViewModelSavedStateHandleインスタンスを渡しているため、コードを変更する必要はありません。

class MainFragment : Fragment(R.layout.main_fragment) {
    private val viewModel: MainViewModel by viewModels()

参考

ViewModel の保存済み状態のモジュール