Android（ネイティブ）で開発とflutterで開発の比較

## はじめに
モバイルエンジニア歴2年の若手です。
この2年でAndroid開発とflutterでの開発の両方に携わることが出来たので、それを通して感じたことを書いていきます。
今回は比較にあたって、ポモドーロタイマーのアプリを作成いたしました。

## 仕様
- 初回起動時に25分（作業）のタイマーがセットされている
- 画面には開始ボタンと停止ボタンがある
- 以下のサイクルでタイマーが使用できる

1. スタートボタンを押すことでタイマーが開始される
1. タイマーの残りの時間が0秒になると5分（休憩）のタイマーがセットされる
1. スタートボタンを押すと休憩時間が開始される
1. タイマーの残りの時間が0秒になると25分（作業）のタイマーがセットされる
1. 項番1に戻る

- タイマー作動中に停止ボタンを押すと25分の作業時間にリセットされる

## アプリ画面
- Androidで開発した画面  
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544661/bafc7cfa-7ae9-ddbc-2747-960a8e63c4d7.jpeg" width="300">
- flutterで開発した画面  
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/544661/fdf78188-f89d-6800-2932-4ec456632079.jpeg" width="300">

## ソースコード 

### Android

[GitHub](https://github.com/ibuki-iida/androidPomodoro)  

- MainActivity.kt  

``` kotlin
package com.example.androidpomodoro

import android.os.Bundle
import android.os.Handler
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*

class MainActivity : AppCompatActivity() {
    private val brakeTime: Int = 300 // 5分
    private val workTime: Int = 1500 // 25分
    private var current: Int = workTime
    private var isWorkTime: Boolean = false
    private var isStart: Boolean = true

    private var timer: Timer? = null
    private val handler = Handler()


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

        var textTitle = findViewById<TextView>(R.id.text_title)
        var textTimer = findViewById<TextView>(R.id.text_timer)
        var startButton = findViewById<FloatingActionButton>(R.id.start_button)
        var stopButton = findViewById<FloatingActionButton>(R.id.stop_button)
        val listener = StartStopListener()

        textTitle.setText(R.string.work_time)
        textTimer.text = formatTime()
        startButton.setOnClickListener(listener)
        stopButton.setOnClickListener(listener)
    }

    private inner class StartStopListener : View.OnClickListener {
        override fun onClick(view: View?) {
            when (view!!.id) {
                R.id.start_button -> {
                    if (isStart) {
                        isStart = false
                        startTimer()
                    } else {
                        null
                    }
                }
                R.id.stop_button -> {
                    if (!isStart) {
                        resetTimer()
                    } else {
                        null
                    }
                }
            }
        }
    }


    private fun startTimer() {
        timer = Timer()
        timer!!.schedule(object : TimerTask() {
            override fun run() {
                handler.post {
                    if (current == 0) {
                        isStart = true
                        timer!!.cancel()
                        if (isWorkTime) {
                            current = workTime
                            isWorkTime = false
                            text_title.setText(R.string.work_time)
                            text_timer.text = formatTime()
                        } else {
                            current = brakeTime
                            isWorkTime = true
                            text_title.setText(R.string.brake_time)
                            text_timer.text = formatTime()
                        }
                    } else {
                        current--
                        text_timer.text = formatTime()
                    }
                }
            }
        }, 0, 1000)
    }

    private fun formatTime(): String {
        val minutes = (current / 60).toString().padStart(2, '0')
        val seconds = (current % 60).toString().padStart(2, '0')
        return "$minutes:$seconds"
    }

    private fun resetTimer() {
        isStart = true
        timer!!.cancel()
        current = workTime
        isWorkTime = false
        text_title.setText(R.string.work_time)
        text_timer.text = formatTime()
    }

}
```  

- activity_main.xml

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

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/stop_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.676"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.397"
        app:srcCompat="@mipmap/stop" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/start_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.295"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.397"
        app:srcCompat="@mipmap/play" />

    <TextView
        android:id="@+id/text_timer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="50sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.167" />

    <TextView
        android:id="@+id/text_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        app:layout_constraintBottom_toTopOf="@+id/text_timer"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
  
- strings.xml

```xml
<resources>
    <string name="app_name">androidPomodoro</string>
    <string name="work_time">ワークタイム</string>
    <string name="brake_time">ブレイクタイム</string>
</resources>
```

### flutter
[GitHub](https://github.com/ibuki-iida/flutter-pomodoro)

- main.dart

```dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Pomodoro',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _brakeTime = 300; // 5分
  int _workTime = 1500; // 25分
  int _current = 1500;
  bool _isWorkTime = false;
  bool _isStart = true;
  String _titleText = "ワークタイム";
  Timer _timer;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Container(
          child: Column(
            children: [
              // タイトル
              Container(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    SizedBox(
                      height: 20,
                    ),
                    Text(
                      _titleText,
                      style: TextStyle(fontSize: 30),
                    ),
                  ],
                ),
              ),
              SizedBox(
                height: 50,
              ),

              // 時間表示
              Container(
                child: Text(
                  formatTime(),
                  style: TextStyle(fontSize: 50),
                ),
              ),
              SizedBox(
                height: 50,
              ),

              // ボタン
              Container(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    FloatingActionButton(
                      child: Icon(Icons.play_arrow),
                      onPressed: !_isStart
                          ? null
                          : () {
                              _isStart = false;
                              startTimer();
                            },
                    ),
                    SizedBox(width: 50),
                    FloatingActionButton(
                      child: Icon(Icons.stop),
                      onPressed: _isStart
                          ? null
                          : () {
                              setState(() {
                                resetTimer();
                              });
                            },
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void startTimer() {
    _timer = Timer.periodic(const Duration(seconds: 1), onTimer);
  }

  void onTimer(Timer timer) {
    if (_current == 0) {
      setState(() {
        _isStart = true;
        _timer.cancel();
        if (_isWorkTime) {
          _current = _workTime;
          _isWorkTime = false;
          _titleText = "ワークタイム";
        } else {
          _current = _brakeTime;
          _isWorkTime = true;
          _titleText = "ブレイクタイム";
        }
      });
    } else {
      setState(() {
        _current--;
      });
    }
  }

  String formatTime() {
    final minutes = (_current / 60).floor().toString().padLeft(2, '0');
    final seconds = (_current % 60).floor().toString().padLeft(2, '0');
    return "$minutes:$seconds";
  }

  void resetTimer() {
    setState(() {
      _isStart = true;
      _timer.cancel();
      _current = _workTime;
      _isWorkTime = false;
      _titleText = "ワークタイム";
    });
  }
}

```

## flutterから見たAndroid（ネイティブ）開発

### 良い所

- UIでレイアウトを組める
- レイアウトを組んでる途中にプレビューが見れる
- ネイティブの機能を全て使うことが出来る

### 悪い所

- 画面の向きを変えたら画面が再生成されてしまうなど、とにかくライフサイクルの管理が大変
- レイアウトのファイルとActivityのファイルが完全に分かれているので、textやbuttonなどの宣言が面倒

## Android（ネイティブ）開発から見たflutterでの開発

### 良い所

- hot reloadが早く、素早く動作確認が可能に
- widgetが豊富
- ライフサイクルの管理をflutter側が管理してくれている
- レイアウトを別ファイルで管理する必要がない
- MaterialDesignに沿ったデザインが作り易い
  - Themeを変更すれば全ての画面で変更される為、カスタマイズも簡単

### 悪い所

- ネイティブの機能を全て使うことが出来ない
  - 主要な機能はflutterで使うためのpluginも用意されている
- レイアウトを組んでいる最中にプレビューが見れない
  - hot reloadが爆速の為、そこまで気にならない

## 最後に

自分はこの比較を通してflutterがより好きになりました。
学習コストもAndroidの方が圧倒的に高いなと感じました。
今回は簡単なアプリでの比較だった為、flutterの方が優勢になってしまった可能性はありますが、今後flutterが成長して、ネイティブと変わりないくらいになってくれたら良いなと思いました。