アゲブログ

プログラマーです。

Parcelableについて

Parcelableとは?

独自のデータクラスのインスタンスをBundleで保存したりIntentに渡す際に使用するインターフェース。 Serializableインターフェースを実装することでも同じことが実現できるが、Parcelableを用いるほうが効率が良いとのこと。

実装例

データクラス

Parcelableインターフェースを実装したクラスを作成する。

class User(
    var age: Int,
    var name: String
) : Parcelable {

    constructor(parcel: Parcel) : this(
        parcel.readInt(),
        parcel.readString() ?: ""
    )

    override fun writeToParcel(dest: Parcel?, flags: Int) {
        if (dest != null) {
            dest.writeInt(this.age)
            dest.writeString(this.name)
        }
    }

    override fun describeContents() = 0

    companion object CREATOR : Parcelable.Creator<User> {

        override fun createFromParcel(parcel: Parcel) = User(parcel)

        override fun newArray(size: Int): Array<User?> = arrayOfNulls(size)
    }
}

以下のメソッドを実装する必要がある。

writeToParcel(Parcel, Int)

メンバ変数の保存を行う。

describeContents(Int)

通常は0を返すようにする。ParcelにFileDescriptorを保存する場合はParcelable.CONTENTS_FILE_DESCRIPTORを返す。

また、Parcelable.Creatorインターフェースを実装したCREATORというコンパニオンオブジェクトを保持しなければならない。

Bundleでの保存・復元方法

onSaveInstanceState(Bundle)Bundle#putParcelable(String, Parcelable)で保存する。

そして、onCreate(Bundle)もしくはonRestoreInstanceState(Bundle)Bundle#getParcelable(String)を実行してインスタンスを復元することができる。

class MainActivity : AppCompatActivity() {

    lateinit var user: User

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.user = if (savedInstanceState != null) {
            savedInstanceState.getParcelable("user")
        } else {
            User(0, "")
        }

        // 省略...
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)

        outState?.putParcelable("user", this.user)
    }
}

インテントでの受け渡し

渡す側

val user = User(10, "aaa")
val intent = Intent(this, AnotherActivity::class.java)
intent.putExtra("user", user)
startActivity(intent)

受け取り側

val user = intent.getParcelableExtra<User>("user") // user.age = 10, user.name = "aaa"

参考

Activityのライフサイクル、バックスタック、タスク

あまり理解できていないのでまとめます。

Activityとは?

AndroidにおけるActivityとは、ユーザーがボタンをタップしたり文字を入力したりする画面そのものです。AndroidアプリはActivityからActivityを起動したりすることで画面遷移のような動作を実現することができます。

ライフサイクルとは?

MainActivityからSubActivityを起動した場合、SubActivityが生成され画面に表示されます。このとき、MainActivityは画面には表示されていないものの裏側では保持されている状態です。そして、SubActivityでバックボタンをタップした際はMainActivityが画面に表示(再開)され、SubActivityが破棄されます。このようなActivityの作成・停止・再開・破棄のような推移をActivityのライフサイクルといいます。開発者は状態が変化した際に実行されるコールバックメソッド(ライフサイクルメソッド)に処理を記述していくことになります。下図はActivityのライフサイクルと主なコールバックメソッドです。

activity_lifecycle.png (45.7 kB)

https://developer.android.com/reference/android/app/Activity

Android OSはメモリ不足などにより非表示状態にあるアプリのプロセスを強制終了することがあるため、すべてのライフサイクルメソッドが必ず呼び出されるという保証はありません。

onCreate()

Activityが作成されるタイミングで呼び出されます。Activityのレイアウト設定、ボタンなどのViewにリスナーを登録するような初期化処理を行います。なお、onCreate()メソッドは必ず実装しなければなりません。

onRestart()

Activityが再開される直前に呼び出されます。

onStart()

Activityが画面に表示される直前に呼び出されます。

onResume()

Activityがフォアグラウンド(ユーザーが操作可能な状態)になる直前に呼び出されます。

onPause()

Activityがバックグラウンドに移行する直前に呼び出されます。たとえば、SubActivityがMainActivityから起動された場合にMainActivityのonPause()メソッドが呼び出されます。このコールバックメソッドが呼び出されたあとはOSによって強制終了される可能性があるため、重要なデータの保存などはここで行うことが推奨されているようです。さらに、このメソッドの処理が完了しないと別のActivityを開始できないため軽い処理にしなければなりません。

onStop()

Activityが非表示になったときに呼び出されます。

onDestroy()

Activityが破棄されるときに呼び出されます。

実際に動かしてみる

Activityのそれぞれのライフサイクルメソッドにログを出力する処理を仕込み、どのようなタイミングでメソッドが呼び出されているのかを確認 します*1 。 重複するログは省略します。

MainActivity.kt

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {

    private val tag = MainActivity::class.java.simpleName

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(tag, "onCreate()")
    }

    override fun onRestart() {
        super.onRestart()
        Log.d(tag, "onRestart()")
    }

    override fun onStart() {
        super.onStart()
        Log.d(tag, "onStart()")
    }

    override fun onResume() {
        super.onResume()
        Log.d(tag, "onResume()")
    }

    override fun onPause() {
        super.onPause()
        Log.d(tag, "onPause()")
    }

    override fun onStop() {
        super.onStop()
        Log.d(tag, "onStop()")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(tag, "onDestroy()")
    }
}

アプリケーションを起動 → バックボタンをタップ

Image from Gyazo

アプリケーションを起動すると起点となるActivityが生成されて画面に表示されます。

09-16 00:43:00.606 7206-7206/org.ageage.myapplication D/MainActivity: onCreate()
09-16 00:43:00.608 7206-7206/org.ageage.myapplication D/MainActivity: onStart()
09-16 00:43:00.610 7206-7206/org.ageage.myapplication D/MainActivity: onResume()

バックボタンをタップするとActivityが破棄されるためonDestroy()が呼び出されるようです。

09-16 00:43:01.568 7206-7206/org.ageage.myapplication D/MainActivity: onPause()
09-16 00:43:01.928 7206-7206/org.ageage.myapplication D/MainActivity: onStop()
09-16 00:43:01.929 7206-7206/org.ageage.myapplication D/MainActivity: onDestroy()

アプリケーションを起動 → ホームボタンをタップ

Image from Gyazo

バックボタンをタップした際とアプリケーションは同じような動作をしているように見えますが ホームボタンをタップした場合はonDestroy()が呼び出されていません。

09-16 00:45:12.381 7586-7586/org.ageage.myapplication D/MainActivity: onPause()
09-16 00:45:12.411 7586-7586/org.ageage.myapplication D/MainActivity: onStop()

これはAndroidがActivityを管理する方法に起因するものです。

Activityはスタック形式のバックスタックに保存され、複数のActivityが積まれている状態でバックボタンをタップするとフォアグラウンド(スタックの先頭)のActivityが破棄されて以前のActivityが再開するようになっています。

diagram_backstack.png (22.6 kB)

https://developer.android.com/guide/components/activities/tasks-and-back-stack

また、連続して開始されたすべてのActivityはタスクという単位で管理されます。たとえば、2つのActivityが積まれているフォーカスされた(画面に表示されている)タスクAが存在する状態で、ホームボタンをタップするとタスクAはバックグラウンドに移動します。この状態になってもタスクAは保存されているためActivityは破棄されません。そして、ホーム画面から別のアプリケーションを起動すると新たにタスクBが作られ(マルチタスク)、タスクAのバックスタックとは別にタスクB用のバックスタックでActivityが保存されます。このような構造になっているため、ユーザーは複数のアプリケーションを切り替えながら操作することができます。

diagram_multitasking.png (12.1 kB)

https://developer.android.com/guide/components/activities/tasks-and-back-stack

そのため、ホームボタンをタップしてもActivityが破棄されずにバックグラウンド(非表示になる)に移動するためonDestroy()が呼び出されず、バックボタンをタップした場合はバックスタックからActivityがPop(破棄)されるためonDestroy()が呼び出されるのです。

アプリケーションを起動 → ホームボタンをタップ → 起動したアプリケーションを選択

Image from Gyazo

一度起動したアプリケーションをホーム画面から選択した場合は、すでにタスクが作られているためActivityを再開した際に呼び出されるonRestart()が呼び出されています。マルチタスクボタンをタップしたあとにタスクを選択した場合も同様です。

09-16 02:27:08.769 11765-11765/org.ageage.myapplication D/MainActivity: onRestart()
09-16 02:27:08.770 11765-11765/org.ageage.myapplication D/MainActivity: onStart()
09-16 02:27:08.772 11765-11765/org.ageage.myapplication D/MainActivity: onResume()

アプリケーションを起動 → マルチタスクボタンをタップ → タスクを削除

Image from Gyazo

マルチタスクボタンがタップされた際はホームボタンがタップされた場合と同じようです。

09-16 03:00:34.158 13627-13627/org.ageage.myapplication D/MainActivity: onPause()
09-16 03:00:34.169 13627-13627/org.ageage.myapplication D/MainActivity: onStop()

タスクを削除するとActivityが破棄されるのでonDestroy()が呼び出されます。

09-16 03:00:35.617 13627-13627/org.ageage.myapplication D/MainActivity: onDestroy()

アプリケーションを起動 → 画面を回転

Image from Gyazo

画面を回転した場合は画面構成が変更されるためActivityは破棄されてから再生成されます。

09-16 03:15:13.534 14217-14217/org.ageage.myapplication D/MainActivity: onPause()
09-16 03:15:13.541 14217-14217/org.ageage.myapplication D/MainActivity: onStop()
09-16 03:15:13.542 14217-14217/org.ageage.myapplication D/MainActivity: onDestroy()
09-16 03:15:13.593 14217-14217/org.ageage.myapplication D/MainActivity: onCreate()
09-16 03:15:13.594 14217-14217/org.ageage.myapplication D/MainActivity: onStart()
09-16 03:15:13.597 14217-14217/org.ageage.myapplication D/MainActivity: onResume()

このとき、Activityの状態保持には注意が必要です。たとえばボタンをタップした数を画面に表示するアプリケーションがあるとします。

Image from Gyazo

ある程度ボタンをタップして画面に表示されている数字が大きくなった状態で画面を回転すると、数字は0になってしまいます。

Image from Gyazo

これはActivityが再生成される際に数字の保存と復元を行っておらず、初期値の0になってしまっているためです。画面状態の保存にはonSaveInstanceState()コールバックを使用します。onSavedInstanceState()に引数としてBundle型のインスタンスが渡されます。Bundleはアプリの状態をKey-Value形式で管理するためのクラスです。保存されたBundleはActivityの再作成時にonCreate()の引数として渡されます*2

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView = findViewById<TextView>(R.id.textView)
        textView.text = savedInstanceState?.getString(KEY_TAPPED_NUM_TEXT) ?: "0"

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            val tappedNum = Integer.valueOf(textView.text.toString())
            textView.text = "${tappedNum + 1}"
        }
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)

        val textView = findViewById<TextView>(R.id.textView)
        outState?.putString(KEY_TAPPED_NUM_TEXT, textView.text.toString())
    }

    companion object {
        const val KEY_TAPPED_NUM_TEXT = "tapped_num_text"
    }
}

上記はonSavedInstanceState()で画面に表示されている数字を文字列として保存しています。そして、onCreate()の引数のBundle型のインスタンスnullでなければ(onSavedInstanceState()で値が保存されている場合)数字を取得して設定し、nullであれば初期値として0 を設定しています。こんな感じで画面が回転した際の処理はちょっと面倒臭いっすね。

他にも縦画面固定にしたり、画面を回転した際にActivityが破棄されないように設定するなどの対策を挙げることができますが、それぞれに利点・欠点があります。

最後に

Androidにおいては基礎的?な部分だと思うのですが、調べれば調べるほど深みにハマってしまうような感じです。Fragment編も書きたいと思います。。

あと、文章を書くのが苦手なので克服していきたい。。構成とか。。

参考

*1:launchMode属性はstandard

*2:onRestoreInstanceStateコールバックの引数にも保存されたBundleが同じように渡されますが、今回はonCreateコールバックを使用します

APTについて

APT(Advanced Packaging Tool)とは?

Debian系のLinuxディストリビューションなどで採用されているパッケージ管理システムです(RedHat系のCentOSFedoraだとyummacOSではHomebrew等が使用されている)。パッケージのインストール、アップデート、アンインストールなどを行うためのaptコマンドを使用することができます。他にもapt-getコマンドやaptitudeコマンドも使用できるようですが、aptを使用することが推奨されているようです。

コマンド 説明
# apt update インストール可能なパッケージの最新情報を取得する。
# apt install <パッケージ名> パッケージをインストールする。
# apt remove <パッケージ名> パッケージを削除する。
$ apt search <キーワード> キーワードに該当するパッケージの一覧を表示する。

※他にもたくさんあります。

使ってみる

今回はDockerでDebianのイメージを使用してVimをインストールしてからアンインストールしてみます(Dockerのインストールは割愛)。

まずはDockerコンテナを起動します。

$ docker container run --rm -it debian
root@9d6870376aeb:/#

コンテナ起動直後はパッケージ情報を取得していないので、apt search vimを実行してもパッケージが表示されません。

root@e3dd01310898:/# apt search vim
Sorting... Done
Full Text Search... Done

そこで、apt updateを実行します。このコマンドは/etc/apt/sources.listファイルの記述を元にパッケージ情報を取得します。

root@5f87dce2fa64:/# apt update
Get:1 http://security.debian.org/debian-security stretch/updates InRelease [94.3 kB]
Ign:2 http://cdn-fastly.deb.debian.org/debian stretch InRelease
Get:3 http://cdn-fastly.deb.debian.org/debian stretch-updates InRelease [91.0 kB]
Get:4 http://security.debian.org/debian-security stretch/updates/main amd64 Packages [471 kB]
Get:5 http://cdn-fastly.deb.debian.org/debian stretch Release [118 kB]
Get:6 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages [12.1 kB]
Get:7 http://cdn-fastly.deb.debian.org/debian stretch Release.gpg [2434 B]
Get:8 http://cdn-fastly.deb.debian.org/debian stretch/main amd64 Packages [9530 kB]
Fetched 10.3 MB in 2s (3734 kB/s)
Reading package lists... Done
Building dependency tree
Reading state information... Done
All packages are up to date.

この状態でapt searchを実行するとパッケージが表示されます(apt search ^vim$のようにキーワードに正規表現を用いることも可能)。

root@e3dd01310898:/# apt search ^vim$
Sorting... Done
Full Text Search... Done
vim/stable 2:8.0.0197-4+deb9u1 amd64
  Vi IMproved - enhanced vi editor

vimをインストールします(途中、続行するか問われるのでYを入力)。

root@e3dd01310898:/# apt install vim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  libgpm2 vim-common vim-runtime xxd
Suggested packages:
  gpm ctags vim-doc vim-scripts
The following NEW packages will be installed:
  libgpm2 vim vim-common vim-runtime xxd
0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.
Need to get 6766 kB of archives.
After this operation, 31.2 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
<省略>
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/view (view) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/ex (ex) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in auto mode

vim --versionでズラッと情報が表示されればOK。

root@e3dd01310898:/# vim --version
VIM - Vi IMproved 8.0 (2016 Sep 12, compiled Sep 30 2017 18:21:38)
Included patches: 1-197, 322, 377-378, 550, 703, 706-707
Modified by pkg-vim-maintainers@lists.alioth.debian.org
Compiled by pkg-vim-maintainers@lists.alioth.debian.org
<省略>

次にapt remove vimでアンインストールします(途中、続行するか問われるのでYを入力)。

root@e3dd01310898:/# apt remove vim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  vim-common vim-runtime xxd
Use 'apt autoremove' to remove them.
The following packages will be REMOVED:
  vim
0 upgraded, 0 newly installed, 1 to remove and 0 not upgraded.
After this operation, 2431 kB disk space will be freed.
Do you want to continue? [Y/n] Y
(Reading database ... 8315 files and directories currently installed.)
Removing vim (2:8.0.0197-4+deb9u1) ...

vim --versionで確認。

root@e3dd01310898:/# vim --version
bash: /usr/bin/vim: No such file or directory

これでアンインストールが完了しました。ちなみに、.vimrcのような設定ファイルも同時に削除したい場合はapt remove vimの代わりにapt purge vimを実行します。

参考

シェルスクリプトでよく見る「#!/bin/sh」について

shebang(シバンまたはシェバン)と呼ばれるものらしく、スクリプトを読み込むインタープリタを指定する事ができて、実行時にshbashを指定する必要がなくなるとのこと。

試してみる

任意のディレクトリにsayhelloというファイルを作成して下記のように記述する。

#!/bin/sh
echo Hello!!

実行権限を付与すると./sayhelloのような形で実行することができる。

$ chmod u+x sayhello
$ ./sayhello
Hello!!

さらに、パスを通すことでコマンドのように実行することもできる。

$ export PATH=$PATH:`pwd`
$ sayhello
Hello!!

また、インタープリタにはshやbashだけでなくrubyを指定することもできるとのこと。sayhelloファイルを以下のように修正して同じように実行してみる。

#!/usr/bin/ruby
puts 'Hello!!'
$ sayHello
Hello!!

rubyインタープリタを指定した状態でecho Hello!!などとするともちろん怒られてしまう。

$ sayhello
/Users/ageage/bin/sayhello:2: syntax error, unexpected end-of-input

独自のコマンドを作成してみる

git logコマンドの短縮形であるglogコマンドを作ってみる。

glogというファイルを作成して下記のように記述する。

#!/bin/bash
git log "$@"

パスを通してから、ローカルリポジトリで使用してみると...

$ glog
commit 59ec0c23c557a7251f1dff64869413422af85faa (HEAD -> master)
Author: ageage <ageage0830@gmail.com>
Date:   Tue Apr 17 19:34:24 2018 +0900

    fix README.md

commit f2340ef8c825f0e685a5065e7502b6490836e47f
Author: ageage <ageage0830@gmail.com>
Date:   Tue Apr 17 19:33:34 2018 +0900

    initial commit.

できた!下記のようにオプションを付与することもできる!

$ glog --oneline
59ec0c2 (HEAD -> master) fix README.md
f2340ef initial commit.

感想

色々と試してみて、自身の中でのshebangの認識が「おまじない」から、ちゃんと意味のある記述に変わったのでとても良かったです。rubyインタープリタを指定する際はenvなどを使用したり、もっと別の書き方もあるようですが、その辺りについては今後作業を行いながら吸収していければと思います。独自のコマンドの作成については、他のコマンドとの衝突には気をつける必要がありそう。むやみに作ってしまうと嵌ってしまう原因にもなりかねないので程々にしておこうかと思います。

※2018年5月10日 追記

このようなケースではaliasを使ったほうが良いですね。。。

参考

CSSで特定の属性を持つ、かつ、特定の属性を持たないセレクタの指定方法

属性セレクタで、特定の属性を持つ要素を選択する場合は以下のようになります。

/* attr属性を持つ要素がマッチする */
[attr] {
  ...
}

特定の属性を持たない要素を選択する場合は:not()擬似クラスを使用します。

/* attr属性を持たない要素がマッチする */
:not([attr]) {
  ...
}

これらを以下のように組み合わせることで実現できます。

/* attr1属性を持つ、かつ、attr2属性を持たない要素がマッチ */
[attr1]:not([attr2]) {
  ...
}

サンプル

See the Pen RybPxN by age (@ageage) on CodePen.

これは見た目のみの制御ですが、上記のようなセレクタJavaScriptでも使用できるので実際にリンクを無効にする処理などにも使えます。

感想

CSSセレクタはとてもたくさんの記述方法があるので、その都度調べるのが良さそうな感じがしますね。

参考