ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

MotionLayoutでFAB Speed dialを実現する

こんにちはtkyです。

前回 、MotionLayoutでアニメーションさせる(Android) - ticktakclockの日記にてMotionLayoutについて学習しました。

今回はこのMotionLayoutを利用してFABメニューにおけるスピードダイアル(Speed dial)を実現してみたいと思います。

これですね。

f:id:ticktakclock:20191231003938g:plain:w200
デモ

実際のコードは前回のGitHubと同じです。 Sample9 をご確認ください。

github.com

レイアウト

まずはlayout.xmlにパーツを定義していきます。ツリー的にはこのようにになります。

f:id:ticktakclock:20191231000743p:plain
layoutツリー

1つViewを設置(@+id/viewのこと)していますが、これは背景をつけるために配置しています。Gif上で赤色の背景にあたる部分です。

layout/motion_09.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    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"
    tools:showPaths="true"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_09">

    <View
        android:id="@+id/view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_menu_add"
        app:fabCustomSize="48dp"
        tools:layout_editor_absoluteX="339dp"
        tools:layout_editor_absoluteY="659dp" />

    <TextView
        android:id="@+id/sub_text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="camera"
        tools:layout_editor_absoluteX="200dp"
        tools:layout_editor_absoluteY="553dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/sub_btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#3F51B5"
        android:clickable="true"
        android:src="@android:drawable/ic_menu_camera"
        app:fabCustomSize="32dp"
        tools:layout_editor_absoluteX="332dp"
        tools:layout_editor_absoluteY="560dp" />

    <TextView
        android:id="@+id/sub_text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="call phone"
        tools:layout_editor_absoluteX="297dp"
        tools:layout_editor_absoluteY="714dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/sub_btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#03A9F4"
        android:clickable="true"
        android:src="@android:drawable/ic_menu_call"
        app:fabCustomSize="32dp"
        tools:layout_editor_absoluteX="363dp"
        tools:layout_editor_absoluteY="723dp" />

    <TextView
        android:id="@+id/sub_text3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="search"
        tools:layout_editor_absoluteX="297dp"
        tools:layout_editor_absoluteY="698dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/sub_btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#FF9800"
        android:clickable="true"
        android:src="@android:drawable/ic_menu_search"
        app:fabCustomSize="32dp"
        tools:layout_editor_absoluteX="368dp"
        tools:layout_editor_absoluteY="691dp" />

    <TextView
        android:id="@+id/sub_text4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="share social"
        tools:layout_editor_absoluteX="297dp"
        tools:layout_editor_absoluteY="698dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/sub_btn4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#4CAF50"
        android:clickable="true"
        android:src="@android:drawable/ic_menu_share"
        app:fabCustomSize="32dp"
        tools:layout_editor_absoluteX="368dp"
        tools:layout_editor_absoluteY="691dp" />

    <TextView
        android:id="@+id/sub_text5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="setting"
        tools:layout_editor_absoluteX="297dp"
        tools:layout_editor_absoluteY="698dp" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/sub_btn5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#9C27B0"
        android:clickable="true"
        android:src="@android:drawable/ic_menu_manage"
        app:fabCustomSize="32dp"
        tools:layout_editor_absoluteX="368dp"
        tools:layout_editor_absoluteY="691dp" />

</androidx.constraintlayout.motion.widget.MotionLayout>

量は多いですが、1つ1つはただのButtonとTextViewです。パーツを定義したので xml/scene_09.xml に開始/終了の成約を設定してどのようにアニメーションするかを定義していきましょう。

トリガー、開始成約、終了成約の順にポイントを解説していきます。

今回すべて1つのxml内に記述していますが、開始と終了成約については別xmlで定義したほうが可読性が上がるので本番投入する際は分けると良いのではないかと思われます。

トリガー

やりたいことは「FABボタンが押された時にメニューを開く/閉じる」です。 Transitionにはfabがクリックされた時に成約をトグルできるようにOnClickを配置します。

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="200">
        <OnClick
            motion:targetId="@id/fab"
            motion:clickAction="toggle"
            />
    </Transition>

開始

@startにはメニューが閉じている状態の成約を指定していきます。

  • 背景の@viewは特にすることはないので透過させておきます
  • メニューの各ボタンも表示する必要はないので android:visibility="invisible" を指定して非表示状態とします
  • アニメーション自体は上下のみで、メニューのFAB(@sub_btn1~@sub_btn5)の下の成約を motion:layout_constraintBottom_toBottomOf="parent" で親につけておきます
    <ConstraintSet
        android:id="@+id/start">
        <Constraint android:id="@id/view">
            <Layout
                android:layout_width="0dp"
                android:layout_height="0dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                />
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="#00FFFFFF"
                />
        </Constraint>
        <Constraint android:id="@id/fab">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
            <CustomAttribute
                motion:attributeName="Rotation"
                motion:customFloatValue="0"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn1"
            android:visibility="invisible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/fab"
                motion:layout_constraintEnd_toEndOf="@id/fab"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text1"
            android:visibility="invisible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn1"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn1"
                motion:layout_constraintTop_toTopOf="@id/sub_btn1"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn2"
            android:visibility="invisible">
            <Layout
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn1"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn1"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text2"
            android:visibility="invisible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn2"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn2"
                motion:layout_constraintTop_toTopOf="@id/sub_btn2"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn3"
            android:visibility="invisible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn2"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn2"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text3"
            android:visibility="invisible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn3"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn3"
                motion:layout_constraintTop_toTopOf="@id/sub_btn3"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn4"
            android:visibility="invisible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn3"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn3"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text4"
            android:visibility="invisible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn4"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn4"
                motion:layout_constraintTop_toTopOf="@id/sub_btn4"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn5"
            android:visibility="invisible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn4"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn4"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text5"
            android:visibility="invisible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn5"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn5"
                motion:layout_constraintTop_toTopOf="@id/sub_btn5"
                />
        </Constraint>
    </ConstraintSet>

終了

@endにはメニューが開いている状態の成約を指定していきます。

  • 背景はViewのBackgroundColor属性を変更するためにCustomAttributeで色を指定します
  • メニューのボタンたちは今度は表示する必要があるので android:visibility="visible" を指定して表示状態とします
  • メニューの各FABは例えば@sub_btn5(一番上のFAB)では motion:layout_constraintBottom_toTopOf="@id/sub_btn4" というように1つ下のFABの上に配置するように指定します

f:id:ticktakclock:20191231001309p:plain:w200
サンプル

    <ConstraintSet
        android:id="@+id/end">
        <Constraint android:id="@id/view">
            <Layout
                android:layout_width="0dp"
                android:layout_height="0dp"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                />
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="#5CEC9D9D"
                />
        </Constraint>
        <Constraint android:id="@id/fab">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintBottom_toBottomOf="parent"
                />
            <CustomAttribute
                motion:attributeName="Rotation"
                motion:customFloatValue="45"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn1"
            android:visibility="visible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/fab"
                motion:layout_constraintEnd_toEndOf="@id/fab"
                motion:layout_constraintBottom_toTopOf="@id/fab"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text1"
            android:visibility="visible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn1"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn1"
                motion:layout_constraintTop_toTopOf="@id/sub_btn1"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn2"
            android:visibility="visible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn1"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn1"
                motion:layout_constraintBottom_toTopOf="@id/sub_btn1"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text2"
            android:visibility="visible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn2"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn2"
                motion:layout_constraintTop_toTopOf="@id/sub_btn2"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn3"
            android:visibility="visible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn2"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn2"
                motion:layout_constraintBottom_toTopOf="@id/sub_btn2"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text3"
            android:visibility="visible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn3"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn3"
                motion:layout_constraintTop_toTopOf="@id/sub_btn3"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn4"
            android:visibility="visible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn3"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn3"
                motion:layout_constraintBottom_toTopOf="@id/sub_btn3"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text4"
            android:visibility="visible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn4"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn4"
                motion:layout_constraintTop_toTopOf="@id/sub_btn4"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_btn5"
            android:visibility="visible">
            <Layout
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintStart_toStartOf="@id/sub_btn4"
                motion:layout_constraintEnd_toEndOf="@id/sub_btn4"
                motion:layout_constraintBottom_toTopOf="@id/sub_btn4"
                />
        </Constraint>
        <Constraint
            android:id="@id/sub_text5"
            android:visibility="visible">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toStartOf="@id/sub_btn5"
                motion:layout_constraintBottom_toBottomOf="@id/sub_btn5"
                motion:layout_constraintTop_toTopOf="@id/sub_btn5"
                />
        </Constraint>
    </ConstraintSet>

全体

長いのでここを見てください。

https://github.com/ticktakclock/MotionLayoutSamaple/blob/master/app/src/main/res/xml/scene_09.xml

まとめ

MotionLayoutを使ってxmlのみでアニメーションを実現しました。

MotionLayoutのポイントはボタンクリック時のアニメーションも含めてxmlで定義して実現できるのが良いかな、と思います。

このようにすることでボタンクリック時に本当にやりたい処理のみktファイルに記述することができて全体的に見通しの良いコードになりそうです。

次はCoordinatorLayoutと組み合わせてMotionLayoutアニメーションを実装してみようと思います。

次回

ticktakclock.hatenablog.com

参考:ありがとうございます!

Android MotionLayout概論 - Eureka Engineering - Medium

GitHub Actionsのワークフロー個人的逆引き

こんにちはtkyです。

GitHub Actionsを使い始めて1ヶ月位が経過したのでやったことなどを忘れないようにまとめていこうと思います。

思いつき次第更新してこうと思いますが、大体ドキュメントに書いてあるので基本ドキュメント参考にするのが良いのかと思われます。

とりあえずパッと試してみたい人へ

GitHubのactionタブからポチポチするだけでとりあえずなにかは作れるので雰囲気を理解したい場合一度ブラウザ上で動かしてみると良いです。

f:id:ticktakclock:20191207134645p:plain
ブラウザからActionsを試す

逆引き辞典

できる限りコピペで使えるようにしています。

作業ディレクトを指定したい

cd foo && ( sample.sh args1) みたいなことやりたくないですよね。

.
├── .github
│   └── workflows
│       └── pr-check.yml
└── foo
    └── sample.sh
    steps:
      - name: exec sample.sh
        working-directory: ./foo
        run: sample.sh args1

ワークフローを実行するコンテナを指定したい

通常ubuntu-latest上で動きますが、コンテナを指定したいときがある・・・?はず・・・?

jobs:
  samplejob:
    name: sample job
    runs-on: ubuntu-latest
    container:
      image: node:10.16.0-alpine # Nodeコンテナ上でsteps実行する
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      - run: npm install

PRのIssueにコメントする

github apiを使います。tokenは secrets.GITHUB_TOKEN に格納されているので便利です。

if: failure() で直前のstepが失敗していたとき(=exit status 0以外)に実行という条件分岐が可能になります。 APIたたいたあとに&& exit 1 で失敗に落として次のsuccess()に進まないようにしますが、もっといいやり方があれば教えて下さい。。。

テストにコケたときにIssueにコメントする

    steps:
      - name: Checkout
        uses: actions/checkout@v1
      - name: uint test
         run: .exec_test.sh  # 各プロジェクトのテストを実行してください
      - name: post result if test failure
        if: failure()
        run: |
          curl \
            -X POST \
            ${{ github.event.pull_request.comments_url }} \
            -H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
            -H 'content-type: application/json' \
            -d "{ \"body\": \"👮<200d>♂️UnitTestでエラーが発生しています🚧内容を確認してください👮<200d>♂️\" }" \
          && exit 1
      - name: post result if test success
        if: success()
        run: |
          curl \
            -X POST \
            ${{ github.event.pull_request.comments_url }} \
            -H 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
            -H 'content-type: application/json' \
            -d "{ \"body\": \"おめでとうございます🎉UnitTestは無事通過しました🥳\" }" \

こんな感じで出てきます

f:id:ticktakclock:20191207162046p:plain
テスト通過時

どんな環境変数が使えるのか

ワークフロー上の${{ secrets.github_token }} (小文字でもいけます)などどんなものが使えるのか

help.github.com

ざっくり3つくらいリストアップ。

変数 説明
${{ secrets.github_token }} トーク
${{ secrets.github_sha }} コミットハッシュ
${{ secrets. github_repository }} オーナーとリポジトリ(ower/repo)

${{ github.event.payload }} にも色々と情報が入っています。GitHubAPIのEvent typeと同じものが入るようです。

PRの時に入るイベントは以下を見ると良いです。

PRにコメントするときは ${{ github.event.pull_request.comments_url }} にPOSTするとコメントできます。

developer.github.com

カスタムアクションが必要になるケース

自分のリポジトリだけで使うプライベートなアクションの作成が必要になるケースは、自分で使ってみて以下のような感じでした。

  • 特殊なDocker環境を使っている
    • phppeclで拡張を入れていてワークフロー上の環境では対応しきれなかった
  • shellをぶん回しながら色々と細かいことをやりたい
    • yamlのRUNでかけなくもないけどごちゃるので・・・

DBも絡めたテストがしたい

大体のケースで当てはまるであろうケース。以下のように書くと localhost:portでアクセスできるようになる

  samplejob:
    name: sample job
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:5.7 # dockerhubのイメージを指定する
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testdb
          MYSQL_USER: docker
          MYSQL_PASSWORD: docker
          TZ: Asia/Tokyo
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      # you can access to localhost:3306

以下のように実行コンテナを指定するとdocker-composeと同じような状況下で動作させることができます。

この時コンテナ間アクセスでは mysql:3066 のようにサービス名でアクセスできるようになります。

  samplejob:
    name: sample job
    runs-on: ubuntu-latest
    container:
      image: php:7.2-fpm # 実行コンテナを指定する
    services:
      mysql:
        image: mysql:5.7 # dockerhubのイメージを指定する
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testdb
          MYSQL_USER: docker
          MYSQL_PASSWORD: docker
    steps:
      - name: Checkout
        uses: actions/checkout@v1
      # you can access to mysql:3306

その他わかったこと

躓いたところなど記載していきます。

github actionはpublicのみ対応だった

どういうことかというと カスタムアクションで ticktakclock/actions-test@v1 のようなリポジトリをPrivateで作ると、他のリポジトリでこのアクションにアクセスできません(当たり前ですよね)

会社でGithubを使っていて、会社用のアクションを共通で利用できるようにリポジトリを作ってもPrivateなのでアクセスできない事象になるので困る・・・

同一Organization内のリポジトリはアクセスできるようにしてほしい・・・と思いながらも今はリポジトリごとに個別でコピペして入れてます。

ワークフローはgithubにデプロイしてみるしか確認できない

どうしようもない気がしますがなにか手がないか・・・ローカルで確認した後にgit push したいのですが。。。

僕はテスト用にリポジトリ作ってそっちにPushしまくって確認した後 コミットをきれいにしてから開発リポジトリに導入しました。

※Slack等にイベント通知しているので最初は開発リポジトリにpushしまくってタイムラインが一瞬荒れました・・・w

MotionLayoutでアニメーションさせる(Android)

こんにちはtkyです。

今日はかねてから気になっていたAndroidでにおけるconstraintlayout2.0のβ3版で追加されているMotionLayoutを使ってアニメーションを試してみたいと思います。

まだベータ版(2019/12/06執筆現在)であり、2.0が正式になった時にどうなるかは不明ですので予めご了承ください。

(2020/10/24 追記)

constraintlayout2.0のstableが出ているのでプロジェクト内のライブラリバージョンを更新しました。

一応すべてのサンプルはちゃんと動いているようでした。

(2020/10/24 追記)

今回試したリポジトリはこちらになります。

github.com

MotionLayout is 何

まずは公式ドキュメントを見に行きましょう。

developer.android.com

冒頭部分を日訳でピックアップすると

  • MotionLayoutはConstraintLayoutのサブクラスでライブラリバージョン2.0から追加されています。
  • MotionScenes 内に定義したconstraint(以降、制約)間の遷移をサポートします。
  • API level 18 (JellyBean MR2)以降で利用可能です。
  • MotionLayoutはMotionSceneファイルが必要で、 MotionScene内トップレベルに LayoutDescription を含みます
    • StateSet (Optional) ・・・システムの状態(selected, focused, enabled, 等)
    • ConstraintSet ・・・制約群
    • Transition ・・・制約か状態間の遷移

※StateSetについてはよくわからず、詳しく知っている方がいれば教えていただきたいです。 android.util.StateSet のことであっているんでしょうか・・・

また2019/12/06現在はベータ3となっており、以下のようにapp.gradleに追記することで利用可能となります。

dependencies {
    // 中略
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3'
    // 中略
}

どうやって学習するか

公式にサンプルがあるのでこれで使い方を学習するのが一番手っ取り早いです。 私のリポジトリもこのサンプルを写経しながら色々と試していきました。

github.com

ざっくり解説

以下のような簡単なアニメーションの実現を考えます。

f:id:ticktakclock:20191206142322g:plain:w200
サンプル

MotionLayoutを使ったアニメーションはやることを列挙したらきりこんな感じかと思いますが、2〜4は 1つのxml内に定義することになります。

  1. アニメーションさせたいレイアウトを MotionLayout タグで囲う
  2. 遷移の仕方(Transition)を決める
  3. 遷移前の制約(ConstraintSet)を決める
  4. 遷移後の制約(ConstraintSet)を決める

アニメーションさせたいレイアウトを MotionLayout タグで囲う

アニメーションさせたい場合、

これを

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/button"
        android:background="@color/colorAccent"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

こうする。

tools:layout_editor_absoluteXとtools:layout_editor_absoluteYは記述しても無視されます。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout
    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:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/scene_01"
    tools:showPaths="true"
    >

    <View
        android:id="@+id/button"
        android:layout_width="128dp"
        android:layout_height="128dp"
        android:background="@color/colorAccent"
        android:text="Button"
        tools:layout_editor_absoluteX="267dp"
        tools:layout_editor_absoluteY="333dp" />

</androidx.constraintlayout.motion.widget.MotionLayout>

前述の通りMotionLayoutはConstraintLayoutのサブクラスなのでConstraintLayoutを囲う必要はありません。

android:id="@+id/button"

また、今まで app:layout_constraintXXXX_toYYYYOf="parent" のようにlayout側で定義していた制約は不要となります。

app:layoutDescription="@xml/scene_01"

layout側で定義するidはMotionSenceに定義するトリガーや制約で使用する為layout側に定義する必要があります。

このscene_01.xmlに遷移の仕方と制約を定義していきます。先に示しておきます。

xml/scene_01.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

    <ConstraintSet
        android:id="@+id/start">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginStart="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet
        android:id="@+id/end">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                />
        </Constraint>
    </ConstraintSet>
</MotionScene>

遷移の仕方を決める

上記のscene_01.xmlでやっていることは、

  • 特定のIDに対して
  • スワイプ/クリックした時の
  • 開始ConstraintSetと終了 ConstrainntSetを定義した
  • <Transition> タグをMotionSence内に設置する

です。遷移の仕方の部分は以下のところになります。

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

遷移前/遷移後の制約(ConstraintSet)を決める

以下の部分ですね。書きっぷりはおおよそConstraintLayoutと一緒です。

これと

    <ConstraintSet
        android:id="@+id/start">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginStart="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                />
        </Constraint>
    </ConstraintSet>

f:id:ticktakclock:20191206142721p:plain:w200
start

これです

    <ConstraintSet
        android:id="@+id/end">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                />
        </Constraint>
    </ConstraintSet>

f:id:ticktakclock:20191206142735p:plain:w200
end

scene.xmlが長くなる問題

上記のようにConstraintSetを書いていると必然的にsence.xmlが長くなって見通しが悪くなりますが、xmlを分けて次のようにも記載することができます。

制約部分はいつもどおり、layout/xmlに記載してしまって motion:constraintSetStart="@layout/motion_01_start" のように参照するだけにすると、役割も明確になって良いかもしれません。

layout/scene_01_start.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/button"
        android:background="@color/colorAccent"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

layout/scene_01_end.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/button"
        android:background="@color/colorAccent"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

xml/scene_01.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetStart="@layout/motion_01_start"
        motion:constraintSetEnd="@layout/motion_01_end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

</MotionScene>

CustomAttributeについて

CostomAttributeは例えば backgroundColorratation といった viewの セッターを呼んだりできます。 ドキュメントに書いてありますが motion:attributeName="BackgroundColor" とした場合 setBackgroundColor を呼ぶことになるので、パスカルケースで記述するところがポイントですね。

また、customAttributeValueは int/float/boolean/stringが指定できて、色は"@color/colorAccent"のようにリソースIDで定義する感じです。

以下のように複数指定することで『左から右に赤から緑に変化しながら90度回転する』みたいなアニメーションが実現できることになります。

    <ConstraintSet
        android:id="@+id/start">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginStart="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                />
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorAccent"
                />
            <CustomAttribute
                motion:attributeName="Rotation"
                motion:customFloatValue="0"
                />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet
        android:id="@+id/end">
        <Constraint android:id="@id/button">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintTop_toTopOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                />
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorPrimary"
                />
            <CustomAttribute
                motion:attributeName="Rotation"
                motion:customFloatValue="90"
                />
        </Constraint>
    </ConstraintSet>

f:id:ticktakclock:20191206145227g:plain:w200
customattribute-sample

KeyFrameSetについて

今まで指定してきたアニメーションは開始地点と終了地点のみ定義してきました。

A ----------------------> B

KeyFrameSetを使用すると、このAとBの間に中継地点Pを設けをその間(A->P, P->B)の移動方法、を定義できるようになります。

A ---------->P-----------> B

コードで説明するとタグ内にKeyFrameSetタグを追加してKeyPositionタグを更に定義します。

KeyPosition以外にはKeyAttribute、KeyCycle、KeyTimeCycle、KeyTriggerを指定することができますが、 まだKeyPositionしか理解していないのでKeyPositionで説明したいと思います。

この場合、『アニメーションのちょうど真ん中に来た時にY軸に25%移動(マイナスがついているので上に移動します)』となります。

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
        <KeyFrameSet>
            <KeyPosition
                motion:keyPositionType="pathRelative"
                motion:percentY="-0.25"
                motion:framePosition="50"
                motion:motionTarget="@id/button"/>
        </KeyFrameSet>
    </Transition>

先程のCustomAttributeと組み合わせるとこのようになります。

f:id:ticktakclock:20191206145407g:plain:w200
keyframeset-sample

デバッグについて

xmlでMotionLayoutのattributeに tools:showPaths="true" を追加すると AndroidStudioのデザインビューで --- でアニメーションが描く線が表示されます。

f:id:ticktakclock:20191205174420p:plain
showpath=true

しかし実機やエミュレータでは線は描かれません。

コード上で motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH) とすると出てくるようになります。

val motionLayout = findViewById<MotionLayout>(R.id.motionLayout)
motionLayout.setDebugMode(MotionLayout.DEBUG_SHOW_PATH)

まとめ

アニメーション自体は昔から存在する仕組みですが、MotionLayoutを使うことでコードを書かないで、xmlで挙動を定義できるようになりました。

これによりViewのレイアウトと業務ロジックを分離して管理できるので見通しが良くなりそうです。

次はもう少し実践向きなレイアウトとアニメーションを試してみたいと思います。

次回Part1

ticktakclock.hatenablog.com

次回Part2

ticktakclock.hatenablog.com

参考記事

理解の助けになりました。ありがとうございます。

Android MotionLayout概論. この記事は eureka Advent Calendar 2018… | by Kurimura Takahisa | Eureka Engineering | Medium

Developers.IO 2019に行ってきました

こんにちは、tkyです。

本日はClassmethodさんのDevelopers.IO 2019に参加してきたレポートブログとなります。 とは言え本家ブログ側に各レポートブログがあるのでこちらではサマリと感想などを綴ることにします。

Developers.IOについてはこちら dev.classmethod.jp

各セッションのレポートはこちら dev.classmethod.jp

ハッシュタグ#cmdevio のようでした。 twitter.com

ブルボン総選挙もやってました。

f:id:ticktakclock:20191105143511j:plain:w200
会場内テーブル

濃密なスケジュールの中見てきたセッションはこちらになります。

時間 Room タイトル 発表者
10:30~11:15 Room2 認証の標準的な方法は分かった。では認可はどう管理するんだい? 都元ダイスケ 氏
10:30~11:15 Room H claspではじめるサーバーレス開発 Google Apps Scriptで簡単自動化 武田隆志 氏
12:24~13:30 昼食休憩してました
13:45~14:30 Room3 サーバーレスの基本とCI/CD構築 & 運用 〜システムは動いてからが本番だ〜 藤井元貴 氏
14:45~15:30 Room3 エンジニアも知っておくと得する、デザイン組織とデザイナーマネジメント ベイジ 枌谷力 氏
15:45~15:30 Room3 サービスを爆速で立ち上げるためのSaaSの活用 諏訪悠紀 氏
16:45~15:30 Room5 障害に備えたアーキテクチャを考える「あのとき何が起こった!?」 坂巻一義 氏 / 吉井亮 氏
17:45~15:30 Room5 Amazon ECSを活用したAWS運用自動化サービスの裏側を包み隠さず解説 伊藤祥 氏
18:45~ Room11 懇親会

認証の標準的な方法は分かった。では認可はどう管理するんだい?

公式レポート

dev.classmethod.jp

感想

通常APIを設計するときにほぼ必ず考慮するであろうアクセス制御、 例えば「一般ユーザ」「管理者ユーザ」で取得できるデータの内容が異なったり、 更新できるデータの範囲が異なったりしますが、実際そのような要件に対して設計アプローチしていくのか、という話となります。

ISO 10181-3で Access Control Framework という名称で標準化されている模様で、この考え方に基づきながら解説されていて非常にすんなりと理解できた気がします。

理解しやすくするために以下のワードが出てきました。

  • 操作する人・・・ユーザ
  • 操作されるもの・・・・リソース
  • 何かの操作・・・アクション

そして上記の『「一般ユーザ」「管理者ユーザ」で取得できるデータの内容が異なったり』これがまさにアンチパターンとなりうる状況だったようです。 前提としてユースケースが異なるのでそもそも同じアクションで管理しては ~ダメ~ (複雑になるので推奨しない)そうです。 別々のAPIで設計することで、余計な条件分岐もなくなりテストしやすくなりそうです。 確かに、不具合が発生したときにその状況を再現しづらくなったり、原因特定に時間がかかるう要因になりかねないですし、なるほどと思いました。私の中ではここが一番収穫でした。

P34に書いてありますが、重要です。 - ユースケースとアクションをちゃんと定義して - 誰が何にアクセスできるのかきっちり決める

アクションがわかったところで、何を以てそのAPIを叩いてOK/NGなのか、 ですが、いろいろあるんですよね。

  • ユーザが持つ権限(どのAPIと叩いて良いのか)
  • オーナー権限
  • 役職、リーダー

アクセス権は ユーザ 自身に紐づくものと その役職(Role) に紐づくものがあって、これまでの話はユーザ側のものでした。 ここでユーザには関係ない役職という軸も出てきます。もうそろそろ脳のキャパシティを超えてきました。

で、このあたりをじっくり観察するとAWSのIAMによるアクセス制御がまさにこのことだそうで、きれいに締めくくられ 後味の良いスッキリとした気持ちでセッションが終了しました。

claspではじめるサーバーレス開発 Google Apps Scriptで簡単自動化

公式レポート

dev.classmethod.jp

感想

自社でGASおじさんと化しているtkyですが、clasp自体知らなかったため、知識を得るために聴講。 GASができることと、デメリットを解消するためのclaspについて、 クラスメソッド内のGASの活用事例などが聴けました。 ※claspについては既知の人だと「聞いたことある内容だ」という感じの初級者向けセッションとなりそうです。

GAS、Google Chrome上で開発できす素早さとスプレッドシートとの連携が強力で便利なのですが、 - JS 1.6である - コードの差分管理ができない - コードフォーマッタなど活用できない

と言ったデメリットが挙げられる中でclaspというCLIツール(Nodeモジュール)の存在を知って心のなかで大きく盛り上がりました。 TSでかけるのが大きいかもしれませんね。ローカル開発となるのでgitも使えるので楽だと思いました。

メリットしか無いのでは?とも思いますが、セッションでもありました通り - 手軽さが減る - 非開発者には敷居が上がる

などもありますし、エクセルの標準関数でできることも多いので、目的に合わせてGASやclaspを利用していくのが良いなと改めて感じました。

活用事例としては以下のようなケースでGASで定常業務にかかる負担を減らしているようです。なるほど!! 1. スプレッドシートJSONにしてS3にアップロード 2. GAとMackerel連携。リアルタイムユーザを定期的に送信して可視化する 3. Backlogチケットをスプレッドシート連携(定例時に転機するのに活用) 4. Gsuiteのグループ作成(メーリングリスト)Admin SDK Directory APIを叩いてグループ一括作成 5. Gsuiteのグループ棚卸し、グループ情報をスプレッドシートに出力

クラスメソッド内でのGASの活用事例もしれてとても良いセッションでした。

サーバーレスの基本とCI/CD構築 & 運用 〜システムは動いてからが本番だ〜

公式レポート

dev.classmethod.jp

感想

入場ギリギリになってしまい立ち見となりました・・・ CI/CDで考慮すべき項目やAWS構成について理解できるセッションとなっていました。

CI/CDにおいて重要なのはセッションにもありましたが以下ですね。 - いつでも動く安心感を手に入れる - デグレに気づける - テストコードでは見つけられない不具合の回避 - オペレーションミスによるデプロイミスの回避

人的ミスをそもそもなくす、仮に発生しても水際回避したり、早期発見できるようになるのがCI/CDの強みだと思います。 テストコードでは見つけられない不具合 についてはピンと来なかったのですが、 AWSサービスを使うにあたり以下のような事象がそれに該当するようですね。なるほど。

  • CloudFunctionのスペルミス
  • IAMの権限不備
  • DBの権限不足

デプロイ時に気をつけるべきポイントとしてのIAMアクセスキーの流出についても興味深かったです。 実際にやってみないとこういった知見にたどり着けないと思うのですごいなぁと思います。(AssumeRoleという機能を考えているAWSもすごい・・・)

最後に監視の話、何を、どのくらい検知したら、いつ、誰にどんな内容を通知するのか、 これをきっちり考えないと、精神的に摩耗してしまいますし、オオカミ少年になってしまいますね。

サービスもリリースして終わりではなく、リリースしてもやることは多くいかに素早く改善していくか、 やはりリリースしてからがやはり本番だ、というタイトル通りのセッションだったと思います。

エンジニアも知っておくと得する、デザイン組織とデザイナーマネジメント

クラスメソッドさんと一緒にお仕事をしている会社さんのセッションでした。 baigie.me

公式レポート

レポートブログはないので資料のみです。

https://baigie.me/download/pdf/devio_191101.pdf

感想

本セッションではデザイナーという職種の細かいジャンルとデザイナーのタイプの紹介がメインとなります。

セッション時、「デザイナー職の人と一緒に仕事をしている/したことがあるか」という質問が上がり 結構な数の手が上がっていました。やはりデザイナーと関わっているエンジニアが日々増えているのだろうと、感じています。 そんな中、エンジニア+デザイナーを含む組織をマネジメントする必要も出てくるのは、そのとおりですね・・・

デザイナータイプについてはエンジニアにも通ずるというか全ジャンルに通ずるところがあると思いました。 そしてお互いが相容れない存在で「混ぜるな危険」の意味も理解できましたw

  • 挑戦的⇔保守的
  • 感覚的⇔論理的

上記の軸からみる分布 - 理想実現型(先駆者) - 成果追求型(データドリブン) - 共同作業型(調和) - 実務遂行型(ドキュメント命)

MBTI診断を大きく4分割すると上記のタイプに分かれそうだな、という印象を受けました。

www.16personalities.com

皆さんはどんなタイプでしたか?

サービスを爆速で立ち上げるためのSaaSの活用

公式レポート

dev.classmethod.jp

感想

Auth0、stripe、 CircleCI を使ってサービスを立ち上げた話となります。

それぞれ、認証、決済、CI/CDを司り、これらを使って約2ヶ月でDevelopers.IO Dafeができたとのことで驚きのスピード感(と技術力!!)

stripeについてはテスト用のクレカを利用できたり、いざという時の返金も行えるようになっているということで、まじで便利じゃん(語彙力)と思いました。

本セッションでも伝えていたことですが『プロダクトにとって最も大切なもの(コア機能)はなにか』が重要だという印象を受けました。 実際認証の仕組みやDBなど自前で用意しようとすると車輪の再発明にもなりますし、運用コストがかかってきます。 本来のサービス、プロダクトのあり方を考えて適材適所でSaaSを利用していく判断も必要になってきますね。

最後に、エンジニアさんは実際にDevelopers.IO Cafe内で開発しているらしく、超羨ましい・・・と思った次第です。

障害に備えたアーキテクチャを考える「あのとき何が起こった!?」

公式レポート

dev.classmethod.jp

感想

2019年8月23日 東京リージョン障害を通じて、どのような対策を取ると良いのか、 実際に稼働率を極限まで高めようとした際の構成及びコスト試算などを共有するセッションで、とても興味深かったです。

障害発生時の実際の調査内容についてもセッション内で共有がありました、こちらも合わせて見るとより流れが把握できそうです。

dev.classmethod.jp

障害発生時、どのようなシステム構成を取ると良いのかについては Well-Archicted Framework 信頼性 が参考になるそうです。 システム設計運用の大局的な考え方や、設計原則のベストプラクティス、システムの最適化度合いを評価する視点をもてるようになるための資料で、 ちょうど11/1当日?くらいに日本語翻訳版が出たようです。 ドキュメントがいたれりつくせりですごいっすね、AWS・・・ 正直このあたりから自分の知識量を超えていて理解が追いつきませんでした。

ホワイトペーパー | AWS

で、その上で稼働率を極限まで高めるために、99.0%、99.9%、99.99%のようにしていくとどういった構成になるのかをまとめていました。 ここからわかることは、落ちないような構成を取るためには相応のコストが掛かるということ、 また構成だけでは限界があるので、落ちたあとにいかに早く復旧できるかが稼働率を高めるポイントにもなるというところが印象深かったです。

Amazon ECSを活用したAWS運用自動化サービスの裏側を包み隠さず解説

公式レポート

dev.classmethod.jp

感想

AWS運用自動化サービス「opswitch」のシステム構成や開発環境などの紹介セッションとなります。 こんなところまで見せてしまって良いんですか、、、というくらい濃い内容でした。 運用の自動化に対する課題の解説と、運用を自動化するサービスの説明に分かれています。

昼一のセッションにもありましたが、リリースして終了ではなく、リリースしてからが本番であり、運用は決して逃れることのできない必要作業になりますが、 やはり自動化して極力ヒューマンエラーを避けていきたいところ。

そこでopswitchという運用自動化サービスが登場します。 EC2/RDSインスタンスの起動・停止、バックアップなどのタスクを組み合わせてジョブ実行できる機能などが魅力的です。

内部のアーキテクチャについては、大きく3システムで構成されていて各システムで扱っている技術が異なっているところが面白いなと思います。 また、コア部分のシステムについてはそれぞれ必要なリソース(CPU,メモリ)や役割などが異なるため関心事別にクラスターを分けて構成しているとのことで勉強になります。

かなりディープな話だけあって知識的に置いてけぼりをくらいながらも知らない領域を知る機会を得られてとても良かったです。

懇親会

最後は再演セッションがいくつかあるなか、並行で懇親会がスタートしました。 食事が美味しい・・・ 登壇されていた方をうまく見つけることができずでしたが、数名の方とコミュニケーションを取ることができました。

f:id:ticktakclock:20191105143339j:plain:w200
懇親会の食事

おわりに

最近AWS等インフラの構築/設計に関わる機会が増えており、自分の知らなかった情報を手に入れることのできた 貴重な体験でした。

Koinを使って依存解決(DI)する

こんにちは、tkyです。

今回は

今までDI(Dependency Injection)ライブラリはDagger2だけしか使ったことがなかったのですが、他のDIライブラリも使ってみたくて Koin を使ってみました。

公式見ながら作業しましたが、想像以上に簡単にDIできたのでびっくり。。。

以下のような2つの数字を足し算するだけのアプリでお試し実装してみました。

f:id:ticktakclock:20190923173251g:plain:w200
サンプル画面

作成したものはGitHubに置いてありますのでご確認ください。

アプリ自体はMVVMを採用し、DataBindingを活用していますが、Koinに特化して記載するため、Bindingについては触れません。 github.com

環境情報

Koin

Kotlin用の軽量な依存注入フレームワーク(日本語訳)です。2019/09/23時点の最新は v2.0.1 となっています。

insert-koin.io

使い方

Koin自体は超らくちんで、App にKoin使う宣言して、モジュールの宣言をするだけです。

app.gradle

    implementation 'org.koin:koin-android:2.0.1'
    implementation 'org.koin:koin-android-scope:2.0.1'
    implementation 'org.koin:koin-android-viewmodel:2.0.1'

App.kt

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // ↓これだけ!DSLでかけるのがいいですね!
        startKoin {
            androidContext(this@App)
            modules(myModule)
        }
    }
}

modules() に依存を解決したいモジュールの宣言をしていきます。

今回は KoinInjector.kt ファイルを作って、その中に変数を定義して見ました。 CalculatorCalculateServiceMainViewModel については後述します。

import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module

// ちゃんと定数とわかるような命名規則のほうが良いです。
val myModule = module {
    single { Calculator() }
    single { CalculateService(get()) }
    viewModel { MainViewModel(get()) } // ViewModel用のモジュール宣言
}

後出しになりますが、サンプルアプリはクラス間で以下のような依存関係にあります。

f:id:ticktakclock:20190923193025p:plain:w100
依存関係図

Calculator クラスは他のクラスに依存しない計算ロジックのクラスです。

myModule には single { Calculator() } のようにして依存解決します。

class Calculator {
    fun sum(a: Int, b: Int): Int = a + b
}

CalcurateService クラスは計算に関する処理をまとめるためのサービスクラスのような扱いで一枚かませました。DIしたかったし😅

実際、 CalcurateService はRepository(DataSource)層、 Calculator は Dao層のような関係で理解すると良いかもしれません。 Calculator に依存していますが、Calculatorインスタンスはコンストラクタから注入します。

myModule には single { CalculateService(get()) } のようにして依存解決します。

class CalculateService(private val calculator: Calculator) {
    fun sum(a: Int, b: Int): Int = calculator.sum(a, b)
}

MainViewModelCalculateService に依存しています。 これもコンストラクタからインスタンスを注入します。

myModule には viewModel { MainViewModel(get()) } のようにして依存解決します。

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel(private val calculateService: CalculateService) : ViewModel() {

    private var _result: MutableLiveData<String> = MutableLiveData()
    val result: LiveData<String>
        get() = _result

    fun calculate(a: String?, b: String?) {
        val numA = if (a.isNullOrEmpty()) 0 else a.toInt()
        val numB = if (b.isNullOrEmpty()) 0 else b.toInt()
        val result = calculateService.sum(numA, numB).toString()
        _result.value = result
    }
}

MainActivityMainViewModel に依存しています。 MainActivityAndroidクラスにつき、コンストラクタから注入できません。 その代わりKoinが遅延初期化の仕組みを用意してくれています。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
// import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject

class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 関数内でもこのように依存注入できます。
        // val viewModel: MainViewModel = get()
    }
}

これで依存解決されたMainViewModelのインスタンスが出来上がりです!

まとめ

コードを貼り付けただけでしたが、やることは

  1. App.ktに以下を書く
startKoin {
    androidContext(this@App)
    modules(myModule)
}
  1. モジュール(myModule)の依存を解決する
val myModule = module {
    single { Calculator() }
    single { CalculateService(get()) }
    viewModel { MainViewModel(get()) }
}
  1. ActivityでDIする
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        ・・・略・・・
    }
}

たった3つ!って思うとやってみたくなりません?

公式にもGet Startedがあるので見てみてください。

WebRTCとWebSocketで画面共有(ビデオチャット)アプリを作る

こんにちはtkyです。

表題の通り、WebRTCとWebSocketの勉強を兼ねて、webアプリを作ってみました。

作り方を記載しているわけではなく、成果物に対しての説明となるブログですので予めご了承願います。

作ったもの

画面共有(ビデオチャット)アプリを作成しました。 音声に対応させていないので画面共有としています。理由は後述。

  • 画面を複数人で共有し
  • 参加者のアクションを画面上で共有できる(いいね機能と呼称しています)

※ローカル環境+Chromeでしか動作(確認)しませんのであしからず!

コードレビューや、プレゼン発表などでみんなのリアクションが励みになるような思いを込めて作ってみました!

作ったものについてはgithubにアップしています。

github.com

技術要素

技術及び環境情報です。

環境

技術

  • React (v16.9.0)
  • Material UI (v4.4.0)
  • Node (v10.14.1)
  • WebSocket (socket.io v2.2.0)
  • WebRTC

構成

サーバ側とクライアント側(Reactアプリのこと)に分かれます。 全体は以下のような構成になります。

WebRTCでP2P通信する情報をやりとりするためにnode.jsで情報交換用のサーバ(シグナリングサーバというようです)を構築します。

f:id:ticktakclock:20190909001549p:plain
構成図

シーケンス

WebRTCで通信するためにシグナリングサーバとどのような情報を交換する必要があるのかシーケンスにしてみました。 流れとしては - 通信を開始したい側がofferを出す - offerを受け取った側がAnswerを出す - この時点でWebRTCが経路情報を算出する(Ice Candidate) - 経路情報が取得できたら自分の経路情報を出す(クライアント毎に自身の経路情報を交換する) - 経路が確立できたらP2P通信が始まる

f:id:ticktakclock:20190909001613p:plain
シーケンス

サーバ側

上記のシーケンスを守れるようにサーバ側は以下を実装します。 サーバの実装は /server/app.js です。サーバ側は以上です。

  • socketを部屋に紐付ける
  • メッセージを部屋に紐づくsoket全員に配信する
// offerとanswerは SDP(SessionDescriptionProtocol)というプロトコルとして処理します
socket.on('SEND_SDP', function(data) {
  data.sdp.id = socket.id;
  if (data.target) {
    socket.to(data.target).emit('RECEIVE_SDP', data.sdp);
  } else {
    socket.broadcast.to(socket.roomname).emit('RECEIVE_SDP', data.sdp);
  }
});

// Ice Candidateを配信する
socket.on('SEND_CANDIDATE', function(data) {
  if (data.target) {
    data.ice.id = socket.id;
    socket.to(data.target).emit('RECEIVE_CANDIDATE', data.ice);
  } else {
    console.log('candidate need target id');
  }
});

上記のようなコードをやりとりしたいメッセージごとに以下の事に注意しながら定義するだけです。

  • 部屋関係なく自分も含め全員 (今回使わない)
  • 部屋関係なく自分以外全員 (今回使わない)
  • 部屋に属する自分も含め全員 (いいねリアクション(機能説明にて後述)のやり取り)
  • 部屋に属する自分以外全員(オファー要求)
  • 特定の人(オファー自体)

クライアント側

クライアント側は結構手間で、Offerの送受信、Answerの送受信、Ice Candidateの送受信があります。 加えて、P2PができたときのStream(動画情報です)の取り扱いなどやることが結構多くて、 実際に作った MultVideoChat.js が結構肥大化してしまいました。もう少しうまく分離できれば良かったのですが・・・

reactプロジェクト自体は create-react-app で作成しました。 あとは自分用のprettierでフォーマッティングしています。

使い方

サーバ起動とクライアント起動は別ターミナル上で行ってください。

$npm i
$node server/app.js  // サーバ起動
$npm start                 // クライアント起動

画面構成

Material-UI: A popular React UI framework を採用しました。自分自身がAndroiderであるのと極力自分でレイアウトするコストを払わないようにするためです。 Material UIを使うこと自体もある程度慣れる必要がありますが、ここは頑張りポイントです。

大きく2つの画面で構成されています。App.jsにて、部屋情報のstateにより表示を切り替えています。

エントランス画面

部屋名を入力して入室する画面です。赤枠の領域が src/Entrance.js の実装範囲になります。

f:id:ticktakclock:20190908201625p:plain
エントランス画面

ビデオチャット画面

src/MultiVideoChat.js に大多数の実装が入っています。 src/Room.js でラップしています。

f:id:ticktakclock:20190908202439p:plain
チャット画面

メニュー

src/CustomDrawer.js で自分の映像情報に対する操作UIを設置しています。 FABでも良かったのですが、操作の関心事がそれぞれ異なり、FABではうまく表現しきれないと判断してドロワーメニューにしました。

f:id:ticktakclock:20190908232035p:plain
メニュー画面

機能

  • チャットルーム機能

WebSocket接続後、サーバに対して SEND_ENTER のsocket通信を行います。 socket.join(roomname) で指定した名前の部屋に入ることを実現しています。 その部屋に新しく入ったsocket idに対してofferを出すための呼び出しメッセージを通知します。 RECEIVE_CALL を受け取ったsocketは指定socket idにOffer要求をしていきます。

// クライアント側
this.socket.on('RECEIVE_CONNECTED', data => {
  // socket情報をstateに持っておく
  this.setState({ socketId: data.id });
  // 部屋に入る
  this.socket.emit('SEND_ENTER', this.state.room);
});
// サーバ側
socket.on('SEND_ENTER', function(roomname) {
  socket.join(roomname);
  console.log('id=' + socket.id + ' enter room:' + roomname);
  socket.roomname = roomname;
  socket.broadcast
    .to(socket.roomname)
    .emit('RECEIVE_CALL', { id: socket.id });
});
  • 複数人会話機能

上記の部屋を作ることで複数人が同じ部屋に属することができます。 また、先に記載したシーケンスを実装することで複数人のP2P通信を実現します。

  • 全画面表示機能

メニューでFull Screenを押下した時、その接続streamを全画面で表示します。

f:id:ticktakclock:20190908234809p:plain
全画面
Material UIのDialogを使います。fullscreen プロパティをつかいました。 自分のstream情報を全画面用のvideoタグに設定する

<Dialog
  maxWidth="xl"
  fullScreen
  open={this.state.fullScreenId !== ''}
  onClose={this.onFullScreen}
>
  <Like socket={this.socket}>
    <video
      style={fullScreenStyle}
      autoPlay="1"
      playsInline
      ref={video => {
        const fullScreen =
          this.videos[this.state.fullScreenId] || this.video;
        if (video && fullScreen) {
          video.srcObject = fullScreen.srcObject;
        }
      }}
    />
  </Like>
  <IconButton
    style={closeButtonStyle}
    color="primary"
    onClick={this.onFullScreen}
  >
    <CloseIcon />
  </IconButton>
</Dialog>
  • いいね機能

全画面表示中、画面をクリックすると、「良いね!!」を表すハートマークを表示する、盛り上がり機能をつけました。 実際これがやりたいがためにビデオチャットアプリを作ったと言っても過言ではありません。 盛り上がりが共有できて、みんなが幸せになれば良いなーと思います。

f:id:ticktakclock:20190910170901g:plain

シーケンスの実装

クライアント側では先に示したシーケンスを実装することになります。 送信・受信はWebSocketの話になります。

  • Offerの送信
  • Offerの受信〜Answerの送信
  • Answerの受信
  • Ice Candidateイベント〜Ice Candidate送信
  • Ice Candidateの受信
  • streamイベント

ポイントのみ、抽出して記載します。

Offerの送信

async makeOffer(id) {
  // 接続先ごとにpeer(=RTCPeerConnection)を確立していきます。
  const peer = this.prepareNewConnection(id);
  const stream = this.video.srcObject;
  stream.getTracks().forEach(track => {
    // peerに送信したいstream情報を設定します。
    peer.addTrack(track, stream)
  });
  const offer = await peer.createOffer();
  await peer.setLocalDescription(offer);
  // websocket offer送信
  this.sendSdp(id, peer.localDescription);
}

Offerの受信〜Answerの送信

async onOffer(sdp) {
  // 相手側のsocket idでRTCPeerConnectionを確立する
  const peer = this.prepareNewConnection(sdp.id);
  const stream = this.video.srcObject;
  stream.getTracks().forEach(track => {
    peer.addTrack(track, stream);
  });
  // 受け取ったofferデータをRTCPeerConnectionクラスにセットします。
  const offer = new RTCSessionDescription(sdp);
  await peer.setRemoteDescription(offer);
  // 受け取ったOfferに対しAnswerを返答します。
  this.makeAnswer(sdp.id);
}

async makeAnswer(id) {
  // 接続先のsocket id ごとにpeerをstateで管理しています。
  const peer = this.state.peers[id];
  const answer = await peer.createAnswer();
  await peer.setLocalDescription(answer);
  this.sendSdp(id, peer.localDescription);
}

Answerの送信

async onAnswer(sdp) {
  // 接続先のsocket id ごとにpeerをstateで管理しています。
  const peer = this.state.peers[sdp.id];
  if (!peer) return;
  // 受け取ったofferデータを相手socketのRTCPeerConnectionクラスにセットします。
  const answer = new RTCSessionDescription(sdp);
  await peer.setRemoteDescription(answer);
}

Ice Candidateイベント〜Ice Candidateの送信

prepareNewConnection(id) {
  const config = { iceServers: [] };
  const peer = new RTCPeerConnection(config);
  // 中略
  peer.onicecandidate = event => {
    this.onIceCandidate(id, event.candidate);
  };
  // 中略
  return peer;
}

onIceCandidate(id, icecandidate) {
  if (icecandidate) {
    // Trickle ICE
    this.sendIceCandidate(id, icecandidate);
  } else {
    // Vanilla ICE
    console.log('empty ice event');
  }
}

Ice Candidateには2種類あります。どのタイミングでIceを送るかの違いです。

  • Vanilla ICE
    • すべてのイベント受信後、Ice Candidateを送信する
  • Trickle ICE
    • イベントを受け取り次第逐次Ice Candidateを送信する
    • 逐次接続情報を送ることでVanillaよりも早く接続確立できる事がある

Ice Candidateの受信

onReceiveCandidate(ice) {
  const peer = this.state.peers[ice.id];
  if (!peer) return;

  // 受け取ったiceを相手socketのRTCPeerConnectionクラスにセットします。
  const candidate = new RTCIceCandidate(ice);
  peer.addIceCandidate(candidate);
}

streamイベント

接続が確立できると、streamが降ってきます。streamイベントが来ていたらP2Pの接続自体は成功しています。

prepareNewConnection(id) {
  const config = { iceServers: [] };
  const peer = new RTCPeerConnection(config);
  // バージョンによりpeer.onAddStream()らしいです。最新のchromeではpeer.ontrak()を使います。
  peer.ontrack = event => {
    this.onAddStream(id, event.streams[0]);
  };
  // 中略
  return peer;
}

本アプリではwebカメラと画面共有の2パターンの共有方法を持っていますが、リソースを切り替えるたびに通信を確立するようにしています。 streamを切り替えようとすると、peerの接続が切れてしまい、うまく映像の共有ができなかったためなのですが、やり方が悪いだけかもしれません。

音声ミュートについて

前提として、音声が流れている時、自身のブラウザにも音声が流れてしまいます。 上記が理由で音声配信は可能でしたが諦めた背景があります。 動画ミュートも音声ミュートも以下のようにしてミュートできますが、 自分のブラウザから出る自分の声をミュートしようとすると相手側も結局ミュートされてしまい、意味がなくなってしまいます・・・

      const videoTrack = this.video.srcObject.getVideoTracks()[0];  // audioの場合getAudioTracks()
      videoTrack.enabled = !videoTrack.enabled;

考えうる対策としては以下が検討できましたが、どんどん仕様が膨らんでいくので今回は一旦ここまでとしました。

  • 配信Streamと表示Streamを分ける
    • 配信Stream(映像・音声)、表示Stream(映像)にすれば切り替えられそう
  • 自分のStreamは表示しない
    • 元も子もないですね・・・

終わりに

  • 初めてReact Hooks関連の実装をしてみて、useState()の使い方が少し理解できた
  • WebRTCの一通りの実装することでビデオチャットの基礎が理解できた
  • 今回はlocalhost上でwebsocketのシグナリングサーバを実装したが、firebaseを使っても良いかもしれないと感じた
  • websocket.ioを使ったが比較的容易に実装できた(気がする)のでまた挑戦してみたい

以上です。

Fuchsiaをビルドしてエミュレータ起動する

こんにちは、tkyです。

Fuchsiaのビルドをしてみたいと思います。

以下のサイトを見ながらやったこととなります。 結構時間かかります。3時間くらい・・・?

fuchsia.dev

実施端末情報

macOS Mojave (10.14.5)
MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
プロセッサ 2.7 GHz Intel Core i7
メモリ16 GB 2133 MHz LPDDR3

Fuchsiaをビルドするソースコード郡を落としてきます。

$ curl -s "https://fuchsia.googlesource.com/fuchsia/+/master/scripts/bootstrap?format=TEXT" | base64 --decode | bash

20分くらいかかりました・・・ このあと pathを追加するように促されるのでされるがままに追加します。

{username} はログイン名が入ります。

export PATH=/Users/{username}/fuchsia/.jiri_root/bin:$PATH

この状態で jiri, fx, cipd コマンドが使えるようになります。

Jiriは開発ツールのようです。Fuchsiaのコードをチェックアウトしたりビルドしたりするためのコマンドがいろいろと用意されている模様です。

ビルドについては以下の通り。

$ xcode-select --install
$ fx set core.x64 --with //bundles:kitchen_sink
$ fx build

2時間強かかりました・・・

ビルドできたのでいざ起動!!!

$ fx run

・・・省略・・・
[00008.390] 01281:01283> devcoordinator: fallback driver 'usb_composite' is avai
[00008.390] 01281:01283> devcoordinator: driver 'usb_composite' added
[00008.390] 01281:01283> devcoordinator: fallback driver 'intel_disp' is availab
[00008.390] 01281:01283> devcoordinator: driver 'intel_disp' added
[00008.391] 02907:02930> devhost[proxy] bind driver '/system/driver/virtual_audi
[00008.408] 02907:02930> devhost[proxy] bind driver '/system/driver/virtual_came
[00009.599] 06866:06887> [00009.595941][1186734803][0][netstack] INFO: netstack.
[00009.623] 06866:06887> [00009.621660][1186734803][0][netstack] WARNING: main.g
[00013.611] 05656:05671> [ERROR:garnet/bin/sysmgr/app.cc(183)] Could not load pa
[00014.980] 05656:05671> [ERROR:garnet/bin/sysmgr/app.cc(190)] Singleton fuchsia
[00017.406] 06866:06887> [00017.405761][1186734803][0][netstack] WARNING: netsta
[00018.418] 11088:11101> wlanstack [I]: Starting

動いたっぽい!!?やった! 🎉ここからどうしたら良いんだ・・・・

ls は反応しませんでしたが、 pwd は反応した。Linuxコマンドは少しだけ準備があるみたいです。

ls
/boot/bin/sh: 1: Cannot create child process: -1 (ZX_ERR_INTERNAL): failed to re
$ ls
/boot/bin/sh: 2: Cannot create child process: -1 (ZX_ERR_INTERNAL): failed to re
$ pwd
/
$ 

ドキュメント読み進めるとわかりますが、グラフィックを有効にして起動するオプションもありました。

QEMUエミュレータのことです。それ以上のことはよく知らない状況です。

-m sets QEMU's memory size in MB. ・・・QEMUのメモリ容量を設定する
-g enables graphics (see below).   ・・・GUIを有効にする
-N enables networking (see below).  ・・・ネットワーク接続を有効にする
-k enables KVM acceleration on Linux. ・・・KVMを有効にする
-h help                                                         ・・・ヘルプ

改めて、この設定で起動してみます。一度シャットダウンしてもう一度起動します。

$ dm shutdown    ・・・シャットダウンする時はdmコマンドを使います。
[01923.365] 04981:05009> minfs: Unmounted
[01923.377] 10107:10120> thread 'main' panicked at 'log_listener: not able to wr
[01923.377] 10107:10120> note: Run with `RUST_BACKTRACE=1` environment variable 
[01923.445] 04398:04413> Allocation Info:
[01923.446] 04398:04413>   Allocated 0 blobs (0 MB) in 0 ms
[01923.446] 04398:04413> Writeback Info:
[01923.446] 04398:04413>   (Client) Wrote 0 MB of data and 0 MB of merkle trees
[01923.446] 04398:04413>   (Client) Enqueued writeback in 0 ms, made merkle tree
[01923.446] 04398:04413>   (Writeback Thread) Wrote 0 MB of data in 0 ms
[01923.446] 04398:04413> Lookup Info:
[01923.446] 04398:04413>   Opened 1257 blobs (738 MB)
[01923.446] 04398:04413>   Verified 339 blobs (153 MB data, 2 MB merkle)
[01923.446] 04398:04413>   Spent 1012 ms reading 5 MB from disk, 7140 ms verifyi
[01923.464] 04398:04413> blobfs: Unmounted
[01923.469] 01281:01283> devcoordinator: Successfully waited for VFS exit comple
$fx run -g

CUIが出てきました。クリックするとマウスが持っていかれるので ctrl + alt + gでマウスを奪い返します。

画面上部に表示されている <0] debug [1] [2] [3] という表記はタブを表していて、 opttion(⌥) + tab でタブ切り替えが可能となります。

f:id:ticktakclock:20190715171354p:plain:w500
fuchsia $run -g

また、-Nの起動オプションも試してみます。

$ fx run -N
WARNING: Please opt in or out of fx metrics collection.
You will receive this warning until an option is selected.
To check what data we collect, run `fx metrics`
To opt in or out, run `fx metrics <enable|disable>

Creating disk image...done
To use qemu with networking on macOS, install the tun/tap driver:

tun/tapドライバを入れろというのでぐぐって入れます。このコマンドで行ける模様。

$ brew cask install tuntap
・・・略・・・
installer: Package name is TunTap Installer package
installer: Installing at base path /
installer: The install failed (エラーによってインストールできませんでした。ソフトウェアの製造元に問い合わせてください。)
Follow the instructions here:
  https://github.com/Homebrew/homebrew-cask#reporting-bugs

っておいぃぃぃ!!!入らないんかーい! ということで-Nオプションはちょっと断念・・・解決できた人いたら教えてください・・・ 😇

#