30歳からの開発日記

30歳でエンジニア転職した元営業の備忘録。

【Android】カスタマイズできる DialogFragment 継承クラスを作る【コピペでOK】

今回やること

DialogFragment を使ったダイアログを実装します。

ダイアログを表示するには DialogFragment を継承したクラスを自作する必要がありますが、毎回実装するのが手間なので、コピペで使えるシンプルなクラスを実装してみます。

DialogFragment の罠

この DialogFragment 継承クラスを作るとき、データを引数として渡すことは避けるべきです。ダイアログが表示された状態で画面を回転させるとダイアログも再生成されますが、このとき自動的に引数なしのコンストラクタを呼ぼうとするため、それがないとアプリがクラッシュします。

class SimpleDialogFragment(val title: String, val message: String, ...) : DialogFragment() {
    // ダメなパターン。DialogFragment の継承クラスは引数なしを実装するべき。
}

DialogFragment 継承クラスを実装するときは必ず引数なしコンストラクを用意しましょう。

(画面の回転でアクティビティやフラグメントを再生成しない設定をしている場合は関係ありません。)

実装

上記の罠を回避するために、必要なデータはセッターを使ってクラスに渡すようにします。

class SimpleDialogFragment : DialogFragment() {

    private var title: String = ""
    private var message: String = ""
    private var positiveButtonLabel: String = ""
    private var negativeButtonLabel: String = ""
    private var positiveButtonClickListener: DialogInterface.OnClickListener? = null
    private var negativeButtonClickListener: DialogInterface.OnClickListener? = null

    fun setTitle(title: String) {
        this.title = title
    }

    fun setMessage(message: String) {
        this.message = message
    }

    fun setPositiveButton(label: String, listener: DialogInterface.OnClickListener?) {
        positiveButtonLabel = label
        positiveButtonClickListener = listener
    }

    fun setNegativeButton(label: String, listener: DialogInterface.OnClickListener?) {
        negativeButtonLabel = label
        negativeButtonClickListener = listener
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        activity ?: throw IllegalStateException("Activity cannot be null.")
        val builder = AlertDialog.Builder(activity)
        return builder.setTitle(title)
            .setMessage(message)
            .setPositiveButton(positiveButtonLabel, positiveButtonClickListener)
            .setNegativeButton(negativeButtonLabel, negativeButtonClickListener)
            .create()
    }
}

使い方

スコープ関数を使うことで、タイトル、メッセージ、ボタンの設定がスマートに記述することができます。

val dialog = SimpleDialogFragment().apply {
    setTitle("タイトル")
    setMessage("メッセージ")
    setPositiveButton("はい", DialogInterface.OnClickListener {dialog, which ->
        Toast.makeText(this, "はいが押されました。", Toast.LENGTH_LONG).show()
    })
    setNegativeButton("いいえ", null)
}
dialog.show(supportFragmentManager, "sample_dialg")

【Android】RecyclerView のリスト表示を MVVM パターン+ DataBinding で実装する

今回やること

タイトルどおり、RecyclerView をMVVM パターン+ DataBinding で実装してみたいと思います。

前回の記事では双方向バインディングのサンプルアプリを作ってみました。これは Text 型の LiveData を TextView にバインドして、値が変わったら自動的に画面に反映するような実装でした。これと同じようなことを RecyclerView でやろうとすると、BindingAdapter の実装などちょっとした工夫が必要です。

BindingAdapter を使えがば RecyclerView に対して「この値が変わったら画面を更新してね!」と指示を出すことができます。つまり、これまで findViewById() で取得したビューをゴニョゴニョしていた処理を書かなくて済むので、少し幸せになれます。

今回はやることをざっくり言うとこんな感じです。

  • ViewModel に RecyclerView で表示したいリストアイテムを持たせる
  • レイアウトファイル内で RecyclerView にリストアイテムをバインドする
  • クリックでリストアイテムの中身を変更するボタンを設置(API に問い合わせる想定)
  • リストアイテムの変更で自動的に画面も変更!!

見た目は普通の RecyclerView です。

ソースファイルはこちらで公開してます。

github.com

最終的なディレクトリ構造

参考までに最終的なディレクトリ構造を載せておきます。

実装手順

1. 新しいプロジェクトを作成

まずは Empty Activity で新しいプロジェクトを作成します。

2. Gradel ファイルを編集

build.gradle (Module: app) を編集してデータバインディングの有効化とライブラリの追加を行います。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // 追加

android {
    ...

    // dataBinding を有効化
    dataBinding {
        enabled = true
    }

}

dependencies {
    ...
    
    // ViewModel と LiveData のライブラリを追加
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

3. ViewModel、User モデルクラスを作成

次に ViewModel と User モデルクラスを作成します。

UserViewModel という名前で Kitlin ファイルを作成して以下のとおり編集します。

/**
 * User モデルクラス
 */
data class User(var name: String)

/**
 * User の情報を保持する ViewModel。
 */
class UserViewModel : ViewModel() {

    // クラス内部で扱う LiveData は変更可能な形式で保持する。
    private val _users: MutableLiveData<List<User>> = MutableLiveData()
    // クラス外部に公開する LiveData は変更不可な形式で保持する。
    val users: LiveData<List<User>> = _users

    // 初期値として10人分のユーザー情報を LiveData に突っ込む。
    init {
        val names = listOf(
            "一郎", "二郎", "三郎", "四郎", "五郎", "六郎", "七郎", "八郎", "九郎", "十郎"
        )
        val users = names.map { name -> User(name) }
        _users.value = users
    }

    /**
     * API 通信してユーザー情報を取ってくることを模した関数。
     * 今回は通信せずにリストをシャッフルする仕様にしている。
     */
    fun fetchUsers() {
        val shuffled = _users.value?.shuffled()
        _users.value = shuffled
    }
}

4. レイアウトファイルを作成、編集

activity_main.xml

リストの中身を更新するためのボタンと RecyclerView を設置します。

<?xml version="1.0" encoding="utf-8"?>
<!-- データバインディングを行うため、ルートを layout タグにする。 -->
<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 タグの中で定義する。 -->
    <data>
        <!-- 変数 viewModel を定義する。 -->
        <variable
            name="viewModel"
            type="com.example.bindingadaptersample.UserViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- ボタンクリックで UserViewModel#fetch を発火させる。 -->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.fetchUsers()}"
            android:text="FETCH!" />

        <!-- BindingAdapter を利用して users という属性を自作している。 -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:users="@{viewModel.users}"
            tools:context=".MainActivity" />

    </LinearLayout>

</layout>

row_user_list.xml

リスト1行分のレイアウトファイルを新規作成します。

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

    <TextView
        android:id="@+id/user_name"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:gravity="center_vertical"
        android:padding="8dp"
        android:textSize="30sp"
        android:textStyle="bold" />

</layout>

5. リストアダプターを作成

UserListAdapter という名前で Kotlin ファイルを追加して以下のとおり編集します。

/**
 * ユーザーリスト用の Adapter。
 */
class UserListAdapter(
    private var users: List<User>,
    private val owner: LifecycleOwner
) : RecyclerView.Adapter<UserListAdapter.UserViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding: RowUserListBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.row_user_list,
            parent,
            false
        )
        return UserViewHolder(binding)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = users[position]
        holder.binding.apply {
            userName.text = user.name
            lifecycleOwner = owner
        }
    }

    override fun getItemCount(): Int {
        return users.size
    }

    /**
     * リストの中身を更新する。
     */
    fun update(users: List<User>) {
        this.users = users
        notifyDataSetChanged()
    }

    /**
     * ユーザーリスト用の ViewHolder。
     */
    inner class UserViewHolder(val binding: RowUserListBinding) :
        RecyclerView.ViewHolder(binding.root)
}

6. BindingAdapter を作成

UserListAdapters という名前で Kotlin ファイルを追加して以下のとおり編集します。

RecyclerView を拡張した関数なので adapter が取得できます。ただし、もし複数の RecyclerView に対して app:users="@{xxx}" を設定していた場合、それがどの RecyclerView なのか判別する必要があります。そのために when() で型をチェックしてから adapter.update() を実行しています。

object BindingAdapters {
    /**
     * RecyclerView の拡張関数を定義。
     * レイアウトファイル内で users という属性を設定できるようにする。
     */
    @BindingAdapter("users")
    @JvmStatic fun RecyclerView.bindUsers(users: List<User>?) {
        users ?: return
        // adapter が UserListAdapter ならリスト情報をアップデートする。
        when (val adapter = this.adapter) {
            is UserListAdapter -> adapter.update(users)
        }
    }
}

7. MainActivity を編集

最後の仕上げに MainActivity を編集します。

class MainActivity : AppCompatActivity() {

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

        // UserViewModel を取得。
        val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        // gradle ファイルで DataBinding を有効にするとバインディングクラスが自動生成される。
        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.also {
            it.viewModel = viewModel // レイアウトファイルで定義した viewModel にバインド。
            it.lifecycleOwner = this
        }

        val users = viewModel.users.value ?: listOf()
        val adapter = UserListAdapter(users, this)
        val manager = LinearLayoutManager(this)
        val decoration = DividerItemDecoration(this, manager.orientation)
        binding.recyclerView.also {
            it.adapter = adapter
            it.layoutManager = manager
            it.addItemDecoration(decoration) // リストアイテム間に境界線をつける。
        }
    }
}

処理の流れ

画面が更新される流れをざっくり表すと以下のようになります。

  1. ボタンをクリック
  2. UserViewModel#fetch が発火
  3. UserViewModel のメンバ変数 users の値が変化
  4. 値の変化を検知して RecyclerView#bindUsers() が発火
  5. UserListAdapter#update() が発火して画面を更新

今回は参考になる記事が少なく少し苦労しましたが、とても便利そうなのでどんどん使っていきたいと思います。

【Android】双方向データバインディング(Two-way data binding)の簡単なサンプルを作る

はじめに

今回は双方向データバインディングを理解するために簡単なアプリを作成してみました。

双方向データバインディングによって、ユーザーの入力値をビューに反映するようなロジックがとても簡単に実装できるのでぜひ覚えておきたい技術の1つです。

今回作るもの

入力欄に名前を入力してボタンを押すと、TextView に「Hello, XXX!」と表示される簡単なアプリを作ってみます。

TextView に入力したテキストと、プログラム内部で保持しているデータがリンクするように実装することで、入力テキストを画面に反映させる処理が簡単に記述できるのが実感できるかと思います。

ソースファイルはこちらで公開してます。

github.com

データバインディングとは

まず、データバインディングについて簡単に説明しておきたいと思います。

データバインディングとは、ビューとデータを結びつけて、どちらかの値が変更されたときもう片方の値も自動的に変更される仕組みのことです。結びつきが一方通行だと One-way、お互いに結びついていると Two-way(双方向)のデータバインディングとなります。

通常、Android 開発で画面を更新したい場合は以下のように findViewById() を使ってビューオブジェクトを取得し、値をセットします。

val textView: TextView = findViewById(R.id.text_view)
textView.text = "Hello World"

データバインディングを利用すれば、画面に表示する値をレイアウトファイル内で直接割り当てることができます。

<TextView
        ...
        android:text="@{viewModel.text}" />

ここでは TextViewviewModel.text を紐づけています。viewModel.text の値が変更されれば、表示されているテキストも自動的に変更されるため、逐一 findViewById() でビューを取得する必要がありません。findViewById()は重たい処理でもあるのでメモリにも優しい実装になります。

データバインディングはビューロジックとビジネスロジックを分離してくれます。個人的には、ビジネスロジックの開発においてビューロジックを意識する必要がなくなる(逆もしかり)ので、あれこれ考えず開発に集中できるのが嬉しいポイントです。データバインディングを使って可読性が高く、保守しやすいアプリ開発を目指しましょう!

最終的なディレクトリ構造

参考までに最終的なディレクトリ構造を載せておきます。

最終的なディレクトリ構造

実装手順

1. 新しいプロジェクトを作成

まずは Empty Activity で新しいプロジェクトを作成します。

2. Gradle を編集

build.gradle (Module.app) を編集してデータバインディングライブラリを利用できるようにします。

また、ViewModelLiveData のライブラリも追加します。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' // 追加

android {
    ...

    // dataBinding を有効化
    dataBinding {
        enabled = true
    }

}

dependencies {
    ...
    
    // ViewModel と LiveData のライブラリを追加
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

ネットの記事を読んでいると lifecycle-extensions を追加している記事をよく見かけますが、Lifecycle バージョン 2.2.0 からはサポートが終了しているので注意してください。developers にも以下のとおり記載されています。

lifecycle-extensionsAPI はサポートが終了しました。代わりに、必要とする特定の Lifecycle アーティファクトの依存関係を追加してください。

詳しくは developers の Lifecycle のページをご確認ください。

3. ViewModel、User モデルクラスを作成

次に ViewModel と User モデルクラスを作成します。

UserViewModel という名前で新しくファイルを作成し、以下の内容を実装します。

/**
 * User モデル。
 */
class User(var name: String)

/**
 * User に関するデータを扱う ViewModel。
 */
class UserViewModel : ViewModel() {

    // クラス内では値の変更が可能なミュータブルな LiveData を扱う。
    private val _user = MutableLiveData(User("John Doe"))
    private val _greeting = MutableLiveData("Hello, ${_user.value?.name}!")

    // 自由に値が変更できない形式で外部に公開。
    val user: LiveData<User> = _user
    val greeting: LiveData<String> = _greeting

    // ボタンがクリックされたら表示する文章を更新。
    fun onClick() {
        _greeting.value = "Hello, ${user.value?.name}!"
    }

}

ViewModel クラスを継承したUserViewModel クラスと、name というメンバ変数を持った User モデルクラスを作成しました。User クラスは1行しかないので今回は1つのファイルにまとめてますが、本来は分けたほうがいいかと思います。

UserViewModel クラスでは LiveData を使ってデータを保存しています。LivaData は Activity や Fragment などのライフサイクルに合わせて監視可能なデータホルダークラスです。

4. レイアウトファイルを編集

先ほど定義した UserViewModel をレイアウトファイル内で扱うためにレイアウトファイルを編集していきます。

<?xml version="1.0" encoding="utf-8"?>
<!-- データバインディングを行う場合、ルートが layout タグである必要がある。 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- レイアウトファイル内で使う変数を data タグの中で定義する。 -->
    <data>
        <!-- 変数 viewModel を定義する。 -->
        <variable
            name="viewModel"
            type="com.example.hellodatabinding.UserViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <!--
         android:text の値が @={...} であることに注意。
         ユーザーの入力値を ViewModel に反映させたい場合はイコールが必要。
         -->
        <EditText
            android:id="@+id/edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Please input your name!"
            android:inputType="text"
            android:text="@={viewModel.user.name}" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end"
            android:onClick="@{() -> viewModel.onClick()}"
            android:text="BUTTON" />

        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="20dp"
            android:text="@{viewModel.greeting}"
            android:textSize="26sp" />

    </LinearLayout>
</layout>

5. Activity を編集

最後に MainActivity を編集して、3で定義した UserViewModel とレイアウトファイル内で定義した変数 viewModel を関連づける処理を追加します。また、MainActivity を lifecycleOwner として設定します。

class MainActivity : AppCompatActivity() {

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

        val userViewModel =
            ViewModelProvider(this).get(UserViewModel::class.java)

        val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.also {
            // レイアウトファイルで定義した viewModel にバインド。
            it.viewModel = userViewModel
            it.lifecycleOwner = this
        }

        /**
         * 双方向データバインディングでは以下のようなリスナーは必要ない。
         */

//        binding.editText.addTextChangedListener { text ->
//            userViewModel.setName(text.toString())
//        }
    }
}

これで完成です。

冒頭に載せた GIF のように、名前を入力してボタンを押せば画面のテキストが変わるのが確認できるかと思います。

【Android】Glide を使って角が丸い画像を表示させる

画像ライブラリでお世話になっている Glide ですが、画像の角を丸くしたい場合は以下のとおり。 Glide のバージョンは 4.11.0 を使用しました。

Glide.with(context)
        .load(imageUrl) // 画像のURL
        .transform(RoundedCorners(8)) // 丸み加減を数値で指定
        .into(imageView) // ImageView につっこむ

【Android】ViewPager2 と TabLayout を使って、スワイプで切り替わるタブを作る

今回作るもの

f:id:u-dai38:20200526115924g:plain
ViewPager サンプル

ViewPagerTabLayout を使って、タブとタブの間をスワイプで移動できるページを作ってみます。

完成版はこちらで公開してます。

github.com

ViewPager2 を使おう

Android Developers に以下の記載がありました。

ViewPager2 1.0.0 のリリースに伴い、ViewPager と連携するための FragmentPagerAdapter クラスと FragmentStatePagerAdapter クラスは非推奨になりました。ViewPager から ViewPager2 への移行をご覧ください。

ViewPager 自体は非推奨ではないみたいですが、それと連携するためのクラスが非推奨になったようです。

ネットでは ViewPager で書かれている記事が多いのですが 、今回は ViewPager ではなく ViewPager2 を使ってスワイプでタブが切り替わるページを作りたいと思います。

最終的なディレクトリ構造

Empty Activity のテンプレートで新規プロジェクトを作り、最終的には以下のようなディレクトリ構造になります。参考までに。

f:id:u-dai38:20200526120027p:plain
最終的なディレクトリ構造

依存関係をプロジェクトに追加

まず初めに ViewPager2TablLayout を利用するため、build.gradle にマテリアルデザインのライブラリを追加します。

dependencies {
    ...
    // 以下を追加
    implementation 'com.google.android.material:material:1.2.0-alpha06'
}

ViewPager2 と TabLayout をレイアウトに追加

さっそく ViewPager2TabLayout をレイアウトファイルに追加していきます。activity_main.xml を以下の内容に書き換えます。

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

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

ViewPager では TabLayout タグの中に ViewPager を入れる構造になっていましたが、ViewPager2 からは TabLayout と同じ階層に配置するようになりました。

Fragment を2つ作成

画面に表示させる Fragment を2つ作ります。最終的に、この2つの Fragment をスワイプして切り替えることができるようになります。

1. PageOneFragment

PageOneFragment という名前で空のフラグメントを作成します。このとき Include fragment factory method? のチェックを外しておきます。

PageOneFragment.kt

Kotlin ファイルは作成したままの状態で OK です。念のためコードを載せておきます。

class PageOneFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_page_one, container, false)
    }

}

fragment_page_one.xml

レイアウトファイルはわかりやすいように背景色をつけて、中央に文字を配置しておきました。

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textSize="40sp"
        android:text="Hello, page one!" />

</FrameLayout>

2. PageTwoFragment

同じ要領で2つ目のフラグメントを作成します。ほとんど1つ目と同じ内容です。

PageTwoFragment.kt

class PageTwoFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_page_two, container, false)
    }

}

fragment_page_two.xml

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textSize="40sp"
        android:text="Hello, page two!" />

</FrameLayout>

アダプターを作成

次にアダプターを作成します。

アダプターは移動する位置に従って表示データを生成する役割を持ったクラスです。今回作るクラスでは FragmentStateAdapter インターフェースを実装させますが、これはフラグメント間を移動させる場合に使用します。

フラグメントではなくビューの間を移動させることもできます。その場合は RecyclerView.Adapter インターフェースを実装させます。カルーセルのように画像や文字情報だけを持ったシンプルな画面を移動させるときなどはこちらの方が適していると思います。

新しく Kotlin ファイルを作成し、以下の内容を実装します。

class SampleViewPagerAdapter(activity: AppCompatActivity) : FragmentStateAdapter(activity) {

    /**
     * 表示する画面の合計数を返す。
     * 今回は画面(タブ)2つを作るので2を返す。
     */
    override fun getItemCount(): Int = 2

    /**
     * 画面のポジションと対応させてフラグメントを返す。
     * 今回は画面が2つなので、position は 0 か 1 となる。
     * もしそれ以外の引数が渡された場合はとりあえず例外を投げる。
     */
    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> PageOneFragment()
            1 -> PageTwoFragment()
            else -> throw IllegalArgumentException()
        }
    }

}

MainActivity の編集

最後に MainActivity クラスを編集していきます。 findViewById()ViewPager2 オブジェクトを取得し、そこに先ほど作成したアダプターをセットします。また、TabLayoutMediator クラスを使って ViewPagerTabLayout を連携させます。ここで同時にタブのタイトルも設定します。

class MainActivity : AppCompatActivity() {

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

        // viewPager にアダプターをセット。
        val viewPager: ViewPager2 = findViewById(R.id.pager)
        val adapter = SampleViewPagerAdapter(this)
        viewPager.adapter = adapter

        // ViewPager と TabLayout を連携させる。
        val tabLayout: TabLayout = findViewById(R.id.tab_layout)
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            // タブのタイトルを設定。
            tab.text = "PAGE${position + 1}"
        }.attach()
    }
}

以上で完成です!

元営業が30歳でWeb系自社開発企業に転職するまで

 

ごあいさつ

はじめまして。ユウダイと申します。

完全未経験の状態からプログラミングを勉強し始めて、30歳にして先日 Web 系自社開発企業から内定をいただくことができました。

今回は自分への振り返りの意味もかねて、内定を獲得するまでにとった具体的な行動や、自分が注意していたことなどを振り返ってみたいと思います。

最近はエンジニアの需要が高まっているので、私と同じように未経験からエンジニア転職を考えている方も多いと思います。そんな方々に少しでも参考になる記事となれば嬉しいです。

 

筆者の経歴について

最初に自分の経歴を簡単にご紹介します。

  • 文系私立大学卒業
  • 翻訳会社 営業(2013年4月〜)
  • プログラミングスクール(2018年1月〜)
  • マーケティングリサーチ会社 社内SE(2018年4月〜)
  • Web系自社開発企業 エンジニア(2020年4月〜)

新卒で入社した翻訳会社を約5年で辞めてプログラミングスクールに入り、Ruby on Rails を使ったウェブアプリケーションの開発について学びました。

その後、プログラミングスクールに紹介してもらったリサーチ会社にエンジニアとして入社。もともとは社内システムの開発担当として入社したのですが、社内体制や方針が変わったこともあり、最終的に開発業務から離れてマネジメント寄りの業務を担当していました。

開発できないことにもどかしさを感じ、ガツガツ開発ができる会社を求めて転職を決意しました。

 

自社開発企業を志望する理由

私が受託ではなく自社開発企業を志望する理由は2つあります。

  • 入社前から使う技術が明確になっている
  • 当事者意識を持って取り組める

技術的な側面で言えば、使う技術が明確になっているところが受託会社と違います。自社開発の場合、同じサービスを開発し続けることになりますので使う技術がコロコロ変わることはありません。モダンな開発環境の会社に入社することができれば、しばらく同じ技術を使い続けることができるのでエンジニアとしての価値を上げ続けることが可能です。

もう1つは当事者意識を持って開発に取り組むことができるという点です。受託開発はクライアントが求める成果物が納品できれば売上が上がります。それに対して自社開発ではサービスの質が悪ければユーザーに使ってもらえませんし、結果として売上も上がりません。どうすればユーザーが喜ぶか、という視点を持ち続けながら開発に取り組むことになりますし、それが達成できたときに大きな達成感をもたらしてくれると思います。

受託開発にもメリットがあるのは理解していますが、個人的には上記のような理由で自社開発の会社を志望していました。

 

Web系自社開発企業へ転職するためにとった具体的な行動

それでは私が Web 系自社開発企業へ転職するときにとった具体的な行動について振り返ってみたいと思います。ざっくり言うと以下のような段取りで行動していました。

  1. ポートフォリオ作成
  2. 職務経歴書の作成
  3. 求人への応募
  4. 面接対策の実施
  5. 面接と反省のループ

1. ポートフォリオ作成

エンジニアとして(特に自社開発の企業に)転職したいのであれば、自分の実力を示すためにポートフォリオの作成が必須です。私も、転職したいと思い何も考えずに転職エージェント2社にに駆け込んだことがありました。

 

エージェント「ポートフォリオ作ってから来てね^^」
ぼく「ぐぬぬ

 

ここからポートフォリオ作成が始まりました。

どんなポートフォリオを作ったの?

複数人で共有できる家計簿アプリを約1.5ヶ月かけて作成しました。支出や収入の入力など、ほとんどの処理を非同期で行うことで、シンプルかつストレスレスに使えるサービスに仕上げました。サーバーサイドは Laravel、フロントエンドは React、インフラは AWS を使っています。

土日以外にも、平日の朝や会社の休憩時間なども利用してなんとか時間を作りながら開発を進めました。

f:id:u-dai38:20200224165759g:plain

ポートフォリオとして開発した家計簿アプリ

 

家計簿アプリを作ろうを思ったキッカケ

ポートフォリオ作成における最初の壁は「何を作るか?」だと思います。急に「なんでもいいから開発して」と言われてもなかなかネタが思い浮かびませんよね。。そんなときは、自分自身や親しい人が困っていることから見つけ出すことをオススメします。私の場合、彼女と同棲しているなかで、家のお金をスプレッドシートで管理するのが煩雑だったことから「複数人が共有して使える家計簿アプリがあったら便利かも」と思ったのがきっかけです。

開発のキッカケは意外と重要

転職活動におけるポートフォリオで「何を作るか」は意外と重要だと感じました。理由は2つ。モチベーションの維持と面接対策です。

自分自身が課題だと感じていることが解決できると、単純に嬉しいですよね。開発が楽しくなってモチベーションの維持につながります。また、面接で「なぜこのサービスを作ろうと思ったの?」と聞かれることが多いので、そのときの回答に具体性が増します。ポートフォリオを「なんとなく」で作ってしまうと、後々苦しくなってしまう可能性がありますので、慎重に検討することをオススメします。

評価されるポートフォリオとは?

ちなみに、どんなポートフォリオが評価されるのかについては参考になる動画がありますのでご紹介します。ここで紹介されている技術を盛り込むことができれば理想的ですが、難しいと感じるなら無理せずにできるところから取り組むのがいいと思います。私はこの動画に出会う前にポートフォリオを作ったので、正直なところあまり網羅できていませんw

www.youtube.com

ポートフォリオ作成で一番大切なこと

ポートフォリオ作成で私が一番大切にしていたことは「とにかく作り上げること」です。開発していると細かい部分がいろいろと気になってしまいますが、動くのであればとにかく前に進むのが吉。"Done is better than perfect." を心に刻んでいました!

2. 職務経歴書の作成

ポートフォリオがある程度形になったら職務経歴書を作成しました。これまで自分が経験してきた業務を時系列で並べ、その中で自分が工夫したことやアピールしたいことなどを盛り込みます。私の場合 Google ドキュメントで作成し、公開できるようにしておきました。リンクを Wantedly、Green に貼り付ければ共有して利用できます。

ここで個人的に気をつけていたことは「読みやすさ」です。フォント、文字の大きさや色、行間などに気を使って、パッと見たときに「読みやすい」と思ってもらえるように工夫しました。例として、私の職務経歴書の一部を掲載します。ここでは表内における文字の位置を統一して、パッと見で理解できるように工夫しています。

f:id:u-dai38:20200224160226p:plain

職務経歴書のスキルセット欄

職務経歴書は様々なテンプレートが公開されていますが、読みやすさの観点から個人的に納得のいくテンプレートがなかったため自分で作成しました。採用担当も時間がないなか資料に目をとおしているはずです。少しでも読みやすく、ストレスがない資料を作ることに気を使っていました。

3. 転職サイトへの登録

転職サイトは Wantedly と Green を利用し、最終的に Green でつながった会社さんから内定をいただきました。エンジニア転職なら、この2つのサイトで十分でしょう。もし志望度が高く、どうしても行きたい会社があれば直接応募するのも効果的かと思います。

未経験の場合どうしても書類の通過率は低くなってしまうので、気になる会社があればガンガン応募しました。

www.wantedly.com

www.green-japan.com

 

転職エージェントも複数利用しましたが、個人的にはあまりオススメできません。エージェント側は売上のためにとにかく転職させたいと思っているので、興味のない会社でもガンガン勧められます。自力でも十分転職活動できますので、わざわざエージェントを利用する意味はないかなと感じました。

※ あくまで個人的な意見ですので悪しからず...

4. 面接対策の実施

書類が通って面接まで進むことができたら、以下のような面接対策を行いました。

鉄板質問に対する回答を作る

面接で良く聞かれる質問をネットで調べ、それに対する回答を考えます。回答を作るときは実際に面接で話す台本を作るつもりで文字に書き出します。面接前は、書き出した回答をスラスラと言えるようになるまで実際に声に出して練習していました。

会社のことを調べる

面接先の会社に関する情報を収集します。事業内容、売上、従業員数、求めている人物像や使用技術など。これに加えて openwork などの口コミサイトや社長のインタビュー記事など、参考になりそうな情報をかたっぱしから集めます。

会社のことを詳しく知ることで面接時の話のネタにもなりますし、志望理由や逆質問を考えるとき必ず必要になります。

志望理由をまとめる

志望動機は個人的にかなり悩む部分でした。未経験という立場上どうしても会社に give できることが少なく、taker 的なスタンスの志望動機になってしまうからです。

「開発環境がモダンで成長できると思ったから」

このような志望理由は自分にとってメリットはあっても会社にとってメリットはありません。志望理由では自分の強みが会社で活きることを強調すると相手に刺さりやすいと思います。

例えば、求人票などで「エンジニアからも提案ができる環境」と書かれていたら「元営業としての提案力を活かせる」という点を志望動機に盛り込むこともありました。自分の強みと会社が求めている人物像で重なっている部分を見つけることが、相手に刺さる志望動機を作るコツかもしれません。

逆質問を準備する

逆質問は、相手がエンジニアなのか人事なのか社長なのかで全然内容が変わってくるので、相手の立場は事前に知っておく必要があります。人事に対して技術的な質問ばかりしても困らせてしまうかもしれませんよね。そのため面接では次に出てくる人の立場を確認するようにしていました。

私が逆質問をするうえで注意していたのは、質問する意図も伝えるという点です。例えば、私はよく以下のような質問をしていました。

「サーバーサイドとフロントエンドでエンジニアを分けていますか?」

これだけで聞いてしまうと Yes/No しか返ってきませんし、相手は「それで?」という気持ちになってしまいます。これに「サーバーサイドだけでなくフロントエンドにも関心があるのですが」という内容を加えることで、相手に「なるほど、フロントもやりたいのね」という意図が伝わります。「基本的には分業しているけど、手を挙げればチャレンジできるよ」という、本当に欲しかった回答が得られますし、コミュニケーションが円滑になり印象が良くなります。

5. 面接と面接の振返り

装備が揃ったところで、いざ面接へ!

とはいえ、いくら準備しても思いどおりにいかないのが面接です。意外な質問をされたり、深掘りされてうまく答えられないような場面も出てきます。そんなときは単純に「次回同じこと聞かれたら答えられるようにしよう」と思うようにしていました。答えられない質問があることに気づけただけでも面接に行った価値があります。

また、自分が面接で言ったことや聞いたことは終わったあとすぐにメモを取るようにしていました。当然ですが、面接での受け答えは次の面接官とも共有されています。次の面接で矛盾するようなことを言ってしまうと印象を悪くしてしまうこともあるので気をつけていました。

  • うまく答えられなかった質問は答えられるようにする
  • 面接でのやりとりはメモして次の面接でのネタにする
  • 少しでも興味があればとにかく受けに行ってみる(数をこなす)

こんな感じで面接に臨んでいました。

 

まとめ

面接が思いどおりにいかずしんどい時期もありましたが、なんとか自分のやりたいことができる会社から内定をいただくことができました。転職はゴールではなくスタートなので、ここで気を緩めず引き続きスキルアップに磨きをかけていきます。

これから数年間は鍛錬の期間となります。5年後、10年後のことはあまり考えていないですが、そのときそのときで心の赴くまま、やりたいことを全力で楽しめる人生を歩んでいきたいと思っています。

勉強会や交流会にも積極的に参加したいなーと思っています。もしどこかでお会いすることがありましたら仲良くしてください。一緒に切磋琢磨していきましょう!