Smile Engineering Blog

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

【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 の保存済み状態のモジュール

Pythonでプロパティを実装する

今回は、Pythonでのプロパティの実装方法を解説します。

プロパティとは?

ここで取り扱うプロパティとは、クラス利用者がそのメンバ変数にアクセスするときにgetterやsetterを意識することなくアクセス出来るようにする仕組みです。

通常、クラスを実装するときはメンバ変数を直接外部に公開せず、プライベートにしつつ、別途getterとsetterを用意します。これをカプセル化と呼びます。よって、クラス利用者はgetterやsetterを介してメンバ変数にアクセスします。

class Name1:
    def __init__(self):
        self._firstname = ''
        self._lastname = ''

    def get_firstname(self):
        return self._firstname

    def set_firstname(self, firstname):
        self._firstname = firstname

    def get_lastname(self):
        return self._lastname

    def set_lastname(self, lastname):
        self._lastname = lastname

    def get_fullname(self):
        return self._firstname + self._lastname

name = Name1()
name.set_firstname('Fugataro')
name.set_lastname('Hogemoto')
print(name.get_firstname())
print(name.get_lastname())
print(name.get_fullname())

# Fugataro
# Hogemoto
# FugataroHogemoto

しかし、プロパティを用いることで、カプセル化を維持しつつ、クラス利用者はgetterやsetterを意識することなくメンバ変数にアクセス出来るようになります。

Pythonでプロパティを実装する方法は2通りあるため、両方解説していきます。

property関数を用いる方法

1つ目はproperty関数を用いる方法です。

まず、今まで通りにgetterとsetterを宣言します。そして、property関数に先ほど宣言したgetterとsetterの関数名を渡します。こうすることにより、クラス利用者はインスタンス名.メンバ変数名のフォーマットでメンバ変数にアクセス出来るようになりつつ、クラス内部では、先ほどproperty関数に渡したgetterとsetterを経由して、メンバ変数の受け渡しが行われます。

class Name2:
    def __init__(self):
        self._firstname = ''
        self._lastname = ''

    def get_firstname(self):
        return self._firstname

    def set_firstname(self, firstname):
        self._firstname = firstname

    def del_firstname(self):
        del self._firstname

    firstname = property(
        get_firstname, set_firstname, del_firstname
        )

    def get_lastname(self):
        return self._lastname

    def set_lastname(self, lastname):
        self._lastname = lastname

    def del_lastname(self):
        del self._lastname

    lastname = property(
        get_lastname, set_lastname, del_lastname
        )

    def get_fullname(self):
        return self._firstname + self._lastname

    fullname = property(get_fullname)

name = Name2()
name.firstname = 'Fugataro'
name.lastname = 'Hogemoto'
print(name.firstname)
print(name.lastname)
print(name.fullname)

# Fugataro
# Hogemoto
# FugataroHogemoto

また、メンバ変数をdelする関数も登録することが出来ます。以下サンプルソースでは実際にメンバ変数をdelしていますが、delした後にそのメンバ変数にアクセスすると例外が発生するため、delが正常に機能していることがわかります。

name = Name2()
name.firstname = 'Fugataro'
print(name.firstname)
del name.firstname
print(name.firstname)

# Fugataro
# AttributeError: 'Name2' object has no attribute '_firstname'

デコレータを用いる方法

2つ目はデコレータを用いる方法です。

デコレータを用いる場合も、getter、setter、deleterを宣言しますが、それら関数に、@property@メンバ変数名.setter@メンバ変数名.deleterを付与するだけです。

class Name3:
    def __init__(self):
        self._firstname = ''
        self._lastname = ''

    @property
    def firstname(self):
        return self._firstname

    @firstname.setter
    def firstname(self, firstname):
        self._firstname = firstname

    @firstname.deleter
    def firstname(self):
        del self._firstname

    @property
    def lastname(self):
        return self._lastname

    @lastname.setter
    def lastname(self, lastname):
        self._lastname = lastname

    @lastname.deleter
    def lastname(self):
        del self._lastname

    @property
    def fullname(self):
        return self._firstname + self._lastname

name = Name3()
name.firstname = 'Fugataro'
name.lastname = 'Hogemoto'
print(name.firstname)
print(name.lastname)
print(name.fullname)

# Fugataro
# Hogemoto
# FugataroHogemoto
name = Name3()
name.firstname = 'Fugataro'
print(name.firstname)
del name.firstname
print(name.firstname)

# Fugataro
# AttributeError: 'Name3' object has no attribute '_firstname'

参考

class property

カプセル化

リンクを Markdown 形式でクリップボードにコピー

はじめに

Markdown で執筆中、サイトのリンクを貼り付けたいときありますよね、きっと。そんなときは、chrome の拡張 Markdown Linker が便利です。

chrome ウェブストアより:

chrome 拡張なので chrome 限定のお話です。ごめんなさい

続きを読む

画像を ASCII 表示させる方法

はじめに

テストに用いるデータなど、サーバ上にあるイメージ(画像)を確認したいときないですか?このファイルなんだっけ?みたいな。そんなとき、jp2a を使えば画像を ASCII 表示できます。テキストで表示されるので、手軽にコンソールからも確認できるってことです。

Talinx/jp2a: Converts jpg/png images to ASCII

続きを読む

Pythonで読み取り専用コレクションを実装する

今回は、Pythonで読み取り専用コレクションのメンバ変数を実装する方法を解説します。

概要

通常、読み取り専用のメンバ変数を実装するときは、メンバ変数をプライベートにしつつ、getterのみ宣言して、setterを宣言しないようにします。

class Name:
    def __init__(self):
        self._firstname = 'Fugataro'

    @property
    def firstname(self):
        return self._firstname

name = Name()
print(name.firstname)
name.firstname = 'Hogemi'

# Fugataro
# AttributeError: can't set attribute

注意が必要なのは、listdictなどのコレクション型変数をメンバ変数とする場合です。

上記と同様に、getterのみ宣言して、setterを宣言しないことにより、コレクション型のメンバ変数を別のコレクション型変数に置き換えることは出来ません。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data = [4, 5, 6]

# [1, 2, 3]
# AttributeError: can't set attribute

しかし、append()remove()などでコレクション型メンバ変数の中身を書き換えることが出来てしまっています。これは、getterのみの宣言により、メンバ変数そのものの置き換えは出来ないようになりましたが、コレクション型メンバ変数の関数はいまだ機能しているため、結果としてそれら関数により、中身の書き換えが出来てしまっているのです。

class NumbersList:
    def __init__(self):
        self._data = [1, 2, 3]

    @property
    def data(self):
        return self._data

numbers = NumbersList()
print(numbers.data)
numbers.data.append(4)
numbers.data.extend([5, 6])
numbers.data.remove(1)
print(numbers.data)

# [1, 2, 3]
# [2, 3, 4, 5, 6]

通常、この書き換えを防ぐために、別途用意されている読み取り専用のコレクションクラスを利用します。

Pythonの場合、読み取り専用のコレクションクラスは用意されていません。しかし、基本的なコレクションであるlistdictsetに対して、以下を用いることにより、代用可能です。

通常 読み取り専用
list tuple
dict types.MappingProxyType
set frozenset

実装方法

listdictsetに対して、対応する代用のコレクションにてキャストすればよいです。以下にサンプルソースを記載します。

tuple

class ReadOnlyList:
    def __init__(self):
        self._data = tuple([1, 2, 3])

    @property
    def data(self):
        return self._data

l = ReadOnlyList()
print(l.data)

print(l.data[0])
print(l.data[1])
print(l.data[2])

for i in l.data:
    print(i)

l.data.append(4)

# (1, 2, 3)
# 1
# 2
# 3
# 1
# 2
# 3
# AttributeError: 'tuple' object has no attribute 'append'

types.MappingProxyType

import types

class ReadOnlyDict:
    def __init__(self):
        d = {'msg': 'Hello,World!', 'param': 1}
        self._data = types.MappingProxyType(d)

    @property
    def data(self):
        return self._data

d = ReadOnlyDict()
print(d)

print(f'msg: {d.data["msg"]}')
print(f'param: {d.data["param"]}')

for key in d.data:
    print(key)

for value in d.data.values():
    print(value)

for key, value in d.data.items():
    print(key, value)

d.data['name'] = 'Foge'

# <__main__.ReadOnlyDict object at 0x0000024B239D7D88>
# msg: Hello,World!
# param: 1
# msg
# param
# Hello,World!
# 1
# msg Hello,World!
# param 1
# TypeError: 'mappingproxy' object does not support item assignment

frozenset

class ReadOnlySet:
    def __init__(self):
        s = set((1, 2, 3))
        self._data = frozenset(s)

    @property
    def data(self):
        return self._data

s = ReadOnlySet()
print(s.data)

for i in s.data:
    print(i)

s.data.add(4)

# frozenset({1, 2, 3})
# 1
# 2
# 3
# AttributeError: 'frozenset' object has no attribute 'add'

Pythonで抽象クラスを実装する

今回は、Pythonで抽象クラスを実装する方法を解説します。

抽象クラスとは?

プログラミング言語によりある程度違いはありますが、抽象クラスとは、以下の特徴を持ったクラスになります。

  • 継承されることを前提としています。
  • 空実装のメソッドが定義されています。(これを抽象メソッドと呼びます。)
  • このクラスを継承したクラスは、抽象メソッドを再定義(オーバーライド)しなければなりません。

よって、抽象クラスは子クラスに対してメソッドの定義を強制する機能を持っています。

用途としては、多態性ポリモーフィズム)を実装するときに、メソッドの実装漏れ、引数または戻り値の違いによるエラーの回避、などがあります。

実装方法

ここでは、抽象クラスと抽象メソッドの実装方法を解説します。

まず、抽象クラスの実装方法はABCMetaというメタクラスを用います。抽象クラスにしたいクラスのメタクラスABCMetaを設定することにより、そのクラスが抽象クラスになります。(ちなみに、ABCとはAbstract Base Classの略だそうです。)

from abc import ABCMeta

class vehicle(metaclass = ABCMeta):
    pass

また、PythonにはABCというクラスがあり、このクラスを継承することでも抽象クラスを実現することが可能です。

from abc import ABC

class vehicle(ABC):
    pass

このABCの実態は、ABCMetaメタクラスに設定しているだけのクラスです。よって、初めのコードと実質同じです。

次に抽象メソッドを実装するときは、@abstractmethodデコレータを使用します。このデコレータを付与されたメソッドが抽象メソッドになります。抽象メソッドには実際の処理を記述しないので、passまたはraise NotImplementedError()を実装しておきます。(NotImplementedError()とは、未実装エラーを表す例外です。)

from abc import ABCMeta
from abc import abstractmethod

class vehicle(metaclass = ABCMeta):
    """抽象クラスvehicleの定義。"""
    @abstractmethod
    def start(self):
        raise NotImplementedError()

    @abstractmethod
    def stop(self):
        raise NotImplementedError()

ABCMetaには、抽象メソッドが実装されている場合にインスタンスを生成しようとすると、例外が発生する仕様になっています。よって、上記vehicleインスタンスを生成しようとすると、以下のように例外が発生します。

v = vehicle()

# TypeError: Can't instantiate abstract class vehicle with abstract methods start, stop

なお、ABCMetaメタクラスとしているが抽象メソッドが実装されていないクラスや、抽象メソッドは実装されているがABCMetaメタクラスとしていないクラスのインスタンスは生成出来てしまいます。抽象クラスを実装するときは、ABCMetaメタクラスにすることと抽象メソッドの実装をセットで行ってください。

以下のように、抽象クラスを継承して抽象メソッドをオーバーライドすれば、インスタンスの生成が出来るようになります。

from abc import ABCMeta
from abc import abstractmethod

class vehicle(metaclass = ABCMeta):
    """抽象クラスvehicleの定義。"""
    @abstractmethod
    def start(self):
        raise NotImplementedError()

    @abstractmethod
    def stop(self):
        raise NotImplementedError()

class car(vehicle):
    """vehicleを継承したクラスcarの定義。"""
    def start(self):
        print("car start.")

    def stop(self):
        print("car stop.")

class motorcycle(vehicle):
    """vehicleを継承したクラスmotorcycleの定義。"""
    def start(self):
        print("moto start.")

    def stop(self):
        print("moto stop.")

my_car = car()
my_car.start()
my_car.stop()

my_motorcycle = motorcycle()
my_motorcycle.start()
my_motorcycle.stop()

# car start.
# car stop.
# moto start.
# moto stop.

抽象メソッドを表すデコレータについては、@abstractmethod以外にも存在します。ここでは詳細を省きますが、抽象メソッドを表すデコレータを以下に列挙しておきます。

  • @abstractmethod
  • @abstractclassmethod
  • @abstractstaticmethod
  • @abstractproperty

ちなみに、誤って抽象クラスのインスタンスを生成してしまっているコードをflake8にかけてみましたが、こちらではなにも表示されませんでした。抽象クラスのインスタンス生成エラーは実行時にしかわからないようです。

参考

【Android】コンストラクタに引数があるViewModelを使う

AndroidでViewModelを使用するとき、以下のようなコンストラクタに独自の引数があるViewModelを使用したいときがあります。今回は、その方法を解説します。

class MainViewModel(id: Int) : ViewModel()

独自Factoryの定義

ViewModelインスタンスの生成はViewModelのコンストラクタを直接呼び出すのではなくて、以下のようにFactoryクラスのcreateメソッドを呼び出して生成します。

if (mFactory instanceof KeyedFactory) {
    viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
    viewModel = mFactory.create(modelClass);
}

よって、まずはコンストラクタに引数があるViewModelに対応したFactoryクラスを定義します。

class MainViewModel(id: Int) : ViewModel() {

    class Factory(
        private val id: Int
    ) : ViewModelProvider.NewInstanceFactory() {

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return MainViewModel(id) as T
        }
    }
}

独自Factoryの使用

独自に定義したFactoryクラスは、そのインスタンスをViewModelProviderに渡す必要があります。ViewModelProviderのコンストラクタの中に、Factoryインスタンスを引数に持つコンストラクタがオーバーロードされているので、以下のようにそのコンストラクタを使用します。

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

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

        val viewModel = ViewModelProvider(
            this, MainViewModel.Factory(0)
        ).get(MainViewModel::class.java)
    }
}

以上のようにして、コンストラクタに引数があるViewModelを使用できます。