くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Androidのツール属性について

今回はAndroidのレイアウト作成時に使うツール属性について書いていこうと思います

ツール属性は主にレイアウトのプレビュー確認で使う名前空間です

<TextView
    android:id="@+id/cardTextView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:text="TextView"
    android:textAlignment="center"
    android:textSize="18sp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

例えば上記のレイアウトが存在するとして、android:から始まるものは実行時にどう表示するかの設定を行い、tools:から始まるものはプレビューで確認するときの設定を行います

ツール属性の設定

toolsを使えるようにするためにはxmlファイルのルート要素(1番上の要素)に下記を追加します。

設定はこれだけです。次にどんなことができるのかを見ていきましょう!

xmlns:tools="http://schemas.android.com/tools"

ツール属性の種類

ここからはツール属性の種類を見ていきましょう

tools:text

まずは個人的によく使うtools:textです

これは固定文言ではなく、場合によって文言が変更する場合に設定した文言をプレビューでどう表示されるか確認するために使います

tools:text="TextView"

上記を例えば、TextViewに設定するとプレビューでTextViewと表示されます。実際にアプリを起動した場合にはTextViewとは表示されないのであくまでも確認のために使われます

tools:ignore

レイアウトの警告を無視したい場合に使います

本当に無視していいものなのかは確認して無視するべきものだけに使うべきだと個人的に思っているので、ほとんど使わないと思います

ある要素に設定するとその要素と全ての子要素に適用されるので、レイアウトファイルの全てに適用したい場合はトップの要素に記述します

<ImageView
    android:layout_width="200dp"
    android:layout_height="200dp"
    tools:igonre= "contentDescription"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:src="@tools:sample/backgrounds/scenic" />

tools:src

tools:textに似ていますが、レイアウトの画像を確認したい場合に使います

<ImageView
    android:layout_width="200dp"
    android:layout_height="200dp"
    tools:igonre= "contentDescription"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:src="@tools:sample/backgrounds/scenic" />

tools:context

tools:contextはレイアウトファイルのトップ要素に追加してどのActivityやFragemtをプレビューで表示させるかを設定するものです。

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".tutorial.MainActivity">

</androidx.constraintlayout.widget.ConstraintLayout>

tools:layout

tools:layoutはFragementで使うもので、プレビューでフラグメント内に描画されるレイアウトを宣言できます

下記のように設定することでフラグメント内に描画されるレイアウトをプレビューで確認できるようになります

<fragment android:name="com.example.main.ItemListFragment"
    tools:layout="@layout/list_content" />

tools:layout

tools:layoutはFragementで使うもので、プレビューでフラグメント内に描画されるレイアウトを宣言できます

下記のように設定することでフラグメント内に描画されるレイアウトをプレビューで確認できるようになります

<fragment android:name="com.example.main.ItemListFragment"
    tools:layout="@layout/list_content" />

tools:itemCount

tools:itemCountはRecyclerViewで使うもので、指定した個数分のアイテムをプレビューで表示させます

実際のデータとは異なるので注意です

<androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:itemCount="3" />

開発時にビルドせずプレビューで確認できるので、便利な機能です

ここまで個人的に使うものを羅列しましたが、ツール属性はまだまだあります

公式ドキュメントのリンクを参照に載せておくので、一度見てみてください

参照

developer.android.com

ViewPager2とCardViewでカルーセル表示を行う

今回はCardViewのカルーセル表示についてです

ルーセル表示とは横にスライドして次のデータを表示させるものです

下記のようなイメージのものを作成します

f:id:kumaskun:20211031195830p:plain

1. レイアウト作成

まずはレイアウトを作成します

大まかにはViewPager2のレイアウトとCardViewのレイアウトを作成します

<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".dashboard.DashboardFragment">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/cardPager"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_marginTop="16dp"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

まずはViewPager2からです

色々設定していますが、重要なのはandroid:orientation="horizontal" の部分です。ここを設定することで横にスライドします。逆にverticalを設定すると縦にスライドするようになります

次にCardViewのレイアウトです

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="36dp"  // 横に見せる範囲とカードのマージンを足した値
    android:layout_marginEnd="36dp"   // 横に見せる範囲とカードのマージンを足した値
    app:cardBackgroundColor="#E91E63"
    app:cardCornerRadius="16dp"
    app:cardElevation="7dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="200dp">

        <TextView
            android:id="@+id/cardTextView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="TextView"
            android:textAlignment="center"
            android:textSize="18sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

重要なポイントは2つあります

1つ目はCardViewのandroid:layout_marginStart="36dp"android:layout_marginEnd="36dp" の部分です

ここでは横のアイテムを表示させるためにマージンを設定しています

どういう値かというと下記を見てもらうとわかると思います

f:id:kumaskun:20211031201915p:plain

ここではマージンを次のアイテムとの距離、オフセットを次のアイテムをどれくらい表示させるかの値とします

CardViewで設定するmarginStartとmarginEndはマージンとオフセットの合計にします

2つ目はCardViewのheightとwidthはmatch_parentに設定しないとExceptionが発生します

java.lang.IllegalStateException: Pages must fill the whole ViewPager2 (use match_parent)のエラーです

あとは自分の好みでカスタマイズしてみてください

2. ソースコードでの設定

レイアウトを先ほど作成しましたが、それだけでカルーセル表示にはなりません。ソースコードで設定が必要になりますので見ていきましょう!!

CardViewを使うクラスで下記設定が必要になります

cardPager.adapter = CardSlideAdapter(listOf(1, 2, 3, 4))

val margin = view.context.resources.getDimension(R.dimen.card_margin)
val offset = view.context.resources.getDimension(R.dimen.card_offset)

cardPager.offscreenPageLimit = 2
cardPager.setPageTransformer { page, position ->
    val offset = position * (2 * offset + margin)
    page.translationX = -offset
}

1つづつ説明します

まず、CardSlideAdapterの部分はカードのデータをAdapterで定義しています。細かくは割愛します

marginとoffsetはdimensファイルで定義しています。直接数字指定すると表示がおかしくなる可能性が高いので、dimensファイルで定義するか数値をdpに変換する処理を行なってください

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="card_offset">16dp</dimen>
    <dimen name="card_margin"> 20dp</dimen>
</resources>

cardPager.offscreenPageLimit = 2の部分は左右のアイテムを描画するために必要です。これがないと左右のアイテムが表示されないです

そして、setPageTransformerはページが変わった場合の処理を行います。アニメーションを変えたり設定値を変更したりします

今回は左右の表示させる値とマージン分移動を打ち消しています。(page.translationX = -offsetの部分)

そうしないと1ページで1つ分のカードしか表示できなくなります

今回紹介した箇所以外にsetPageTransformerで色々なカスタマイズができると思います。それは試してみたら追記したいと思います

参照

developer.android.com

satoshun.github.io

iOSDC Japan 2021 に参加した

iOSDC Japan 2021 に参加してきたので、ブログにしようと思います

iosdc.jp

iOSDC Japanとは?

iOSDC Japan 2021はiOS関連技術をコアのテーマとしたソフトウェア技術者のためのオンラインカンファレンスです。 日本中、世界中から公募された100本を超える知的好奇心を刺激するトークの他にも、パンフレットに掲載された技術記事、参加者であれば誰でも作れる即興のトーク・アンカンファレンスなど、初心者から上級者まで楽しめるコンテンツがみなさんを待っています!

今年、iOS15が発表されたり、Swift5.5が発表されたりしたので最新情報をまとめたものを発表したり、自分の会社の説明やこういう実装の仕方をしていますというのを共有したものが多かった印象です

自分にとっては自社以外の開発が知れたり他の人の考え方が知れて驚きが多く、モチベーションが上がる機会でした

また、オンラインで開催されるのが個人的にはよかったと思っています

2〜3日前に今年初めて都内に行ったくらいなので、会場に行くより自宅で参加できるのは大きかったです。オフラインで開催される流れになってもオンラインは残ってほしいと思いました

ask the speakerというシステムがあり、スピーカーに質問できることやDiscordで交流ができるのでオンラインでも皆で楽しめるのでオフライン寄りのオンラインイベントだと感じました

そして、オンライン開催ならではなのが、見逃たものを後で見れることです

ニコニコ生放送で開催されたのですが、見逃したり見たいものが同じ時間に開催されても後から視聴可能です。仕事で見れないセッションがあったり、見たいものがかぶったりしたのでありがたいです

ここからは個人的に今年の良かったセッションを少しまとめてみようと思います

noteのiOSアプリで実装したアクセシビリティの全て

fortee.jp

noteでのアクセシビリティ向上のために実施してきた内容を紹介するセッションでした

アクセシビリティに力を入れるのはなかなか難しいものがある中で、単純にすごいと思いました

ただ、セッションを聞いていく中でアクセシビリティが良くないのははバグなんだと思うようになり、アクセシビリティに力を入れるのはハードルが高い対応ではなく、当たり前の対応なんだと思えるようになりました

バックグラウンドでアプリがキルされても怖くない!アプリの状態を元に戻すリストア機能の全て

fortee.jp

アプリがキルした後にリストアする方法を紹介するセッションでした

リストア対応を以前行ったときにはすごく大変だったイメージがあったのですが、今回のセッションでとてもシンプルに対応できるなあと感じました

正直、もっと前に知りたかったです

ケースに応じたUICollectionViewのレイアウト実装パターン

fortee.jp

UICollectionViewのレイアウト実装をパターン別に紹介するセッションでした

UICollectionViewはほとんどのアプリで使われる、なおかつFlowLayoutやCompotisionLayoutなどいろんな方法がありどの方法がいいのか悩むことがあったので、今回のセッションである程度使い分けができそうな印象でした

ちょうど、UICollectionViewのレイアウト実装で色々調べていたというのもあり、理解が深まりました

最後に

主催者・スポンサー・発表者・参加者などいろんな人が1つのカンファレンスを楽しめるイベントだったと思いました

来年も開催されると思っているので、来年はよりカンファレンスに参加できたらいいと思いました

Callkitで着信画面を表示させる

今回はCallkitについてです

Callkitは下記のようなiPhone標準の着信画面を表示させる機能です。画面がOFFの状態であれば、スライドして応答する画面が表示されます。また、着信履歴にもデータが残ります

f:id:kumaskun:20210919214412p:plain

SkypeやLINEなどで電話をかけたときに、着信時に同じ画面が表示されていると思います。それはCallKitというiOSの公式フレームワークを使っています。

着信側の端末はPushKitと呼ばれるライブラリによってPush通知を受信し、これをトリガーにCallKitと呼ばれるライブラリを利用して着信画面を表示します。

iOS13以降からVoIP Push (PushKit)は着信通知専用に仕様変更されたことにより、PushKitとCallKit はセットで使うことが必須となりました。

ただ、単純に該当のメソッドを呼び出せばいいわけではありません

正しい順番を守って呼び出さないと、システム側がPushKitの呼び出しに失敗したと判断され、着信が来なくなることがあります

着信画面を表示させるためには以下の手順を踏む必要があります

  1. PushKitからVoIPのPushを受け取る
  2. CallKit 着信画面を表示
  3. reportNewIncomingCallのcompletionHandler内部でPushKit のcompletionHandlerを呼び出す

PushKitの設定

まずはPushKitで通知を送る設定が必要になります

VoIPのPushを受け取るためには、基本的にはAPNS通知を行うときと同じですが、Apple DeveloperでCertificateを作るときにVoIP用のチェックボックスがあるので、チェックして証明書を作成します

f:id:kumaskun:20210919220642p:plain

そして、Xcodeの設定で、Signinig&Capabilityの下記にチェックを入れましょう

f:id:kumaskun:20210919221004p:plain

また、ペイロードの設定などに注意しましょう。下記に記載されていますが、Sandboxの場合に届かなかったりするので注意が必要です。

developer.apple.com

ここまで設定するとPushKitの通知が送れるはずです。

PushKitの通知を受け取る処理は下記になります

extension AppDelegate {
    
    private func initVoIP() {
        var voipRegistry = PKPushRegistry(queue: .main)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [.voIP]
    }
    
}

extension AppDelegate: PKPushRegistryDelegate {

    // PushKitからVoIPPush用のトークンを受け取る
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type{
}

    // VoIPPushを受け取ったときの処理
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        // 通知の形式が .voIP の場合に後続の処理を行う
        guard type == .voIP else {
            completion()
            return
        }
        
        let userInfo = payload.dictionaryPayload
        
        // Callkitを表示させる処理を行う
    }
}

PKPushRegistryDelegateでPushKitからVoIPPushを受け取り、Callkitを表示させる処理を行います

基本的に上記の処理はAppDelegateで行います

ここまでで着信のPush通知が届くので、次に着信画面を表示させる流れになります

Callkitの設定

次に着信画面を表示させるCallkitの処理を行います

まずはCallkitの初期化を行います。今回はシングルトンで作成しています

import CallKit

final class CallKitManager {
    
    private(set) var callKitProvider: CXProvider!
    private(set) var callKitController = CXCallController()
    var uuid = UUID()
    
    static let shared = CallKitManager()
    
    private init() {
        let configuration = CXProviderConfiguration(localizedName: "TEST".localize())
        configuration.maximumCallGroups = 1
        // 最大通話人数
        configuration.maximumCallsPerCallGroup = 1
        // 扱うタイプ
        providerConfiguration.supportedHandleTypes = [.phoneNumber] 
        // 着信音の設定
        configuration.ringtoneSound = "〇〇.mp3"
        callKitProvider = CXProvider(configuration: configuration)
    }
}

CXCallControllerは通話の管理インタフェースを提供しているクラスです。

developer.apple.com

CXProviderConfigurationは挙動や表示を制御するためのプロパティです

developer.apple.com

localizedNameは着信で表示されるアプリ名を設定します

次に着信処理を行います。

// 通話相手の名前設定
let callHandle = CXHandle(type: .generic, value: "TEST君")

let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
// 他の着信が来たら受け取るかどうか
callUpdate.supportsHolding = false
// グループ通話を行うかどうか
callUpdate.supportsGrouping = false
// ビデオ通話を行うかどうか
callUpdate.hasVideo = false

provider.setDelegate(self, queue: .main)
 provider.reportNewIncomingCall(with: CallKitManager.shared.uuid, update: callUpdate, completion: {_ in})

CXProviderクラスのreportNewIncomingCallが着信画面を表示するAPIです。

CXHandle のインスタンスに発信者の情報をセットします

電話番号やメールアドレスなどを指定すると、標準の連絡先アプリに登録されている情報を元に発信者名の表示や、画像が登録されていれば着信画面に表示されます

また、reportNewIncomingCallの先頭の引数に通話毎にユニークな uuid を渡しています

着信・発信・拒否・保留等の通話における各アクションの挙動はCXProviderDelegateのメソッドとして定義します(Callkitを表示させるViewcontrollerで行います)

extension CallKitViewController: CXProviderDelegate {
    // リセット時
    func providerDidReset(_ provider: CXProvider) {
        action.fail()
    }
    
    // 通話終了・着信を拒否した時
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill(withDateEnded: Date())
    }
    
    // タイムアウト時
    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {        
        action.fulfill()
    }
    
    // 発信時
    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    }
    
    // 着信を許可した時
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }
}

action.fulfill()は着信を受け取ったり拒否したりのアクションを終了させる処理です

通話発信や終了などを行う場合

通話発信や終了などを行う場合にはCXTransactionにアクションのインスタンスを登録してリクエストする必要があります

developer.apple.com

let endCallAction = CXEndCallAction(call: CallKitManager.shared.uuid)
 let transaction = CXTransaction(action: endCallAction)

CallKitManager.shared.callKitController.request(transaction) { error in
      if let _ = error {
          CallKitManager.shared.callKitProvider.reportCall(with: CallKitManager.shared.uuid, endedAt: nil, reason: .remoteEnded)
      }
}

上記は通話終了のアクションです

iOSでは標準の電話アプリの通話も含めてCXCallControllerで管理されるため、インスタンスを経由して発信や終話の状態をOSに通知する必要があります

OSに通知するというのはrequest(transaction)で行います

通話を開始する場合には上記のCXEndCallActionCXStartCallActionに変更します

まとめ

最低限しか記載していませんが、通話アプリの着信画面ではこんなことが行われているという大枠は理解できると思います

今回記載したこと以外に、着信履歴からリダイアルなど標準の電話とほとんど同じ機能が実現できるはずなので試してみてください!!

なぜ?突然Githubにpushできなくなった

いつも通りGitHubにpushしようとしたら、突然エラーが発生するようになりました。

fatal: unable to access 'https://github.com/ユーザー名/リポジトリ名.git/': The requested URL returned error: 403

リポジトリ名は合っているし、なんだろう?

ssh接続しているので、GitHubにアップしている公開鍵が何らかの問題を起こしているのかと思い、見てみたのですが、特に問題なし

えー???と思いながら調べてみるとpush時のエラーに下記のようなメッセージがありました

remote: Please see https://github.blog/2020-07-30-token-authentication-
requirements-for-api-and-git-operations/ for more information.

リンク先を見てみると、今まではローカルでGitHubにアクセスするにパスワード認証を使用していたのですが、脆弱性防止のために個人アクセストークン認証に変更になったそうです

なので、パスワードではなく、個人アクセストークンに変更する必要があります。(メールアドレスとユーザー名はそのまま使います)

個人アクセストークンは下記に作成方法が記載されています。

docs.github.com

個人アクセストークンが表示されたページは再表示はできないのでメモしてください

プッシュ時のパスワードに個人アクセストークンを入力するとpushできるはずです

1度プッシュすると今後は必要ないと思いますが、登録されているかを確認するのはキーチェーンアクセスでできます

キーチェーンアクセスでgithub.comと検索するとgithub.comという名前で表示されるはずです

もしパスワードが更新されていないのであれば、1度登録されている認証情報を削除して登録すれば問題なと思います

SourceTreeを使っている場合には下記記事にある対応も必要になります。すごくわかりやすいので参考になると思います。

zenn.dev

UIViewのレイアウトライフサイクルについて

今回はUIViewの表示についてまとめようと思います。

AutoLayoutが効かない・Viewの表示位置がおかしい・表示されない・画面表示までが遅いことが実装している中で1度は経験はあると思います。

このような問題は、Viewの表示の流れを理解せずに開発していることが原因かもしれません。

UIViewのレイアウトライフサイクルは下記の順で行われます。

  1. 制約の更新
  2. フレームの更新
  3. レンダリング

それでは、1つずつ見ていきましょう!

制約の更新

以下の条件で、Viewの制約が変更されるとUIViewクラスのupdateConstraints()が呼び出され制約の再計算が行われます。

  • 制約のactiveフラグの有効・無効の切り替え
  • 制約の優先度の変更
  • 制約の追加・削除
  • 制約が与えられたviewの階層の変更

updateConstraints()はシステム側がレイアウトパフォーマンスを考慮したタイミングで呼び出すので、意図的に呼び出してはいけません。

ただし、パフォーマンスを大きく損なうような制約の更新などの場合は updateConstraint() 内で行なった方がいいです。

これは、updateConstraint() がレイアウトののバッチ処理を行いメインスレッドをブロックしないためです。

使い方としては、UIViewのサブクラスでメソッドをoverrideして使用します。

意図的に制約の計算を行いたい場合には、下記のメソッドを使いましょう。

  • updateConstraintsIfNeeded()
  • setNeedsUpdateConstraints()

2つの違いはsetNeedsUpdateConstraints() は指定された ViewとそのSubviews がシステムのタイミングによって updateConstraint() を呼ぶようになり、updateConstraintsIfNeeded() は更新を即座に行います。

ただ、この2つのメソッドは、制約の更新は行われるが次に説明するフレームの更新の実行は保証されているわけではないです。

フレームの更新

制約が更新されるとlayoutSubviews()が呼び出され、フレームが更新されます。

フレームの更新というのはあるViewのサイズに合わせてSubViewやlayerの配置を更新することです。

layoutSubviews()が呼び出されるとupdateConstraintsIfNeeded()も呼び出され、必要であれば制約も更新されます。

ただし、制約の更新と同じようにlayoutSubviews()は意図的に呼び出してはいけません。

意図的に更新したい場合には、下記のメソッドを使います。

  • setNeedsLayout()
  • layoutIfNeeded()

2つの違いはsetNeedsLayout()は 次の再描画が行われる際に更新されるのに対し、layoutIfNeeded()は更新を即座に行います。

setNeedsLayout()は更新リクエストのフラグをONにするイメージです。

なので、明示的にフレームの更新を行った後に何らかの処理を実行する場合はlayoutIfNeeded()を使用し、それ以外はsetNeedsLayout()を使ったほうがパフォーマンスが良いと言われています。

レンダリング

フレーム情報が更新されると画面に表示されるためにレンダリングが行われ、draw(rect:)が呼び出されます。また、以下の条件で、draw(rect:)が呼び出され再描画を行います。

  • Viewの追加・削除
  • 非表示になっていたViewの再表示
  • Viewを画面外までスクロールし、再び画面内に戻す
  • ViewのsetNeedsDisplay(), setNeedsDisplayInRect(_:)の明示的な呼び出し

ここまでUIViewが描画されるまでの流れを書いていきました。もう少し細かいところまで調べて追記していきたいと思います。

参照

developer.apple.com

developer.apple.com

developer.apple.com

developer.apple.com

qiita.com

スコープ関数について

kotlinにはスコープ関数というものがあります。

あるインスタンスに対して、連続して処理をするような場合、スコープ関数を使えばコードを簡潔にすることができます。(Lamba形式で処理をおこないます)

スコープ関数にはletrunwithapplyalsoの5種類あります。

それぞれ説明しようと思います。

let

まずはletについてです。

letは個人的に1番使っているので、最初に説明します。

どんなときに使うかというと、nullチェックで行うことが多いです。

val test = "test"

val str = test.let {
    it.toUpperCase()
}

print(str)

上記のprintlnで表示されるのはstrがnullではない場合のみです。また、itはtestを表すレシーバーです。

Lambaの引数はitでitはスコープ関数で使わないといけません。また、戻り値として利用できます。

run

runはletとほとんど変わらないですが、レシーバーをthisにする必要があります。

val test = "test"

val str = test.run {
    this.toUpperCase()
}

print(str)

ただ、runはExtensionとして使用するパターンとExtensionとして使用しないパターンの両方で使えます。

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}

上記はT.runのように使わずにrun {}というように使っています。

なので、runはオペレーション(操作)だけでなく、初期化のためにも使えます。

with

withに関して、働きはrunと同じです。そのため、withはあまり見かけることは無いかもしれません。

特徴としては、Extensionとして使用しないパターンです。

withは通常の関数のように呼び出し、2つの引数を取ります。第一引数にレシーバ、第二引数にラムダ式を記述しましょう。レシーバーはthisです。

レシーバーのthisは省略できます。

val test = "test"

val str = with(test) {
    this.toUpperCase()
}

print(str)

apply

ここからはlet・run・withとは考え方は同じですが、用途が異なります。

applyはプロパティを設定するために使用します。

val book = Book().apply{
        price = 827
        author = "AAAAA"
}
println(book.author)

上記のコードはBookインスタンスを生成したらプロパティを設定するコードです。

apply関数のラムダ式の中では、レシーバ(ここではBookインスタンス)をthisで参照することもできますが、このthisは省略することが可能です。

戻り値はプロパティが設定されたレシーバになります。(ここではBookインスタンス)

also

alsoはapplyと似た使い方ですが、明確に違う点があります。

alsoの中のラムダ式で、Bookインスタンスは「it」で参照されていて、省略不可です。

val book = Book().also{
    it.price = 827
    it.author = "AAAAA"
}

上記のitは省略できません。

まとめ

ここまでスコープ関数を説明しましたが、全てを使うことはほとんどないかもしれません。下記にまとめますが、用途に応じてしっくりくるものを使ってみてください。

レシーバー 戻り値 用途
let it 指定可能 オペレーション
run this 指定可能 オペレーション
with this 指定可能 オペレーション
apply this 対象オブジェクト プロパティ指定
also it 対象オブジェクト プロパティ指定

参照

kotlinlang.org

pouhon.net

Androidのスレッドについて

今回はAndroidのプロセスやスレッドについてまとめようと思います。

最近は非同期処理でCoroutine などが出てきたおかげで非同期処理が簡単にできるようになりました。

ただ、Coroutine などを理解する前にプロセスやスレッドについて理解する必要があると思い、簡単にまとめようと思います。

スレッドについて

Androidは、処理を行うためのベルトコンベア(スレッド)が1本しか存在しないシングルスレッドモデルです。

なので、重い処理が走った場合はアプリが止まってしまう可能性があります。

それを回避するために、一時的に別のベルトコンベア(サブスレッド)で処理が出来る仕組みが用意されています。

流れとしては、アプリの起動時にアプリ実行用のスレッドを作成します。

これは、メインスレッド(UI スレッド)と呼ばれていて、イベント(描画イベントを含む)を適切なユーザー インターフェースウィジェットに送信する役割を担うため非常に重要です。

また、これはほとんどの場合、アプリが Android UIツールキットのコンポーネント(ViewやButtonなど)とやり取りをするスレッドでもあります。

つまり、UI更新などやボタンのクリックなどUIの操作はUI スレッドで行う必要があり、重い処理(APIなど)を行う場合にはワーカースレッドで行う必要があります。

また、Android UI ツールキットはスレッドセーフではありません。

したがって、ワーカー スレッド(サブスレッド)からUIを操作するべきではありません。

ユーザー インターフェースに対するすべての操作は、UI スレッドから行う必要があります。そのため、Android のシングル スレッド モデルには下記のルールがあります。

  • UI スレッドをブロックしない
  • UI スレッド以外から Android UIツールキットにアクセスしない

下記がイメージです。ワーカースレッドはUIスレッドとは別の処理のコンベアのようなイメージです。

f:id:kumaskun:20210725195236p:plain

スレッドの作成

次はどうやってスレッドを作成するかについてです。

スレッドの作成については何パターンかあるので、説明したいと思います。

①handler

Handler は Android が提供するライブラリです。

別スレッドからアプリの UI を操作するのにLooperHandlerというしくみを使うことができます。

LooperはUIスレッドにあらかじめ用意されています。また、Looper は処理対象のリスト(キュー)を持っていて、そこに登録されたスレッドを順番に実行します。

Looperのリスト(キュー)への登録など、LooperとのやりとりはHandlerを使って行います。

f:id:kumaskun:20210725212019p:plain

実際のソースコードは下記です。

// Handlerのオブジェクト作成
val handler = Handler(Looper.getMainLooper())

// 別スレッドを作成
val runnable = object : Runnable {
    override fun run() {
        // 別スレッドで行う処理
    }
}

// 別スレッドを実行
handler.post(runnable)

Runnableは別スレッドを作成するインターフェイスで、run()のなかで別スレッドで行う処理を定義します。

そして、別スレッドで処理を行ったら、Handlerを介して、UIスレッドで処理を行います。

val handler = Handler(Looper.getMainLooper())となっているのはLooperはスレッドごとにあり、UIスレッドのLooperに属しているHandlerであることを明示しています。

また、別スレッドの結果をUIスレッドで更新するのは下記runOnUiThreadで行えます。

runOnUiThread {
    // UIスレッドで行う処理
}

②AsyncTask

Handlerとは別にAsyncTaskという非同期処理用クラスがあります。

使い方は下記のようになります。

class TestAsyncTask : AsyncTask<Void, Int, Void>() {

      // 別スレッドの処理を行う前にUIスレッドで処理を行う場合
        override fun onPreExecute() {
        }

      // 別スレッドの処理
        override fun doInBackground(vararg param: Void?): Void? {
        }


      // 別スレッドの処理が終わり、UIスレッドで行う場合
        override fun onPostExecute(result: Void?) {
        }

    }

上記のようなAsyncTaskクラスを作成して、インスタンス化して呼び出して、execute()を実行することで行えます。(TestAsyncTask().execute()など)

また、上記のAsyncTask<Void, Int, Void>のVoid, Int, Voidの部分はParamsProgressResultと定義されており、それぞれ下記を意味するので、必要に応じて変更します。

Params→別スレッドの処理に渡すパラメータ(doInBackgroundに渡される型)

Progress→進捗状況を更新する処理に渡される値の型

Result→別スレッドの処理の結果の返り値

AsyncTaskはAPI30からは非推奨となっているので、今後使うことはあまりないと思います。

2つのパターンを例に出しましたが、あまり使うことがないかもしれません。

例えば、APIの処理を行う場合にはokHttpやRetrofitなどはライブラリが勝手にワーカースレッドで処理を行うなどしてくれたり、RxjavaやCoroutineなどあまり意識せず、UIスレッドで処理を行ったりできるためです。

まとめ

AndroidではUIスレッドとワーカースレッドがあり、意識して実装しないと思いもよらない例外が発生してアプリがクラッシュすることがあります。

最近はライブラリが進歩してスレッドをあまり意識せずに開発ができるようになりました。

ただ、想定外のことが起きたときに概念を知っておくのと知らないのでは解決までのプロセスに差ができると思っているので、概念の理解の一助になると幸いです。

参照

developer.android.com

developer.android.com

engineer-club.jp

kommkett.co.jp