ticktakclockの日記

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

Navigation SafeArgsを使って画面遷移でパラメータを渡す

こんにちは、tkyです。

前回はBottomNavigationとNavigationをあわせた画面遷移を作りましたが、パラメータを渡せないと実務では利用しづらいケースが多いと思います。

↓前回記事↓ ticktakclock.hatenablog.com

今回はSafe Argsの機能を使って次の画面にパラメータを渡すような実装を試してみたいと思います。

Home画面、Dashboard画面という単語が出てきますが、前回作ったアプリの画面をそのまま利用しているので前回記事を一読しておくと理解しやすいと思います。

今回の実装は前回のGitHubに追記しています。

github.com

Safe Argsできて何が嬉しいのか

ここで解決できることは、今までFragmentにnewInstance()でFragmentのインスタンスを作っていた関数が不要になり

companion object{
    fun newInstance(editName: String): DashboardFragment {
        val bundle = Bundle()
        bundle.putString("DETAIL_NAME", editName)
        return DashboardFragment().apply {
            arguments = bundle
        }
    }
}

このように記述できることにあると思います。

引数を含めたDestinationが型になるためActivityとFragmentを意識する必要がなくなる、contextも不要になることが大きなメリットです。

// 引数を含めたDestination
val destination =
    HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(textValue)
findNavController().navigate(destination)

githubは前回と同じリポジトリに追記しています。

gradleへ追記

Safe Argsを利用するためにはNavigationの依存追加の他に、Project.gradleに以下を追加する必要があります。

    buildscript {
        repositories {
            google()
        }
        dependencies {
            def nav_version = "2.1.0"
            classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        }
    }
    

また、app.gradleに以下のプラグインを追加します。Kotlinだけのプロジェクトの場合の例です。

apply plugin: "androidx.navigation.safeargs.kotlin"

Java/Kotin混合プロジェクトの場合 apply plugin: "androidx.navigation.safeargs" となります。

Argumentの定義

Navigation Design上で確認していきましょう。HOME画面→Dashboard画面の遷移でDashboard画面でパラメータを受け取りたい場合、

DashboardDestinationをクリックしてAttributesの中にあるArgumentsの+ボタンをクリックします。

f:id:ticktakclock:20200202152649p:plain:w200

するとどのような引数を追加するか入力するダイアログが出てきますので、NameとTypeだけ入力してあとは何もせずでAddします。

  • Name: editName
  • Type: String
  • Array: non checked
  • Nullable: non checked
  • Default Value: empty

これで完了です。xml上でも引数が追加されていることが確認できます。

<fragment
    android:id="@+id/navigation_dashboard"
    android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
    android:label="@string/title_dashboard"
    tools:layout="@layout/fragment_dashboard">
    <!-- 中略 -->
    <argument
        android:name="editName"
        app:argType="string" />
</fragment>

argTypeについてはSerializableやParcelableなどの型も対応しているので独自データクラスなども対応できそうですね。良かったです。

詳細はこのページを見るとより深く理解できそうです。

遷移元処理

home画面にテキスト入力できるようにレイアウトを変更して、画面遷移時テキストの内容を遷移先に渡してみましょう。

HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(editName: String) という遷移情報が自動で作られていることがわかります。

ここで作られたDestinationで画面遷移することになります。

val button: Button = root.findViewById(R.id.button)
button.setOnClickListener {
    val editText: EditText = root.findViewById(R.id.editText)
    val textValue = editText.text.toString()
    // 以前はリソースIDを直接指定していたが今回は引数を含めたDestinationを指定
    val destination =
        HomeFragmentDirections.actionNavigationHomeToNavigationDashboard(textValue)
    findNavController().navigate(destination)
//            findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard)
}

遷移先処理

Home画面から送った引数をDashboard画面で受け取ってみましょう。2種類方法があります(やってることは同じです)。

1つめは DashboardFragmentArgs.fromBundle()を使ってargumentsからDashboardFragmentArgsインスタンスを取得するやり方です。

arguments?.let {
    val args = DashboardFragmentArgs.fromBundle(it)
    val argsText: TextView = root.findViewById(R.id.text_args)
    argsText.text = args.editName
}

2つめは by navArgs() プロパティデリゲートを使ってDashboardFragmentArgsインスタンスを取得するやり方です。

private val args: DashboardFragmentArgs by navArgs()

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

val argsText: TextView = root.findViewById(R.id.text_args)
argsText.text = args.editName
    return root
}

1つ目はargumentsがBundle?型なので一度Nullチェックをする必要があります。nullだったときのことを考える必要があります。 この場合引数は必ず存在することが前提なのでBundleの状態不正としてIllegalStateExceptionをThrowして実装者に気づかせるのが最善かと思われます。

2つ目の方が公式ドキュメントでも解説されていたやり方となります。nullチェックする必要がなく、こちらのほうが記述量も少なくなって楽ですね。

navArgs()は何をしているのか

実は navArgs() デリゲートの中を見ると1つ目とやっていることは何も変わりませんでした。ライブラリ側にチェック処理を任せられるので実装ミスもなくなる観点からnavArgs()を使ったほうがメリットが大きそうです。

@MainThread
inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
}

この状態でHomeFragmentで findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard) で引数を与えないリソースIDのみで画面遷移しようとすると想定通り IllegalStateException が吐かれました

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.github.ticktakclock.bottomnavigationsample, PID: 9503
    java.lang.IllegalStateException: Fragment DashboardFragment{d5e17c6 #1 id=0x7f08006c} has null arguments
        at com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment$$special$$inlined$navArgs$1.invoke(FragmentNavArgsLazy.kt:42)
        at com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment$$special$$inlined$navArgs$1.invoke(Unknown Source:0)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:44)
        at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)

まとめ

  • Safe Argsでを使うと Activity.createIntent(), Fragment.newInstance() といった関数が不要になる
  • arg typsはSerializable, Parcelableも渡せるので独自クラスでも対応できる
  • navArgs() を使ってパラメータ取得すべし

Navigationでスマートな画面遷移を実現していきましょう〜!

あとは公式Doc見るのがよいです

developer.android.com

Android Jetpack Navigationで画面遷移する

こんにちは、tkyです。

下タブ(BottomNavigation)とNavigationをあわせて画面遷移とアニメーションを実現するところまで実装してみようと思います。

Navigationがわからない人が

  • AtivityとFragmentの画面遷移ができる
  • アニメーションに必要なパラメータがわかる

なんとなくNavigationわかってきたくらいを目標に記事を書いています。

作ったプロジェクトはこちら

github.com

Navigation

公式ドキュメントも合わせて確認しながら理解していこうと思います。

Androidアプリ内の「目的地」間を移動するためのフレームワーク、とのことです。目的地はActivityでもFragmentでもどちらでも問題なく、一貫した呼び方ができるのが特徴のようです。

今まで画面遷移は activity.startActivity(Intent(this, FooDetailActivity::class.java)) だったり fragmentTransaction.replace(R.id.container, BarDetailFragment.newInstance() のように呼び方がActivity/Fragmentで異なりましたが、

Navigationを使用するとActivity/Fragment遷移関係なくfindNavController().navigate(R.id.navigation_home) のように統一されたIFなるみたいです。

developer.android.com

AndroidStudioのプロジェクトテンプレート

実はプロジェクトテンプレートでBottomNavigationActivityを選択すれば手早くサンプルアプリが構築できるので使わない手はありません。早速作ってみます

f:id:ticktakclock:20200126164121p:plain:w200
プロジェクトテンプレートからBottomNavigation

デフォルトでふわっとしたフェードアニメーションが付与されているのが確認できます。

f:id:ticktakclock:20200126185324g:plain:w200
アプリ動作

更に嬉しいのがViewModelまで作っておいてくれている親切設計。ありがたいですね

class NotificationsFragment : Fragment() {

    private lateinit var notificationsViewModel: NotificationsViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        notificationsViewModel =
            ViewModelProviders.of(this).get(NotificationsViewModel::class.java)
        val root = inflater.inflate(R.layout.fragment_notifications, container, false)
        val textView: TextView = root.findViewById(R.id.text_notifications)
        notificationsViewModel.text.observe(this, Observer {
            textView.text = it
        })
        return root
    }
}
class NotificationsViewModel : ViewModel() {

    private val _text = MutableLiveData<String>().apply {
        value = "This is notifications Fragment"
    }
    val text: LiveData<String> = _text
}

activity_main.xml

まずはActivityから理解を深めます。

BottomNavigationが配置されています。メニューは@menu/bottom_nav_menuで定義しています。Home,Dashboard,Notificationsが定義されているだけです。

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

次に nav_host_fragment というFragment領域が定義されています。 androidx.navigation.fragment.NavHostFragment が実体のようです。

ドキュメントによると「NavHostはナビゲーション グラフからの宛先を表示する空のコンテナ。」ということらしいです。NavHostFragmentはNavHostインターフェースの実装クラスということですね。

一番下には app:navGraph="@navigation/mobile_navigation" の記述があります。ナビゲーショングラフと呼ばれ、ユーザが遷移可能なすべてのデスティネーション(遷移先/宛先)を指定します。

これでNavHostとナビゲーションを紐付けているわけですね。

MainActivity

        val navView: BottomNavigationView = findViewById(R.id.nav_view)

        val navController = findNavController(R.id.nav_host_fragment)
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        val appBarConfiguration = AppBarConfiguration(
            setOf(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications
            )
        )
        setupActionBarWithNavController(navController, appBarConfiguration)
        navView.setupWithNavController(navController)

findNavController() は拡張関数です、ですのでJavaから呼ぶ場合は Navigation.findNavController(Activity, @IdRes int viewId) で呼ぶ必要があります。

続いてAppBarConfigurationですが表示している画面に合わせてAppBarの表示タイトルを変更するのに使っているぽいです。

コメントによると「メニューIDとナビゲーションのIDを一致させる必要がある」というような記述があります。 確認するとたしかにxml上でIDが一致していました。ここは知らないとハマりそうなポイントです。

MenuIdとNavigationIdが一致していれば、画面と画面名をAppBarと連携する必要がないならAppBarConfigurationの記述は省いても問題なさそうです。

後はメニューとNavitgationを関連付ける処理が入っている感じです。 メニューとNavigationを紐付けているのでドロワーメニューなどでも同様のやり方で画面遷移実現できそうです。

<?xml version="1.0" encoding="utf-8"?>
<navigation 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/mobile_navigation"
    app:startDestination="@+id/navigation_home">

    <fragment
        android:id="@+id/navigation_home"   ★★★ここと★★★
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" />

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" />

    <fragment
        android:id="@+id/navigation_notifications"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.notifications.NotificationsFragment"
        android:label="@string/title_notifications"
        tools:layout="@layout/fragment_notifications" />
</navigation>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/navigation_home"   ★★★ここのIDを一致させる必要がある★★★
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>

mobile_navigation.xml

先程登場したナビゲーションのxmlですがデザインモードというのがあって、グラフィカルに画面が表示されるモードがあります。

ここでnavigation_homeをクリックしたまま→navigation_dashboardにドラッグして線を引いてみます。 するとxml側にアクションが追加されます。アクションIDから「HomeはDashboardへの遷移アクションを持っている」というような文脈が想定できます

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_navigation_home_to_navigation_dashboard"
            app:destination="@+id/navigation_dashboard" />
    </fragment>

せっかくなのでHomeFragmentにButtonを追加して遷移してみましょう。

findNavController().navigate()はActionかDestinationのリソースIDを受けて画面遷移します。

class HomeFragment : Fragment() {

    private lateinit var homeViewModel: HomeViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 〜中略〜
        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            findNavController().navigate(R.id.action_navigation_home_to_navigation_dashboard)
        }
        return root
    }

ボタンを押すとぱっと画面が切り替わることが確認できました。ここにはデフォルトのアニメーションは適用されないようです。

独自のアニメーションを実現するにはAnimationsを修正します。

f:id:ticktakclock:20200126190753p:plain:w200

Home->Dashboardへの画面遷移の場合の各アニメーションの役割です

  • Enter
  • Exit
    • Dashboardへ遷移する時のHome側のアニメーション
  • Pop Enter
    • Dashboardから離れる時のHome側のアニメーション
  • Pop Exit

それぞれ

nav_side_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="100%p"
        android:toXDelta="0" />
</set>

nav_side_exit.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="0"
        android:toXDelta="-100%p" />
</set>

nav_side_pop_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="-100%p"
        android:toXDelta="0" />
</set>

nav_side_pop_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromXDelta="0"
        android:toXDelta="100%p" />
</set>

またナビゲーションは先程定義したアニメーションのIDを指定していきます。

    <fragment
        android:id="@+id/navigation_home"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.home.HomeFragment"
        android:label="@string/title_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_navigation_home_to_navigation_dashboard"
            app:destination="@+id/navigation_dashboard"
            app:enterAnim="@anim/nav_side_enter"
            app:exitAnim="@anim/nav_side_exit"
            app:popEnterAnim="@anim/nav_side_pop_enter"
            app:popExitAnim="@anim/nav_side_pop_exit"/>
    </fragment>

こんな感じに動きました

f:id:ticktakclock:20200126211447g:plain:w200

Activityに遷移する

DashboardFragmentにボタンを追加してSubActivityに遷移するように作ってみます。

f:id:ticktakclock:20200126222800p:plain:w300

先ほどと同様にmobile_navigation.xmlにはタグで定義してみます。同じようにdashboard->sub_activityに遷移するようなActionを定義します。

    <fragment
        android:id="@+id/navigation_dashboard"
        android:name="com.github.ticktakclock.bottomnavigationsample.ui.dashboard.DashboardFragment"
        android:label="@string/title_dashboard"
        tools:layout="@layout/fragment_dashboard" >
        <action
            android:id="@+id/action_navigation_dashboard_to_navigation_sub"
            app:destination="@id/navigation_sub"
            app:enterAnim="@anim/nav_default_enter_anim"
            app:exitAnim="@anim/nav_default_exit_anim"
            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
    </fragment>

    <activity
        android:id="@+id/navigation_sub"
        android:name="com.github.ticktakclock.bottomnavigationsample.SubActivity"
        android:label="@string/title_sub"
        tools:layout="@layout/activity_sub" />

FragmentでもActivityでも Action として画面遷移を定義しているのでActivitiyは遷移先がActivity/Fragmentどちらなのか意識することなく画面遷移できていることがわかります。

class DashboardFragment : Fragment() {

    private lateinit var dashboardViewModel: DashboardViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // 〜中略〜
        val button: Button = root.findViewById(R.id.button)
        button.setOnClickListener {
            findNavController().navigate(R.id.action_navigation_dashboard_to_navigation_sub)
        }
        return root
    }
}

まとめ

  • Navigationを使うと画面遷移を定義できる
  • 画面遷移はActivity/Fragment意識することなく同じインターフェースで実現できる
  • 遷移はActionを使い、遷移時のアニメーションも定義できる

次はSafeArgsを試してみようと思います。

→Next article ticktakclock.hatenablog.com

CoordinatorLayoutでMotionLayoutアニメーションを実現する(Android)

こんにちは、tkyです。

前回はMotionLayoutを使って実践で使えそうなアニメーションを実装しました。

今回も実践で使えそうなアニメーションを実装してみようと思います。

こちら

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

画面スクロール時にタイトルのテキストがアニメーションするようなMotionLayoutです。

前回同様のGitHubリポジトリとなります。 Sample10 をご確認ください。

github.com

レイアウト

CoordinatorLayoutなのでここは特に変わらずです。AppBarのレイアウト(layout/motion_10_header)が今回MotionLayoutに対応させることになります。

body側はただのTextViewなので特に触れません。GitHub参照ということで。。。

layout/motion_10.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="false">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:theme="@style/AppTheme.AppBarOverlay">

        <include layout="@layout/motion_10_header" />

    </com.google.android.material.appbar.AppBarLayout>

    <include layout="@layout/motion_10_body" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

AppBar

これがMotionLayoutのレイアウトになりますが、ポイントは Sample10MotionLayout というMotionLayoutを継承したカスタムMotionLayoutクラスを使っているというところです。

Sample10MotionLayout については後述します。

あとは表示したいUIを配置していきます。今回の場合、

  • 背景に使うImageView(写真は適当に準備しています)
  • タイトルTextView
  • サブタイトルTextView
<?xml version="1.0" encoding="utf-8"?>
<com.github.ticktakclock.motionlayoutsample.Sample10MotionLayout
    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"
    android:minHeight="50dp"
    app:layoutDescription="@xml/scene_10"
    app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed">


    <ImageView
        android:id="@+id/background"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        app:srcCompat="@drawable/public_domain_photo_2"
        tools:layout_editor_absoluteX="0dp"
        tools:layout_editor_absoluteY="0dp" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/sample_10_title"
        android:textColor="@android:color/white"
        android:textSize="24sp"
        tools:layout_editor_absoluteX="173dp"
        tools:layout_editor_absoluteY="263dp" />

    <TextView
        android:id="@+id/title_sub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/sample_10_sub_title"
        android:textColor="@android:color/white"
        tools:layout_editor_absoluteX="184dp"
        tools:layout_editor_absoluteY="348dp" />
</com.github.ticktakclock.motionlayoutsample.Sample10MotionLayout>

トリガー

やりたいことは「スクロールしたときにTextViewの位置を変更する」です。

スクロールは”縦のスワイプ”になるので motion:dragDirection="dragUp" でスクロールを実装します。ターゲットは背景画像にしておきます。

    <Transition
        motion:constraintSetEnd="@id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="200"
        motion:motionInterpolator="linear">
        <OnSwipe
            motion:dragDirection="dragUp"
            motion:targetId="@id/background"
            motion:touchAnchorSide="bottom" />
    </Transition>

開始

@startにはAppBarが開いている状態の成約を指定していきます。

背景画像は見せておきたいので android:alpha="1" で表示状態にしておきます。

あとは通常通りタイトルTextViewは中央に来るように、サブタイトルTextViewはタイトルの下に来るように成約を設定します。

f:id:ticktakclock:20191231150318p:plain:w200
開始レイアウト

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@id/background"
            android:alpha="1">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent" />
        </Constraint>
        <Constraint android:id="@id/title">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                motion:layout_constraintBottom_toBottomOf="@id/background"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="@id/background" />
        </Constraint>
        <Constraint android:id="@id/title_sub">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toBottomOf="@id/title" />
        </Constraint>
    </ConstraintSet>

終了

@endにはAppBarが閉じている状態の成約を指定していきます。

背景画像は見せなくて良いので android:alpha="0" で表示状態にしておきます。

スクロールした時にタイトルTextViewは左下、サブタイトルTextViewはタイトルの右横に配置します。

f:id:ticktakclock:20191231151114p:plain:w200
終了レイアウト

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/background"
            android:alpha="0">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                motion:layout_constraintBottom_toBottomOf="parent" />
        </Constraint>
        <Constraint android:id="@id/title">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginBottom="8dp"
                motion:layout_constraintBottom_toBottomOf="@id/background"
                motion:layout_constraintStart_toStartOf="parent" />
        </Constraint>
        <Constraint android:id="@id/title_sub">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                motion:layout_constraintBottom_toBottomOf="@id/title"
                motion:layout_constraintStart_toEndOf="@id/title"
                motion:layout_constraintTop_toTopOf="@id/title" />
        </Constraint>
    </ConstraintSet>

カスタムMotionLayoutについて

ここまでやれば普通であればスクロールでアニメーションできるのですが、残念ながらMotionLayoutクラスではアニメーションできませんでした。

f:id:ticktakclock:20191231165627g:plain:w200
MotionLayoutのみデモ

アニメーションする際、MotionLayoutの progress 値が変化することでTransition発生することになりますが、AppBarのレイアウトの縦移動だけではMotionLayoutのこの値が変化することはないようです。

ということでMotionLayoutに AppBarLayout.OnOffsetChangedListener を実装してAppbarLayout で発生するoffsetをMotionLayoutのprogressに適用させることでTransitionの発生を促すようにチャレンジします。

class Sample10MotionLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {

    override fun onOffsetChanged(appBarLayout: AppBarLayout?, offset: Int) {
        val scrolled = appBarLayout?.totalScrollRange?.toFloat()
        progress = -offset / (appBarLayout?.totalScrollRange?.toFloat() ?: 0f)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        (parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
    }
}

これでスクロールでMotionLayoutのTransitionを効かせる事ができたのですが、もっと良い方法がないか模索したいところです。

まとめ

CoordinatorLayoutとMotionLayoutを組み合わせたアニメーションを実装してみました。

MotionLayoutを拡張してAppBarのoffsetをMotionLayoutのprogressに適用させることでアニメーションを実現しました。

どうしてもカスタムMotionLayoutが必要なのかはわかりませんが、もっと良い方法があれば是非教えていただきたいです。

発展

カスタムMotionLayoutのprogressをうまく使えば様々なケースでMotionLayoutを使えそうなことがわかったのでDrawerLayoutでも適用可能かと思われます。

詳しくはGitHubSample11 をご確認ください。

f:id:ticktakclock:20191231172336g:plain:w200
Sample11

class Sample11MotionLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), DrawerLayout.DrawerListener {

    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        progress = slideOffset
    }

    override fun onDrawerClosed(drawerView: View) {

    }

    override fun onDrawerOpened(drawerView: View) {

    }

    override fun onDrawerStateChanged(newState: Int) {

    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        (parent as? DrawerLayout)?.addDrawerListener(this)
    }
}

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