はじめに
モバイルエンジニア歴2年の若手です。
この2年でAndroid開発とflutterでの開発の両方に携わることが出来たので、それを通して感じたことを書いていきます。
今回は比較にあたって、ポモドーロタイマーのアプリを作成いたしました。
仕様
- 初回起動時に25分(作業)のタイマーがセットされている
- 画面には開始ボタンと停止ボタンがある
- 以下のサイクルでタイマーが使用できる
- スタートボタンを押すことでタイマーが開始される
- タイマーの残りの時間が0秒になると5分(休憩)のタイマーがセットされる
- スタートボタンを押すと休憩時間が開始される
- タイマーの残りの時間が0秒になると25分(作業)のタイマーがセットされる
- 項番1に戻る
- タイマー作動中に停止ボタンを押すと25分の作業時間にリセットされる
アプリ画面
- Androidで開発した画面
ソースコード
Android
- MainActivity.kt
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 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
<resources>
<string name="app_name">androidPomodoro</string>
<string name="work_time">ワークタイム</string>
<string name="brake_time">ブレイクタイム</string>
</resources>
flutter
- main.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が成長して、ネイティブと変わりないくらいになってくれたら良いなと思いました。