20200624のAndroidに関する記事は6件です。

Flutterの始め方

初めに

AndroidのアプリエンジニアがFlutterで開発を始めるためのセットアップをまとめました。
AndroidのアプリエンジニアがFlutterを始めるまでなので、Android Studioは既にインストール済みであることを前提とします。
公式

環境

PC:Windows 10
Android Studio:3.5

1.Android Studio

1-1.Android Studioのアップデート

Android StudioでのFlutter開発環境の構築
FlutterのサポートがAndroid StudioのVersion:3.6以上なので、アップデートしていきます

[Events]からアップデート
現在の最新Version4.0にアップデート

1-2.Pluginの設定

上記の画面の[Configure]->[Plugins]からFlutterのPluginを追加

[MarketPlace]タブからFlutterを検索

Android Studioの再起動

2.アプリ作成

[Starta new Flutter project]を選択

[Flutter Application]を選択

適当なディレクトリを選択してFlutter SDKをinstall

[Next]を選択
main.dartが表示されれば完了

3.デバイスのセットアップ

エミュレータをセットアップ
(Androidアプリの開発をするときと同じです)
[Tools]->[AVD Manager]
image.png
セットアップ後、実行
image.png

4.実行結果

サンプルアプリの実行結果

5.Hot Reload

Flutterといえばこれ
このアイコンでHot Reload実行
image.png

参考文献

公式

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android + kotlin + databindingでカレンダーを自作する【日付設定編】

概要

前回なんとなくいい感じにレイアウトは作成できたので、今回は日付の部分を手作業で入れていた箇所を動的に入れられるようにしたいと思います。

目標

カレンダーの日付をデータバインディングを用いてアクティビティから動的に設定する

手順

Activityからレイアウトの値を変更する

まずは最小限のActivity→レイアウト変更を書きます。

MainActivity.kt
package com.example.myfavoritecontentsmanage

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.myfavoritecontentsmanage.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
        binding.cell1.text = "サンプル"
    }
}

こうすると表示画面の一番最初のテキストが「サンプル」に変わったと思います。
これでActivityの方からレイアウトの値をいじることができるようになりました。

日付を設定していく

IDを工夫したので指定するときにそこが可変的にできたらいいなと思ったのですが、そんなに世の中甘くないみたいでJS時代のようにid名を変数みたいに指定するってことができないみたいなので、配列を渡してレイアウトの方でそれを解釈してもらう方法にします...(絶対やり方ちがうんだろうな...)

まずレイアウトの方を修正します。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="date"
            type="String[]" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <!--縦分割のガイドライン-->
        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/youbi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.1" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.25" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.40" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.55" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.70" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.85" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/line_6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="1" />

        <!--横分割のガイドライン-->
        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.14" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.28" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.42" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.56" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.70" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="0.84" />

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/row7"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_percent="1" />


        <TextView
            android:id="@+id/sunday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="Sun"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:id="@+id/monday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="Mon"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@+id/sunday" />

        <TextView
            android:id="@+id/tuesday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="Tue"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@+id/sunday" />

        <TextView
            android:id="@+id/wednesday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="Wed"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@+id/sunday" />

        <TextView
            android:id="@+id/thursday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="Thu"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@+id/sunday" />

        <TextView
            android:id="@+id/friday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="Fri"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@+id/sunday" />

        <TextView
            android:id="@+id/saturday"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="Sat"
            app:layout_constraintBottom_toBottomOf="@id/youbi"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@+id/sunday" />


        <TextView
            android:id="@+id/cell_1"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[0]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/youbi" />


        <TextView
            android:id="@+id/cell_2"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[1]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_3"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[2]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_4"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[3]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_5"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[4]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_6"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[5]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_7"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[6]}"
            app:layout_constraintBottom_toBottomOf="@id/line_1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/youbi" />

        <TextView
            android:id="@+id/cell_21"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[7]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/line_1" />


        <TextView
            android:id="@+id/cell_22"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[8]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_23"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[9]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_24"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[10]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_25"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[11]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_26"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[12]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_27"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[13]}"
            app:layout_constraintBottom_toBottomOf="@id/line_2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/line_1" />

        <TextView
            android:id="@+id/cell_31"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[14]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/line_2" />


        <TextView
            android:id="@+id/cell_32"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[15]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_33"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[16]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_34"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[17]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_35"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[18]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_36"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[19]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_37"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[20]}"
            app:layout_constraintBottom_toBottomOf="@id/line_3"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/line_2" />

        <TextView
            android:id="@+id/cell_41"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[21]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_42"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[22]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_43"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[23]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_44"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[24]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_45"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[25]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_46"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[26]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_47"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[27]}"
            app:layout_constraintBottom_toBottomOf="@id/line_4"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/line_3" />

        <TextView
            android:id="@+id/cell_51"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[28]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_52"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[29]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_53"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[30]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_54"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[31]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_55"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[32]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_56"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[33]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_57"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[34]}"
            app:layout_constraintBottom_toBottomOf="@id/line_5"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/line_4" />

        <TextView
            android:id="@+id/cell_61"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_red"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[35]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_62"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[36]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row2"
            app:layout_constraintStart_toEndOf="@+id/row1"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_63"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[37]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row3"
            app:layout_constraintStart_toEndOf="@+id/row2"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_64"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[38]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row4"
            app:layout_constraintStart_toEndOf="@+id/row3"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_65"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[39]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row5"
            app:layout_constraintStart_toEndOf="@+id/row4"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_66"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[40]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="@id/row6"
            app:layout_constraintStart_toEndOf="@+id/row5"
            app:layout_constraintTop_toTopOf="@id/line_5" />

        <TextView
            android:id="@+id/cell_67"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="@drawable/flame_style_blue"
            android:gravity="center_horizontal|center_vertical"
            android:text="@{date[41]}"
            app:layout_constraintBottom_toBottomOf="@id/line_6"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/row6"
            app:layout_constraintTop_toTopOf="@id/line_5" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

一番最初に配列を宣言して、それをすべての要素に当てはめていく構成にします。
こうすることで、配列に渡した通りのものが展開されます。

アクティビティでは、日にちの配列を作成しそれを渡すようにします。

MainActivity.kt
package com.example.myfavoritecontentsmanage

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.myfavoritecontentsmanage.databinding.ActivityMainBinding
import java.util.*

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        val calendar = Calendar.getInstance()
        val year = 2020
        val month = 6
        val minDay = 1
        calendar.set(year, month - 1, 1)
        val maxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
        val dayArray: MutableList<String> = mutableListOf()

        for (i in minDay..maxDay) {
            dayArray.add(i.toString())
        }
        binding.date = dayArray.toTypedArray()
    }
}

この状態で実行すると、まあ当たり前なのですが、最初の日曜日に1日になり、そこから月の日数分日にちが振られるようになります。
スクリーンショット 2020-06-24 23.10.03.png

最初の曜日を考える

ただ、上記の状態ではだめで2020年6月の1日は月曜日です。
そうするために最初の1日の前に空白を入れるようにします。

調べたところ特定の日付の曜日は

calendar.get(Calendar.DAY_OF_WEEK)

みたいな感じに取れるらしく、しかも

曜日 数値
1
2
3
4
5
6
7

こんな形で取れるみたいで、つまりこれは数値-1した分だけブランクを開ければ良さそうだぞ?
と思いアクティビティを下記のように書き換える。

MainActivity.kt
package com.example.myfavoritecontentsmanage

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.myfavoritecontentsmanage.databinding.ActivityMainBinding
import java.util.*

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        val calendar = Calendar.getInstance()
        val year = 2020
        val month = 6
        val minDay = 1
        calendar.set(year, month - 1, 1)
        val maxDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
        val blankDays = calendar.get(Calendar.DAY_OF_WEEK)

        val dayArray: MutableList<String> = mutableListOf()
        for (i in minDay until blankDays) {
            dayArray.add("")
        }
        for (i in minDay..maxDay) {
            dayArray.add(i.toString())
        }
        binding.date = dayArray.toTypedArray()
    }
}

すると何ということでしょう...
スクリーンショット 2020-06-24 23.14.36.png

ちゃんと月曜に1日が設定されるようになりました!!!!!!

ちなみに去年の6月は2020の部分を2019にすればいいだけで、その状態で実行すると...

スクリーンショット 2020-06-24 23.18.45.png
ちゃんと土曜日から始まってる!!!!!
素晴らしい!!!!!

次回はこれを横にスワイプしたら次の月にいけるようにできたらいいな....

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActivityResultContractとViewModelを使ってFirebase Auth×Googleログインをスッキリ書く

はじめに

AACのViewModelを用いた場合、onActivityResultが絡んだコードを書くときに辛みが発生していましたが、前回の記事を基に、Firebase AuthでのGoogleログインの処理を書いてみたら思いの外スッキリしたので記録を残しておきます。
なお、本記事ではFirebase Auth×Googleログインに関する基本的な解説はしません。

全体のソースコードはこちらです:https://github.com/masaibar/SimpleGoogleLogin

実装

詳細な差分はこちらを御覧ください。

app/build.gradle

安定版である1.1.0にはActivityResultContractが含まれていないので注意して下さい。

app/build.gradle
    implementation "androidx.activity:activity-ktx:1.2.0-alpha06"
//      implementation "androidx.fragment:fragment-ktx:1.3.0-alpha06" Fragmentの場合

MainActivity.kt

ログインに関する処理を全てViewModel側に閉じ込めることが出来るようになり、Activity側にonActivityResultを書かなくて良くなったのでボタンのリスナーを呼び出すだけになりました。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

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

        sign_in_button.setOnClickListener {
            viewModel.onClickSignIn(this)
        }
    }
}

MainViewModel.kt

onClickSignInで受け取るのはFragmentActivity型にしないとactivityResultRegistryが参照できないので気をつけましょう。

MainViewModel.kt
class MainViewModel : ViewModel() {

    private val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()

    fun onClickSignIn(activity: FragmentActivity) {
        val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(activity.getString(R.string.default_web_client_id))
            .requestEmail()
            .build()
        val googleSignInClient = GoogleSignIn.getClient(
            activity,
            googleSignInOptions
        )

        activity.activityResultRegistry.register(
            "key",
            ActivityResultContracts.StartActivityForResult()
        ) { activityResult ->
            if (activityResult.data == null) {
                return@register
            }

            GoogleSignIn.getSignedInAccountFromIntent(activityResult.data)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        task.getResult(ApiException::class.java)?.let {
                            firebaseAuthWithGoogle(it)
                        }
                    }
                }
        }.launch(googleSignInClient.signInIntent)
    }

    private fun firebaseAuthWithGoogle(account: GoogleSignInAccount) {
        firebaseAuth.signInWithCredential(
            GoogleAuthProvider.getCredential(
                account.idToken,
                null
            )
        ).addOnSuccessListener {
            Log.d("Login Succeeded!!!", "${it.user?.uid}")
        }
    }
}

おわりに

ViewModel側にログインにまつわる処理を閉じ込めることが出来たおかげで、UI側をシンプルなコードに保つことが出来るようになりました。
例えばこの記事のように、MVVMとFirebase Authの実装方法は様々な議論がされてきていると思いますがActivityResultContractを利用することによって新たな可能性が生まれたと思います。

全体のソースコードはこちらです(再掲):https://github.com/masaibar/SimpleGoogleLogin

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter歴1ヶ月がオンラインハッカソンで初心者チームのTech Leadをしてみた

はじめに

1ヶ月前のGWにもハッカソンに出場し、そこで初めてFlutterを触り5日間でアプリを開発しました。

そのときの記事はこちらです
Flutter初見が5日間のハッカソンでアプリ開発してきた

今回は1週間でサービスを作るハッカソンを企画/開催したためその概要と、Flutter初心者チームでTech Lead(笑)な役割をし、メンバーを牽引した軌跡を残せれば良いと思いこの記事を書いています。

CA21Hackathon

目的

今回のハッカソンは

CyberAgentの21卒内定者エンジニアでのハッカソンです。

このハッカソンを開催した目的は2つあります。

  • 同期理解のため
  • 同期間での技術知見共有

このハッカソンは、内定者数人で週に1度zoomで集まりミーティングを行い

  • そもそもどんなイベントを開催するか
  • ハッカソンを開催する目的/ テーマ
  • 開催日時/希望職種アンケート
  • グループ決め

等を1ヶ月間綿密に決めた上で開催されました。

人事の方とかけあったり、詳細を決めてくれたみんなに感謝です?

概要

結果、テーマは

with COVID-19, after COVID-19

開催期間は1週間。

初日と最終日のみ必須参加 とし、 稼働時間はチーム相談として各々調整する形です。

初日:チームでアイデアソンを行い、発表。他の人からフィードバックをもらうフェーズ

2日目〜6日目:任意の稼働時間で各々開発を進める

最終日:開発したサービスに関して発表(発表時間7分、質疑応答3分)

な流れでした。

チーム構成

自分のチームは

サーバーサイドのいないFlutterチーム

です。

チームの構成は

  • Android (Flutter経験1ヶ月?‍♂️)
  • Android (Flutter経験なし?‍♂️)
  • iOS (Flutter経験なし?‍♂️)
  • iOS (Flutter経験なし?‍♂️)

で、自分含め、ほとんどFlutter経験がないメンバーでした。

さらに、全員Firebaseの知識もなく、サーバレスで通信を伴うアプリを作りたい場合にも少し苦労するかもという印象でした。

作成したアプリ

今回のハッカソン企画において、

企画側は

  • アンケート作成が面倒臭い
  • バランス良くチーム分をするのが大変
  • 参加者への連絡が大変

参加者側は

  • 毎回のプロフィール情報を入力するのが手間
  • メールだと大切な情報を見逃しがち
  • チームの管理が面倒
  • グループ名を決めるのが面倒くさい
  • github, slide, document等リンクの管理が面倒

という課題/面倒がありました。

その面倒ごとを解決できるようなアプリを作ろうと思って生まれたのが

Hack ×2 です

HackathonをHackするという意味での命名です、チームメンバーに命名マスターがいて即決まりました(さすが)

スクリーンショット 2020-06-24 19.34.06.png

ハッカソン期間中はミニマムで実装をしましたが、元々の想定では15画面ほどありました。

シンプルにハッカソンで作る規模ではないですねw 

また、要件的に通信が伴うため、サーバーサイドのいないこのチームではFirebase等のmBaaSを使用する必要がありました。

この、FlutterおよびFirebase初心者チームが、どのように膨大な画面数/仕様が存在するアプリを1週間で作成したか、その道のりについて記述できればと思います。

キャッチアップとハッカソンの進行

基本的な進行方向としてはFlutter初見が5日間のハッカソンでアプリ開発してきた をご覧いただければと思います。

大枠で紹介すると、

  • 経験者を中心に技術の共有、キャッチアップ方針を定める
  • 毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす
  • オンラインでのコミュニケーションを円滑にするためのツールを活用する

の3つに特に注力していました。

経験者を中心に技術の共有、キャッチアップ方針を定める

自分は、1ヶ月強前にハッカソンで右往左往しながらFlutterのキャッチアップをしました。
その際に無駄だったことや、初めからやればよかったこと等の知見が溜まっていたため、チームメンバーに

  • どのような手順で
  • 何を参考に
  • いつまでに
  • 何をするか

をなるべく具体的に提示することで、効率よく学べるように心がけました。

具体的には、
Flutterは状態管理が少し難しい反面、UIは直感的に簡単に組めるため、
まず状態管理に慣れてもらいました。

  1. udemyを用いて StatefulWidget や、 Provider の概念を知り、
  2. 以前のおうちハッカソンで書いたコードを参考に ChangeNotifier を理解し、
  3. ブログや公式ドキュメント、自分のサンプル実装で StateNotifier を使いこなせるようになる

の流れで、初めの2〜3日の時間を使いました。

この状態管理packageを使う過程でWidgetの組み方もある程度は勉強できるため、4日後にはある程度実装できるようになっていたと思います。

この間に、自分は設計やCI,linterを導入したり、FirestoreのModelingで試行錯誤したり、快適に開発ができるような環境づくりに注力しました。

毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす

今回のハッカソンは1週間と、期間としては短くはないですが、それでも時間は限られています。

これはハッカソンに限った話ではないですが、限られた時間の中で形にするためには、
「やること」「やらないこと」を明確にする必要があります。

さらに、知らない技術に触れる中で「できないこと」も判別してタスクを組むことも大事になってきます。

これらを共通認識として保つために、毎日Discordで進捗確認をし、タスクの割り振りや棚卸し、ゴールから逆算したときの進み具合をすり合わせました。

みんな実装に気を取られていた中、これを率先してくれたメンバーに感謝です?

オンラインでのコミュニケーションを円滑にするためのツールを活用する

今回、コロナや居住地の関係でオンラインでのハッカソン開催となりました。

そのため、コミュニケーションやアイデア出し、その他諸々は工夫する必要がありました。

スクリーンショット 2020-06-24 19.35.12.png

  • Notion
  • Figma
  • miro
  • Whimsical
  • Discord

それぞれの詳細な仕様方法等は触れませんが、Notionでドキュメントやその他情報を管理し、進捗やスケジュールの共有はとても有意義でした。

技術の話

今回、膨大な仕様と画面数はさることながら、1番の頑張りポイントはは技術的な挑戦でした。

繰り返しますが、

3/4はFlutter初見、残りはFlutter歴1ヶ月

全員がFirebase初心者(サンプル触った程度)

です。

触ったことのないプラットフォーム 自体が挑戦でしたが、

さらに最近流行のpacakgeを使用する等、設計にもこだわりました。

Flutterアプリ全体のArchitecture

スクリーンショット 2020-06-24 19.37.30.png

発表スライドの貼り付けになりますが、アプリ全体のアーキテクチャとしては上図のようになります。

MVSN + Layered Architecture と書いていますが、SNはState Notifierのことです。

これは自分が作った造語で、実際にこういった呼び方のアーキテクチャがあるわけではありませんので悪しからず...。

MVVMのViewModelが、

Viewを描画するための状態の保持と、Viewから受け取った入力を適切な形に変換してModelに伝達する役目を持つ

とwikiに定義されているため、広義の意味ではMVVMなのかもしれません。

ただ、普段Androidをしている自分のイメージなViewModelとは少し構造が違うように感じたため、

MVSNと呼んでみました。

使用したpackage

State Notifier

今回の設計の要となるpacakgeです。

スライドに記載したり、上述した通り、State NotifierはつまりViewModelです。

View側で必要な状態をStateクラスで持ち、StateNotifierを継承したクラス側で、状態を変更してあげます。

View側では、Provider packageを利用して単発のイベント呼び出したり、StateをObserveしておくことで、Stateに変更があった場合にWidgetを再描画したりします。

文字だけではイメージがつきづらいと思うため、例をあげます。

ex.) プロフィール詳細画面で、RepositoryからUser情報を取得し、Viewに反映する例

ProfileDetailState
@freezed
abstract class ProfileDetailState with _$ProfileDetailState {
  const factory ProfileDetailState({
    User user,
  }) = _ProfileDetailState;
}

freezedに関しては後述しますが、このProfileDetailStateをKotlinでいうData Classとして記述している

と理解して大丈夫です。

色々書いてますが、基本はfreezedのお作法でLive Templateで補完できるため、ここでは User という、プロフィール詳細画面で表示するべき状態をもっていることに注目です。

ProfileDetailController
class ProfileDetailController extends StateNotifier<ProfileDetailState> with LocatorMixin {
  ProfileDetailController() : super(const ProfileDetailState());

  UserRepository get userRepository => read<UserRepository>();

  Future<void> getProfileDetail() async {
    final User user = await userRepository.getMyInfo();
    state = state.copyWith(user: user);
  }
}

さきほどの ProfileDetailState を持つ、StateNotifierを継承したクラスを作成します。

ここでのポイントは2つ

  • LocatorMixinを使って UserRepository をinject(read)している
  • UserRepositoryから取得したuserを state = state.copyWith(user: user); で更新している

です。

ChangeNotifierとの違いは、Controllerクラス(StateNotifier継承クラス)でローカル変数を持たず、StateNotifierのstateの状態を変更してあげるだけで良いことです。

notifyListeners をわざわざ呼ぶ手間は省けますね。

ちなみに、Controllerは基本的には画面ごとに持っています。

ProfileDetailPage
class ProfileDetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return 
    // 略
    body: StateNotifierProvider<ProfileDetailController, ProfileDetailState>(
        create: (_) => ProfileDetailController(),
        child: Builder(
          builder: (context) {
            return Column(
              children: <Widget>[
                Text(context.select<ProfileDetailState, String>((state) => state.user?.fullName ?? 'no name')),
                GestureDetector(
                  onTap: () async {
                    await context.read<ProfileDetailController>().getProfileDetail();
                  },
                  child: const Icon(Icons.add),
                )
              ],
            );
          },
        ),
      ),
   // 略
  }
}

状態をobserveする際は、

context.select<ProfileDetailState, String>((state) => )

単発のイベントを呼ぶ際は

context.read<ProfileDetailController>().getProfileDetail()

のようにしています。

state_notifier に関してよりくわしく知りたい方は

安定の @itomeさんのブログ を参照ください。

自分の環境設定が悪いのか、

readやselectの補完がデフォルトでは出ず、わざわざ手打ちでprovider pacakgeのimport文を書く必要があったところは少し不便でした。

freezed

freezedに関しては、上記の @itomeさんのブログ や、他にも色々な記事があるためここでは詳細には紹介しません。

簡単にいうと、KotlinのData Classや、Sealed Classに相当するデータ構造クラスが簡単に作成できるpacakageです。

上記のStateNotifierの例で言うと、

state = state.copyWith(user: user);copyWith メソッドがfreezedの機能にあたります。

冗長なコードを書かずに完結にstateの状態を変更できるため、StateNotifierと相性が良いです。

とても便利なのですが、コード生成が伴うため、

  • file構造が少し煩雑になってしまう
  • Modelの変更をしたら再度コード生成コマンドを打つ必要がある(忘れがち)

と、少し不便なところもありました。

Json Serializable

上記のfreezedと併用するとより効果を発揮するpackageです。

fromJsonや、toJsonを、ボイラープレートなしにコード生成してくれるpacakgeです。

控えめに言って最高です。

ただこちらも、コード生成が伴うため、freezedで述べたデメリットや、

  • プリミティブ型ではない独自のModelクラスは Converterを書く必要があり、ちょっと面倒くさい
  • ModelにListが入っている場合、コード生成されたクラスで型が宣言されておらずlintエラー( missing type parameter )が表示されるのがつらい

というつらみポイントもありました。

RxDart

Widgetへの状態変更通知に関してはStateNotifierを使用しましたが、
ログイン状態の変化等の、RepositoryからControllerへの変更通知はStreamを使用することにしました。

Dart標準のStreamでも事足りるのですが、BehaviorSubjectが使用したかったためRxDartを採用しました。

自分は普段Androidでインターンをしているため、LiveData/Coroiutineの世界に馴染みがあり少し詰まったポイントではありました。

FirestoreのModeling(議論とご指摘ほしいです)

今回1番苦労したのがこのFirestoreのModelingと実装です。(本当にきつかった、いまだに良く分からない)

AndroidとFlutterというクライアントサイドしか経験がなく、RDBすらまともに設計したことがない状態から、NoSQL風かつSubcollectionという特殊な概念を持ち合わせたFirestoreの設計をしたため、フィードバックいただけるとすごくありがたいです?‍♂️

概要

スクリーンショット 2020-06-24 19.39.40.png

こんな感じで設計しました。

※実装している間に辛いところがちょくちょくあったりして、改善の余地ありまくりです。

Modelingに関しては、firestore-data-modelingを参考に勉強しました。

User - Hackathon(多対多)

スクリーンショット 2020-06-24 19.40.28.png

まず、このアプリのコンセプトが

簡単にオンラインハッカソンを企画/運営できる というものです。

その中に、ユーザーが参加する際に入力すべき項目を減らし、参加障壁を低くするという目的もあります。

そのため、rootにUserがありアプリ全体としてユーザーのプロフィール等を保持し、並列してHackathonなcollectionがある感じです。

Discordをイメージしていただきたいのですが、

Discordでは、新しい サーバー に参加した際にroot?のUser情報を元にアイコンと名前が表示されます。

このHack ×2も同様に、新しいハッカソンに参加した際にrootのUser情報を使いまわしたく、この設計にしました。

多対多の表現は少し困ったのですが、中間テーブルを設けることで表現してみました。

どのユーザーがどのハッカソンに所属しているかを取得する中間テーブルかつ、ドロワーに参加しているハッカソンのアイコンを表示させたいので、urlも同時に持たせることで読み取り回数を減らしました。

Hackathon - Participant/Group/Notification(1対多)

スクリーンショット 2020-06-24 19.41.03.png

Participant, Group, Notificationは全てHackathonの SubCollection で持っています。

なぜなら、それぞれいくらでもスケールし得て、documentへの埋め込みだと1MBを超える可能性があるためです。

1対多に関しては、他にもroot collectionで持っている記事があったり、最適があまりよくわかっていません。

Participant とは、ハッカソンの参加者を表しており、Userを埋め込みで持っています。

Userをラップしており、他にはハッカソンで必要な情報(酸化可能日、稼働可能日数、希望職種etc...)のプロパティを持っています。

Group はその名の通り、ハッカソンで組むグループです。(チームという命名のほうが正しい...?)

GroupParticipant は1対多の関係で、Participantは何人になるか不明なため SubCollection で持たせるようにしています。

Notification は今回時間の関係(Modelingで分からないこともあり...)で実装していません。

  • ハッカソンの管理者がお知らせを送信することができる
  • 参加者はお知らせ画面で閲覧することができる
  • 通知バッヂをつける

の要件があるとき、

Hackathon : Notification = 1 : 多 になると思うのですが、

Participant : Notification = 1 : 多 にもなる感じなのかな...?

参加者の既読状況を表すのにはどうするのが正解なんでしょう...。

よければコメントいだければ幸いです。

全体図

Notification周りは未完成なのと、dartのModel Classとして記載しています。

スクリーンショット 2020-06-24 19.41.18.png

実装

Androidでは簡単なサンプルを実装してみたことがあるのですが、FlutterでFirestoreを扱うのは初めてだったので色々つらみがあありました。

Firestoreからデータを取得してFlutterのfreezedなclassに変換する際、

  • idを別で取り出す必要がある
  • Future型で返却するためにこねくり回す
  • toJson、fromJson時、CustomObjectが内包されている場合はConverterを書く必要がある

ことが手間でした。
これが生コードなのですが、かなり汚く苦悩が見えると思います...

HackathonRepository
  // TODO: エラーハンドリング
  Future<Hackathon> getHackathon(String hackathonId) async {
    final DocumentReference hackRef = _firestore.collection('hackathons').document(hackathonId);
    Future<List<Map<String, dynamic>>> getJsonList(String collectionName) async =>
        (await hackRef.collection(collectionName).getDocuments()).documents.map((document) {
          if (document.data.isNotEmpty) {
            return document.data..putIfAbsent('id', () => document.documentID);
          } else {
            return <String, dynamic>{};
          }
        }).toList();

    // TODO: 並列実行 => fromJsonするやり方を調べる
    final List<Map<String, dynamic>> participants = await getJsonList('participants');
    final List<Map<String, dynamic>> groups = await getJsonList('groups');
    final List<Map<String, dynamic>> notifications = await getJsonList('notifications');

    await (await prefs).setString(HACKATHON_ID_KEY, hackRef.documentID);

    return Future.value(Hackathon.fromJson(hackSnapshot.data
      ..putIfAbsent('id', () => hackRef.documentID)
      ..putIfAbsent('participants', () => participants)
      ..putIfAbsent('groups', () => groups)
      ..putIfAbsent('notifications', () => notifications)));
  }

Hackathonに紐づいているSubcollectionごと取得する良い方法があれば教えていただきたいです。
※今は個別で取得して、 putIfAbsent で付け加えてる形。

また、
Firestoreにデータをセットする際に、

  • freezedなclassはidを@requiredにしているが、idはFirestore側で自動生成させたい場合にDTOクラスを作るのかパラメータをだけで渡すか

とかも結構面倒くさかったですね。

FirestoreのModel設計をコードを織り交ぜて解説している良い記事あれば教えていただきたいです。

おわりに

ハッカソンを通じて、話したことない同期同士で仲良くなったり、同期がどんなことが得意かが分かると同時に、技術的/非技術的な知見を互いに共有することができました。

今後も内定者間や人事の方と合同での企画を予定しているため、今回参加できなかった同期とも徐々に打ち解けていければ良いと思います。

そして、21年度の入社時までに色々な知見を溜め、仲を深め、入社時から即戦力として最高のパフォーマンスを出せるような新卒になれるように、組織として力を入れていきたいと思っています。

行動力、技術力、キャッチアップ力ともに同期を尊敬しました。

素晴らしいイベントでした。企画、運営、協力感謝です。

技術的な内容だけQiitaにして後はnoteに投稿するように分けようかな...?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dagger2 - Android Dependency Injection

DIとは

Dipendency Injection(DI)、「オブジェクトの注入」のこと。

要するに、依存性を自由に差し替えれるようにすることで、テストを実行しやすくしたり(モックを差し込む)、オブジェクトをそれぞれ管理できるようになる。

AndroidにおけるDIの必要性

例えば、SharedPreferencesにデータを保存したい場合、DIなしで実行すると、SharedPreferencesからデータをインスタンス化、保存、取得することになり、すべてアクティビティに似たような記述をしなければならない。
スクリーンショット 2020-06-24 15.34.25.png

この方法でアプリが大きくなると、最悪の場合改修不可能になる可能性がある。

なのでSharedPreferencesを毎回アクティビティでインスタンス化する代わりに、別のクラスから注入するようにする。

スクリーンショット 2020-06-24 15.37.29.png

Dagger2とは?

2012年にSquareの開発者によって開発されたライブラリ。

Dagger1は、クラスのインスタンスを作成し、Reflectionを介して依存関係を注入するために使用されていた。
その後Googleの開発チームと協力して、Dagger2はReflectionsを使用しない、はるかに高速なバージョンが導入されることになった。

Dagger2は、コンパイル時のAndroid依存性注入フレームワークであり、Java仕様要求(JSR)330を使用し、注釈プロセッサを使用する。

Dagger2で使用される基本的なアノテーションは以下。

@Module: 最終的に依存関係として提供されるオブジェクト構築をするクラス
@Provides: オブジェクトを返すModuleクラス内のメソッドで使用される
@Inject: 依存関係が要求されたことを示す(コンストラクタ/フィールド/メソッドで使用される)
@Component: Moduleを要求するクラスへ依存関係を渡すためのブリッジクラス
@Singleton: 依存関係において、単一のインスタンスを作成することを示す

Sample

main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.journaldev.dagger2.MainActivity">

    <EditText
        android:id="@+id/inUsername"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:hint="Username" />

    <EditText
        android:id="@+id/inNumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/inUsername"
        android:layout_margin="8dp"
        android:inputType="number"
        android:hint="Number" />

    <Button
        android:id="@+id/btnSave"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SAVE"
        android:layout_below="@+id/inNumber"
        android:layout_toLeftOf="@+id/btnGet"
        android:layout_toStartOf="@+id/btnGet"
        android:layout_marginRight="8dp"
        android:layout_marginEnd="8dp" />

    <Button
        android:id="@+id/btnGet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GET"
        android:layout_below="@+id/inNumber"
        android:layout_alignRight="@+id/inNumber"
        android:layout_alignEnd="@+id/inNumber" />

</RelativeLayout>

Moduleで依存性を定義

SharedPrefModule.java
package com.journaldev.dagger2;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;

@Module
public class SharedPrefModule {
    private Context context;

    public SharedPrefModule(Context context) {
        this.context = context;
    }

    @Singleton
    @Provides
    public Context provideContext() {
        return context;
    }

    @Singleton
    @Provides
    public SharedPreferences provideSharedPreferences(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }
}

Componentで依存クラスに渡すオブジェクトの定義

MyComponent.java
package com.journaldev.dagger2;

import javax.inject.Singleton;
import dagger.Component;

@Singleton
@Component(modules = {SharedPrefModule.class})
public interface MyComponent {
    void inject(MainActivity activity);
}

依存先で必要な箇所に@injectを記述(そこに注入される)

MainActivity.java
package com.journaldev.dagger2;

import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import javax.inject.Inject;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {


    EditText inUsername, inNumber;
    Button btnSave, btnGet;
    private MyComponent myComponent;
    @Inject
    SharedPreferences sharedPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();
        myComponent = DaggerMyComponent.builder().sharedPrefModule(new SharedPrefModule(this)).build();
        myComponent.inject(this);


    }

    private void initViews() {
        btnGet = findViewById(R.id.btnGet);
        btnSave = findViewById(R.id.btnSave);
        inUsername = findViewById(R.id.inUsername);
        inNumber = findViewById(R.id.inNumber);
        btnSave.setOnClickListener(this);
        btnGet.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {

            case R.id.btnGet:
                inUsername.setText(sharedPreferences.getString("username", "default"));
                inNumber.setText(sharedPreferences.getString("number", "12345"));
                break;
            case R.id.btnSave:
                SharedPreferences.Editor editor = sharedPreferences.edit();
                editor.putString("username", inUsername.getText().toString().trim());
                editor.putString("number", inNumber.getText().toString().trim());
                editor.apply();
                break;

        }
    }
}

さいごに

雑ですが、以上がDagger2の説明です。
今後アップデートします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】タップされた座標にあるビューが知りたい

ソース

class MainActivity : Activity() {

    private var views = mutableSetOf<View>()
    private var debugCount = 0

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

        // タップ時に判定したいViewを詰める
        views.add(findViewById(R.id.redView))
        views.add(findViewById(R.id.purpleView))
        views.add(findViewById(R.id.greenView))
        views.add(findViewById(R.id.yellowView))

        findViewById<ScrollView>(R.id.scrollView).setOnTouchListener(View.OnTouchListener(
            fun (view, event): Boolean {
                if (event.action == MotionEvent.ACTION_DOWN) {
                    val x = event.rawX.toInt()
                    val y = event.rawY.toInt()

                    for (view in views) {
                        if (getRect(view).contains(x, y)) {
                            Log.d("", "${debugCount++}: ${resources.getResourceEntryName(view.id)}がタップされた。")
                            break
                        }
                    }
                }

                return false
            }
        ))
    }

    private fun getRect(view: View): Rect {
        val rect = Rect()
        view.getGlobalVisibleRect(rect)
        return rect
    }
}

レイアウト

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ScrollView
        android:id="@+id/scrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingBottom="100dp">

            <View
                android:id="@+id/redView"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginStart="72dp"
                android:layout_marginLeft="72dp"
                android:layout_marginTop="152dp"
                android:background="#F44336"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="211dp"
                android:layout_height="179dp"
                android:layout_marginTop="312dp"
                android:layout_marginEnd="44dp"
                android:layout_marginRight="44dp"
                android:background="#FFC107"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent">

                <View
                    android:id="@+id/purpleView"
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:layout_marginTop="36dp"
                    android:layout_marginEnd="56dp"
                    android:layout_marginRight="56dp"
                    android:background="#9C27B0"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent" />
            </androidx.constraintlayout.widget.ConstraintLayout>

            <View
                android:id="@+id/greenView"
                android:layout_width="200dp"
                android:layout_height="100dp"
                android:layout_marginTop="556dp"
                android:background="#4CAF50"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <View
                android:id="@+id/yellowView"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginTop="820dp"
                android:background="#FFEB3B"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

実際の動作

sample.gif

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む