しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

StoreKit2のサンプルコードを動かしてみる

今回は下URLからダウンロードできるStoreKit2のサンプルコードを動かしてみました。

developer.apple.com

StoreKit2は旧来のStoreKitを改良したもので、シンプルなコードで購入処理が記述できるようになっています。
ただしiOS15限定以上なので利用する際はアプリの対象OSを15以上にするか旧StoreKitと併用する必要があります。

StoreKitの概要 - Apple Developer

StoreKit2の処理

商品情報の取得方法は下の通りです。
こちらはサンプルコードを少し分かりやすく改変したものになります。
StoreKit2ではawaitを使う事でブロックやDelegateを使わずに取得できるようになりました。

let products = try? await Product.products(for: ["com.example"])

購入処理下の通りです。
purchaseメソッドだけで購入できるので今までに比べて非常に楽になりそうです。

戻り値の型はVerificationResultなのでswitch文でTransactionを取得してfinishさせています。

func purchase(_ product: Product) async throws -> Transaction? {
    //Begin a purchase.
    let result = try await product.purchase()

    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)

        //Deliver content to the user.
        await updatePurchasedIdentifiers(transaction)

        //Always finish a transaction.
        await transaction.finish()

        return transaction
    case .userCancelled, .pending:
        return nil
    default:
        return nil
    }
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    //Check if the transaction passes StoreKit verification.
    switch result {
    case .unverified:
        //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
        throw StoreError.failedVerification
    case .verified(let safe):
        //If the transaction is verified, unwrap and return it.
        return safe
    }
}

過去の購入情報はTransactionのlatestメソッドで取得する事ができます。
購入時同様、VerificationResult型が返ってくるのでswitch文で取得して検証しています。

func isPurchased(_ productIdentifier: String) async throws -> Bool {
    //Get the most recent transaction receipt for this `productIdentifier`.
    guard let result = await Transaction.latest(for: productIdentifier) else {
        //If there is no latest transaction, the product has not been purchased.
        return false
    }

    let transaction = try checkVerified(result)

    //Ignore revoked transactions, they're no longer purchased.

    //For subscriptions, a user can upgrade in the middle of their subscription period. The lower service
    //tier will then have the `isUpgraded` flag set and there will be a new transaction for the higher service
    //tier. Ignore the lower service tier transactions which have been upgraded.
    return transaction.revocationDate == nil && !transaction.isUpgraded
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    //Check if the transaction passes StoreKit verification.
    switch result {
    case .unverified:
        //StoreKit has parsed the JWS but failed verification. Don't deliver content to the user.
        throw StoreError.failedVerification
    case .verified(let safe):
        //If the transaction is verified, unwrap and return it.
        return safe
    }
}

まとめ

簡単に動かしてみましたが従来に比べて処理がシンプルになりそうで良かったです。
シンプルんになっただけでなく返金対応が増えていたり継続課金の検証が簡単だったりするようなので実際に使う時は更に詳しく調べていきたいと思いました。

ShazamKitで音声認識を試してみる

下の記事を参考にShazamKitを動かしてみました。

ichi.pro

ShazamKitは音楽認識ライブラリで、マイク経由で曲を渡すと曲名を出してくれたりします。
ShazamKitのiOS15から登場したライブラリで、SDKAndroidでも使えるようです。

developer.apple.com

初期設定

まずは使いたいアプリのBundleIDのShazamKitを有効化します。
AppleのMemberCenterにアクセスしてShazamKitを使いたいBundleIDを登録してから「App Services」にあるShazamKitにチェックを入れて保存します。

f:id:llcc:20210927135717p:plain

実装

新規にプロジェクトを作成します。
BundleIDは先程登録したIDを使います。

f:id:llcc:20210927140444p:plain

音声認識の際にマイクを使うので、Info.plistにNSMicrophoneUsageDescriptionを追加してマイクを使う理由を記述します。

f:id:llcc:20210927140043p:plain

まずはマイクの音を取り込む実装をします。
ViewControllerを下のように修正します。

class ViewController: UIViewController {
    let engine = AVAudioEngine()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let audioSession = AVAudioSession.sharedInstance()
        try? audioSession.setCategory(.record)
        try? audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        
        let inputNode = engine.inputNode
        try? engine.start()
    }
}

起動するとマイクの音声を読み取る事ができるようになります。

f:id:llcc:20210927141056p:plain

次は読み取った音声を元に曲名を調べる実装をします。
ViewControllerを下のように変更します。

import UIKit
import ShazamKit

class ViewController: UIViewController {
    let session = SHSession() // 追加
    let engine = AVAudioEngine()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let audioSession = AVAudioSession.sharedInstance()
        try? audioSession.setCategory(.record)
        try? audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        
        let inputNode = engine.inputNode
        try? engine.start()
        
        // ここから追加
        session.delegate = self
        let format = inputNode.outputFormat(forBus: .zero)
        inputNode.installTap(onBus: .zero, bufferSize: 1024, format: format) { buffer, _ in
            self.session.matchStreamingBuffer(buffer, at: nil)
        }
    }
}

extension ViewController: SHSessionDelegate {
    func session(_ session: SHSession, didFind match: SHMatch) {
        print(#function)
        print(match.mediaItems)
    }
    
    func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
        print(#function)
        print(error)
    }
}

音声をShazamKitにわたす処理は下の箇所で行っています。

inputNode.installTap(onBus: .zero, bufferSize: 1024, format: format) { buffer, _ in
    self.session.matchStreamingBuffer(buffer, at: nil)
}

読み取り成功時や失敗時はSHSessionDelegateのメソッドが呼ばれます。

extension ViewController: SHSessionDelegate {
    func session(_ session: SHSession, didFind match: SHMatch) {
        print(#function)
        print(match.mediaItems)
    }
    
    func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
        print(#function)
        print(error)
    }
}

曲を流すと下のように正しく曲が認識されました。

f:id:llcc:20210927141534p:plain

日本の曲も試した所、正しく認識する事ができました。

f:id:llcc:20210927141813p:plain

Docker+Railsでサーバー立ち上げメモ

Docker+Railsでサーバーを立ち上げた時のメモです。
元々Dockerを使わずに運営していたのですが、CentOS6がサポート切れになったことやbundle installでよく苦戦する事もあって環境を作り直す事にしました。
今回のOSはmacOSになります。

今回の構成ですがまずはCentOSのImageにRailsMySQLを乗せていく形で作っていきます。
DBやアプリケーションサーバーを分ける構成が推奨されているのですが、いきなり分割するのは難易度が高いので最初は1つのコンテナにすべて詰め込みます。

ローカル環境構築

まずはMacにDockerをインストールします。
インストールは下のサイトから行いました。

hub.docker.com

続けてDockerfileを作っていきます。
ここではrbenvを使ったRubyのインストールをしています。

下の方にあるENVはパスを通すためのコマンドです。
Dockerの実行中は.bash_profileを読んでくれないのでENVコマンドでパスを通しています。

FROM centos:8

# Ruby
RUN yum -y install git gcc make bzip2 openssl-devel readline-devel zlib-devel
RUN git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
RUN cd ~/.rbenv && src/configure && make -C src
RUN git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
RUN echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile && echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
ENV PATH /root/.rbenv/bin:/root/.rbenv/shims:$PATH
RUN rbenv install 2.5.1 && rbenv global 2.5.1

続けて下コマンドでImageを作成します。
tオプションはImageの名前やタグを指定するためのものです。

docker build -t my_image .

buildの後にdocker imagesコマンドを打つとImageが作られている事が分かります。

f:id:llcc:20210807172951p:plain

Imageを消したい場合はdocker rmi xxx(ImageのIDか名前)で削除する事ができます。
ImageIDは先頭の数文字だけでも問題ありません。
すべて一気に削除したい時は下コマンドが便利です。

docker images -aq | xargs docker rmi

次はImageからコンテナを作成して起動します。
起動は下のコマンドです。

docker run -itd --name my_container -p 3000:3000 --privileged my_image /sbin/init

http://localhost:3000でDockerの3000ポートに繋ぎたかったので下のように-pオプションでポートを指定しています。
他オプションですがdはバックグラウンド起動のため、iはホストの入力をコンテナに送るため、tはコンテナからの出力を受けるためのものです。
--privilegedはホスト側などへのアクセス権限を与えるオプションです。
マウント周りで権限関連のエラーが出たので追加したのですが、本番環境などでは避けた方が良いかもしれません。

コンテナへのアクセスする場合は下コマンドを使います。
loginオプションはbash_profileの読み込みの為に入れています、ただDockerfileのENVでRubyのパスを追加したので除いてもいいかもしれません。

docker exec -it my_container bash --login

試しにRubyバージョンを確認した所、無事に2.5.1が入っていました。

f:id:llcc:20210807173800p:plain

execを使うとコンテナ上でコマンドを実行します。
そのためbash --loginをlsに置き換えると下のようにファイル一覧を表示します。

f:id:llcc:20210809153556p:plain

立ち上げたコンテナですが停止したい時はdocker stop xxx(コンテナのIDか名前)、削除したい時はdocker rm xxx(コンテナのIDか名前)を使います。
起動中のコンテナを削除したい時は下のようにrmにfオプションを付けます。

docker rm -f xxx(コンテナのIDか名前)

次にMySQLをインストールします。
Dockerfileの下部に下の行を追加してImageを作り直して実行します。

# MySQL
RUN yum -y install mysql mysql-server

起動すると下のように実行できるようになっています。

f:id:llcc:20210807213453p:plain

最後にRailsを導入します。
Dockerfile最下部に下の処理を追加します。

# Rails
RUN yum -y install mysql-devel gcc-c++ nodejs crontabs wget fontconfig-devel
RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
RUN gem install bundler --version=1.16.3
RUN mkdir /my_app
WORKDIR /my_app
COPY . /my_app
RUN bundle install

rails s の-bオプションは127.0.0.1で立ち上げるとうまく動かないためです。
こちらは下サイトを参考にしました。

docker上のアプリにlocalhostでアクセスしたらERR_EMPTY_RESPONSEが出る - Qiita

この状態でコンテナに入り下コマンドを実行するとサーバーを起動できます。

systemctl start mysqld
rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

MySQLのデータの永続化

次はデータベースのデータの永続化をします。
今はコンテナを削除するとデータが消えてしまうので、volumeを作る事で永続化しようと思います。
最初はホスト側にデータを保存しようと思ったのですが、MySQLの起動がうまくいかなかったのでvolumeを作る事にしました。

まずは下コマンドでvolumeを作成します。

docker volume create my_volume

docker volume lsコマンドを打つと今作ったvolumeを確認する事ができます。
削除はdocker volume rmコマンドを使います。

f:id:llcc:20210808195004p:plain

volumeはdocker runの-vオプションで指定します。
今回はMySQLのデータ部分をvolumeに保存したかったので-v my_volume:/var/lib/mysqlというオプションを付けました。

docker run -itd --name my_container -p 3000:3000 -v my_volume:/var/lib/mysql --privileged my_image /sbin/init

これでコンテナを作り直してもデータを保持するようになりました。

Docker Composeでコンテナを管理

次はDocker Composeを使ってコンテナを管理します。
Docker Composeを複数のコンテナを管理できるツールです。

まずは下コマンドでDocker Composeが入っていることを確認します。
入ってない場合は各環境に応じた方法でインストールします。

docker-compose version

次はDocker Composeの設定ファイルであるdocker-compose.ymlを作成して下のように記述します。

version: '3'
services:
  app:
    container_name: my_container
    privileged: true
    volumes:
      - my_volume:/var/lib/mysql
    ports:
      - 3000:3000
    command: /sbin/init
    build: .

volumes:
  my_volume:

起動コマンドは下のとおりです。
先程使っていたdocker runに比べると非常にシンプルになりました。

docker-compose up -d

コンテナの停止と削除は下コマンドになります。

docker-compose down

Imageの削除も行いたい場合は下のオプションを追加します。

docker-compose down --rmi all

データベースを別コンテナ化

Docker Composeを入れたので、コンテナもアプリケーションとデータベースで分けたいと思います。
下のようにdocker-compose.ymlにデータベースコンテナを追加します。
ホスト側からDBにアクセスしない場合はportsの部分は削除して構いません。

environmentのMYSQL_USERとMYSQL_PASSWORDではユーザーを作成しています。
MYSQL_DATABASEでデータベースの作成とそのデータベースへの権限付与を行います。

appの方にはdepends_onを追加します。
これによってdbを起動してからappを起動できるようになります。

version: '3'
services:
  app:
    container_name: my_container_app
    privileged: true
    volumes:
      - my_volume:/var/lib/mysql
    ports:
      - 3000:3000
    command: /sbin/init
    build: .
    depends_on:
      - "db"
  db:
    container_name: my_container_db
    image: mysql:8.0
    volumes:
      - my_volume:/var/lib/mysql
    environment:
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_DATABASE: my_db
      MYSQL_ROOT_PASSWORD: password
    hostname: db_container
    ports:
      - 3306:3306

volumes:
  my_volume:

起動するとデータベースのコンテナも作られている事が分かります。

f:id:llcc:20210809220013p:plain

ホスト側からは下コマンドでアクセスできるようになります。

mysql -h localhost -u test --protocol=tcp -p

次はアプリケーション用コンテナから今作ったデータベースを見るようにします。
まずはDockerfileから下のようにMySQL関連の処理を削除します。

FROM centos:8

# Ruby
RUN yum -y install git gcc make bzip2 openssl-devel readline-devel zlib-devel
RUN git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
RUN cd ~/.rbenv && src/configure && make -C src
RUN git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
RUN echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile && echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
ENV PATH /root/.rbenv/bin:/root/.rbenv/shims:$PATH
RUN rbenv install 2.5.1 && rbenv global 2.5.1

# Rails
RUN yum -y install mysql-devel gcc-c++ nodejs crontabs wget fontconfig-devel
RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
RUN gem install bundler --version=1.16.3
RUN mkdir /my_app
WORKDIR /my_app
COPY . /my_app
RUN bundle install

Railsのdatabase.ymlにDBコンテナのパスワードやホスト情報を追加します。

default: &default
  adapter: mysql2
  reconnect: false
  pool: 5
  username: user
  password: password
  host: db_container

development:
  <<: *default
  database: my_db

test:
  <<: *default
  database: my_db

production:
  <<: *default
  database: my_db

これでデータベースのコンテナを分割する事ができました。
あとは下コマンドでアプリサーバーを立ち上げれば完了です。

rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

起動に関する処理の自動化

次は上に記載したrails sなども自動化していきます。
まずは自動化用のファイルdocker_app_command.shを作成して下のように記述します。

#!/bin/sh

rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

ファイルには実行権限を与えておきます。

chmod 755 docker_app_command.sh

次はdocker-compose.ymlにcommandを追加して先程追加したファイルをセットします。
commandはコンテナ起動時に呼ばれる処理になります。

services:
  app:
    container_name: my_container_app
    ports:
      - 3000:3000
    build: .
    command:
      /sbin/init && ./docker_app_command.sh

ローカルでの更新を即時反映する

次はホスト側で編集したコードを即時反映されるようにします。
今は変更の度にImageの作り直しが必要なので時間がかかってしまいます。

コードの反映はdocker-syncというツールを使います。
ホスト側をマウントする形も考えたのですがパフォーマンスが良くないようなのでdocker-syncで進めていきたいと思います。

まずは下コマンドでdocker syncをインストールします。

gem install docker-sync

次にdocker-sync.ymlというファイルを作って下のように記述します。

version: "2"

syncs:
  my_sync_volume:
    src: "."
    sync_excludes:
      - "log"
      - "tmp"
      - ".git"

docker-compose.ymlのservices → app → volumesとvolumesにdocker-sync.ymlで定義したVolumeを追加します。

services:
  app:
    volumes:
      - "my_sync_volume:/my_app:nocopy"

volumes:
  my_volume:
  my_sync_volume:
    external: true

Docker Syncは下コマンドで動かします。

docker-sync start

docker-sync利用時ですが.docker-syncというフォルダが作られるので下のように.gitignoreに追加すると良いかと思います。

.docker-sync

同時にアプリケーションサーバーのDockerfileも修正します。
今までプロジェクト全体をCOPY . /my_appでコピーしていましたが下のように最小限にします。
そうする事で更新が最小限になってbundle install部分もレイヤーキャッシュを使ってくれるようになります。

COPY Gemfile /my_app/Gemfile
COPY Gemfile.lock /my_app/Gemfile.lock
RUN bundle install

コンテナ起動時にもbundle installをしてほしいのでdocker_app_command.shにbundle installを追加します。

#!/bin/sh

bundle install
rake db:create && rake db:migrate
rake db:seed
bin/rails s -b 0.0.0.0

これで無事にRailsを立ち上げられるようになりました。

その他利用したコマンド

キャッシュの全削除。

docker builder prune

キャッシュ利用状況の確認

docker system df

CentOS8へのインストール

Dockerをyumでインストールしたらうまくいかなかったので下コマンドでインストールと起動をしました。

dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf -y install --nobest docker-ce docker-ce-cli
systemctl enable docker
systemctl start docker

Docker Composeのインストールは下コマンドで実施しました。

wget https://github.com/docker/compose/releases/download/1.25.5/docker-compose-Linux-x86_64
mv docker-compose-Linux-x86_64 /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

参考URL

Docker入門(第一回)~Dockerとは何か、何が良いのか~ | さくらのナレッジ

rbenvの使い方と仕組みについて - Qiita

docker images を全削除する - Qiita

【Docker】コンテナの停止と削除を同時に行う - (O+P)ut

Ruby on Railsプロジェクトの開発環境をDocker化する - Qiita

CentOS8 Docker/Docker Composeインストール - Qiita

PHPickerViewControllerで画像を取得する

PHPickerViewControllerというiOS14から登場したクラスで画像を取得してみました。
このクラスはUIImagePickerControllerと違って複数画像を選択することなども可能です。

まずは画面に画像選択用のボタンを設置します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("画像選択", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action:#selector(self.tapButton), for: .touchUpInside)
        button.sizeToFit()
        button.center = view.center
        view.addSubview(button)
    }
    
    @objc func tapButton() {
    }
}

画面の真ん中にボタンを設置できました。

f:id:llcc:20210514115016p:plain

次はボタンを押すとPHPickerViewControllerが立ち上がるようにします。
画像選択可能数などの設定はPHPickerConfigurationを使って行っています。

import PhotosUI

class ViewController: UIViewController {
    override func viewDidLoad() {
        // 省略
    }
    
    @objc func tapButton() {
        var config = PHPickerConfiguration()
        config.filter = .images
        config.selectionLimit = 0
        let pickerView = PHPickerViewController(configuration: config)
        present(pickerView, animated: true, completion: nil)
    }
}

無事にPickerを表示できました。

f:id:llcc:20210514115227p:plain

最後にUIImageの取得処理を実装します。
画像の取得はPHPickerViewControllerDelegateで行います。

class ViewController: UIViewController {
    @objc func tapButton() {
        let pickerView = PHPickerViewController(configuration: config)
        pickerView.delegate = self // 追加
        present(pickerView, animated: true, completion: nil)
    }
}

extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        results.forEach {
            $0.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
                print(image)
            }
        }
    }
}

ログを見ると正しく画像が選択できている事が分かります。

f:id:llcc:20210514115407p:plain

注意点ですがloadObjectは非同期の為、読み込み完了順に呼ばれます。
そのため画像選択の順番は保持されません。
もし順番を保持したい場合、保持するような仕組みを実装する必要があります。

AndroidのEditTextやTextViewのフォントを明朝体にする

android:fontFamilyをserifにするだけです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:fontFamily="serif" />

f:id:llcc:20210502171809p:plain

日本語も対応しています。

f:id:llcc:20210502171836p:plain

android:typefaceをserifにしても同じ結果になります。
typefaceはboldやitalicを指定できるプロパティーです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="こんにちは"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:typeface="serif" />

【Kotlin】Applicationを継承したクラスを試してみる

AndroidのApplicationクラスを使ってみました。
Applicationを継承したクラスは全Activityからアクセスできるクラスになります。

クラスは下のように定義します。

class MainApplication: Application() {
}

下のようにAndroidManifest.xmlのapplicationタグに追加することでActivityから呼び出せるようになります。

<application android:name=".MainApplication">
</application>

Activityからは下のように呼び出す事ができます。
もしAndroidManifest.xmlへの記載がない場合、applicationにはandroid.app.Applicationクラスのインスタンスが入ります。

val app = (application as MainApplication)

ライフサイクルメソッドとしては、インスタンス作成時に呼ばれるonCreateなどがあります。

class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

【Kotlin】NavControllerを試してみる

Empty Activityを選んでプロジェクト作成をします。

f:id:llcc:20210323205236p:plain

起動すると下のようにラベルだけの画面が表示されます。

f:id:llcc:20210323205601p:plain

次はナビゲーションを作成します。 まず新規作成でAndroid Resource Fileを選択します。

f:id:llcc:20210323205925p:plain

Resource TypeをNavigationにしてOKを押します。

f:id:llcc:20210323205857p:plain

my_navigation.xmlが作られたので、次はここに登録するFragmentを2つ作ります。
今回はFirstFragmentとSecondFragmentという2つのフラグメントを作成しました。

f:id:llcc:20210323210344p:plain

Fragmentができたのでmy_navigation.xmlに登録します。
my_navigation.xmlを開いて画面上部の+ボタンからフラグメントを2つ追加します。

f:id:llcc:20210323210437p:plain

下のようにフラグメントが2つ登録されました。

f:id:llcc:20210323210614p:plain

次はナビゲーションを表示します。
activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/my_navigation"
        app:defaultNavHost="true" />

</androidx.constraintlayout.widget.ConstraintLayout>

アプリを起動するとFirstFragmentの内容が表示されました。

f:id:llcc:20210324111557p:plain

次はSecondFragmentへの遷移を実装します。
my_navigation.xmlの右側のActionsの+からActionを作成します。

f:id:llcc:20210324111815p:plain

次はFirstFragmentに画面遷移用のボタンを作成します。
fragment_first.xmlを下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fragment_first_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />
</FrameLayout>

FirstFragment.ktのonViewCreatedにボタン押下時の画面遷移処理を追加します。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    view.findViewById<FloatingActionButton>(R.id.fragment_first_button)?.setOnClickListener {
        findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
    }
}

起動するとボタンだけの画面が表示されます。
このボタンをタップすれば次の画面に移動します。

f:id:llcc:20210324112600p:plain

先程作ったActionを選択してAnimationsの所を編集すれば遷移時のアニメーションを指定できます。

f:id:llcc:20210324122213p:plain

ActionBarの戻るボタンですが、ActivityのsetupActionBarWithNavControllerを呼び出す事で表示できます。
押されたときの動作はonSupportNavigateUpで実装します。

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

        setupActionBarWithNavController(findNavController(R.id.nav_host_fragment))
    }

    override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.nav_host_fragment).navigateUp()
    }
}

f:id:llcc:20210324123037p:plain

次は遷移のときに変数を渡してみます。
まずはそれぞれのbuild.gradleで下のようにプラグインを設定します。

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4"
apply plugin: "androidx.navigation.safeargs.kotlin"

プラグインを入れたらmy_navigation.xmlを開いて遷移先を選択、画面右側Argumentsの+から変数を追加します。

f:id:llcc:20210324123624p:plain

この状態でビルドするとFirstFragmentDirectionsというクラスが生成されます。
画面遷移時のnavigateメソッドの引数にこのクラスを渡すようにします。

findNavController().navigate(FirstFragmentDirections.actionFirstFragmentToSecondFragment("testValue"))

遷移先では自動生成されたSecondFragmentArgsというクラスを使って変数を取り出します。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let {
        val args = SecondFragmentArgs.fromBundle(it)
        Log.d("test", "${args.testValue}")
    }
}

ログを見ると無事に渡せていることが分かります。

f:id:llcc:20210324125220p:plain

もし上記クラスが自動生成されない場合、Android studioの「Sync Project with Gradle Files」を選択すると良いかもしれません。

f:id:llcc:20210324124848p:plain