【注目記事】
・SEの転職体験談 | 受託開発から自社製品開発へ ・SIer勤務のSEがパソナキャリアに相談してみた ・SIer勤務のSEがマイナビエージェントに相談してみた
アラームの作成・編集・削除ができるシンプルな目覚ましアプリを作成しました。



ソースはGitHubにアップロードしました。
なぜ作ったか
目覚まし時計アプリを作る前に、Androidアプリ開発の入門書を読みました。
入門書の内容を身に着けるため実際にアプリをつくろうと思い、簡単そうな目覚まし時計アプリを作ることにしました。
どれくらいかかったか
Androidのプロジェクトを作成してから、簡単なテストを終えるまでに約20時間かかりました。
もう少し早くできると予想していましたが、入門書に記載がない機能を多く使う必要があり、調査に時間がかかりました。
有給消化中で時間があったので、1週間ほどで出来上がりました。
処理内容
一覧画面
データベースに登録されているアラームの一覧を表示します。
一覧のアラームを押下すると入力画面に遷移し、編集や削除ができます。
また、下部のフローティングアクションボタンを押下すると、入力画面でアラームを新規作成できます。
<?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"> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/toolbarConfirm" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
android:background="?attr/colorPrimary" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:popupTheme="@style/AppTheme.PopupOverlay" | |
app:title="@string/title_confirm" /> | |
<androidx.constraintlayout.widget.ConstraintLayout | |
android:id="@+id/bk" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:paddingLeft="16dp" | |
android:paddingTop="16dp" | |
android:paddingRight="16dp" | |
android:paddingBottom="16dp" | |
app:layout_constraintTop_toBottomOf="@+id/toolbarConfirm"> | |
<androidx.recyclerview.widget.RecyclerView | |
android:id="@+id/rv" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> | |
<com.google.android.material.floatingactionbutton.FloatingActionButton | |
android:id="@+id/fbtn" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:layout_marginEnd="16dp" | |
android:layout_marginBottom="16dp" | |
android:clickable="true" | |
android:focusable="true" | |
app:backgroundTint="#388E3C" | |
app:fabSize="normal" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:srcCompat="@drawable/ic_add_black_24dp" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
package com.example.alarmclock.activity; | |
import androidx.annotation.Nullable; | |
import androidx.appcompat.app.AppCompatActivity; | |
import androidx.recyclerview.widget.LinearLayoutManager; | |
import androidx.recyclerview.widget.RecyclerView; | |
import android.content.Intent; | |
import android.database.Cursor; | |
import android.database.sqlite.SQLiteDatabase; | |
import android.os.Bundle; | |
import android.view.View; | |
import com.example.alarmclock.util.DatabaseHelper; | |
import com.example.alarmclock.listcomponent.ListAdapter; | |
import com.example.alarmclock.listcomponent.ListItem; | |
import com.example.alarmclock.R; | |
import com.google.android.material.floatingactionbutton.FloatingActionButton; | |
import java.util.ArrayList; | |
public class ConfirmationActivity extends AppCompatActivity { | |
private DatabaseHelper helper = null; | |
final static public int NEW_REQ_CODE = 1; | |
final static public int EDIT_REQ_CODE = 2; | |
RecyclerView rv = null; | |
RecyclerView.Adapter adapter = null; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.confirmation_main); | |
// アラームのデータを取得 | |
ArrayList<ListItem> data = this.loadAlarms(); | |
// RecyclerViewに設定 | |
this.setRV(data); | |
// フローティングアクションボタンの設定 | |
FloatingActionButton fbt = findViewById(R.id.fbtn); | |
fbt.setOnClickListener( | |
new View.OnClickListener(){ | |
@Override | |
public void onClick(View view){ | |
Intent i = new Intent(ConfirmationActivity.this, InputActivity.class); | |
i.putExtra(getString(R.string.request_code),NEW_REQ_CODE); | |
startActivityForResult(i,NEW_REQ_CODE); | |
} | |
}); | |
} | |
@Override | |
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { | |
super.onActivityResult(requestCode, resultCode, data); | |
// リクエストコードと結果コードをチェック | |
if(requestCode == RESULT_CANCELED){ | |
// 何もしない | |
}else if((requestCode == NEW_REQ_CODE || requestCode == EDIT_REQ_CODE) && resultCode == RESULT_OK){ | |
ArrayList<ListItem> dataAlarms = this.loadAlarms(); | |
this.updateRV(dataAlarms); | |
} | |
} | |
private ArrayList<ListItem> loadAlarms(){ | |
helper = DatabaseHelper.getInstance(this); | |
ArrayList<ListItem> data = new ArrayList<>(); | |
try(SQLiteDatabase db = helper.getReadableDatabase()) { | |
String[] cols ={"alarmid","name","alarttime"}; | |
Cursor cs = db.query("alarms",cols,null,null, | |
null,null,"alarmid",null); | |
boolean eol = cs.moveToFirst(); | |
while (eol){ | |
ListItem item = new ListItem(); | |
item.setAlarmID(cs.getInt(0)); | |
item.setAlarmName(cs.getString(1)); | |
item.setTime(cs.getString(2)); | |
data.add(item); | |
eol = cs.moveToNext(); | |
} | |
} | |
return data; | |
} | |
private void setRV(ArrayList<ListItem> data){ | |
rv = (RecyclerView)findViewById(R.id.rv); | |
rv.setHasFixedSize(true); | |
LinearLayoutManager manager = new LinearLayoutManager(this); | |
manager.setOrientation(LinearLayoutManager.VERTICAL); | |
rv.setLayoutManager(manager); | |
adapter = new ListAdapter(data); | |
rv.setAdapter(adapter); | |
} | |
private void updateRV(ArrayList<ListItem> data){ | |
adapter = new ListAdapter(data); | |
rv.setAdapter(adapter); | |
// rv.swapAdapter(adapter,false); | |
} | |
} |
入力画面
アラームの新規作成・編集・削除を行います。
AlarmManagerへの操作と同時に、DBへの操作も実施します。
どちらかの操作が失敗したときに、コールバックするような対応はできていません。
(今気がつきましたが、保存時と削除時で操作の順番が異なっていますね…)
<?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=".activity.InputActivity"> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/toolbarInput" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:background="?attr/colorPrimary" | |
android:minHeight="?attr/actionBarSize" | |
android:theme="?attr/actionBarTheme" | |
app:layout_constraintTop_toTopOf="parent" | |
tools:layout_editor_absoluteX="17dp" | |
tools:layout_editor_absoluteY="-3dp" /> | |
<TimePicker | |
android:id="@+id/time_picker" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:headerBackground="@color/design_default_color_primary" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toBottomOf="@+id/toolbarInput" /> | |
<EditText | |
android:id="@+id/editAlarmText" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:ems="10" | |
android:hint="@string/alarm_name" | |
android:inputType="textPersonName" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toBottomOf="@+id/time_picker" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
package com.example.alarmclock.activity; | |
import androidx.appcompat.app.AppCompatActivity; | |
import androidx.appcompat.widget.Toolbar; | |
import android.app.AlarmManager; | |
import android.app.PendingIntent; | |
import android.content.ContentValues; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.database.Cursor; | |
import android.database.sqlite.SQLiteDatabase; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.os.Bundle; | |
import android.text.format.Formatter; | |
import android.view.Menu; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.widget.EditText; | |
import android.widget.TimePicker; | |
import android.widget.Toast; | |
import com.example.alarmclock.listcomponent.ListItem; | |
import com.example.alarmclock.receiver.AlarmReceiver; | |
import com.example.alarmclock.R; | |
import com.example.alarmclock.util.DatabaseHelper; | |
import com.example.alarmclock.util.Util; | |
import java.text.Format; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Calendar; | |
public class InputActivity extends AppCompatActivity { | |
private AlarmManager alarmMgr = null; | |
private PendingIntent alarmIntent = null; | |
private TimePicker timePicker = null; | |
private DatabaseHelper helper = null; | |
private EditText editAlarmName = null; | |
private int reqCode = -1; | |
Intent retnIntent = null; | |
int currentApiVersion = Build.VERSION.SDK_INT; | |
private static int MENU_DELETE_ID = 2; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_input); | |
// タイムピッカーを取得 | |
timePicker = findViewById(R.id.time_picker); | |
// アラーム名を取得 | |
editAlarmName = findViewById(R.id.editAlarmText); | |
// ヘルパーの準備 | |
helper = DatabaseHelper.getInstance(InputActivity.this); | |
// キャンセルボタンの設定 | |
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbarInput); | |
toolbar.setNavigationIcon(R.drawable.ic_close_black_24dp); | |
toolbar.setNavigationOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
Intent i = new Intent(); | |
setResult(RESULT_CANCELED, i); | |
finish(); | |
} | |
}); | |
// 保存、削除ボタンの設定 | |
toolbar.inflateMenu(R.menu.edit_menu); | |
// 新規 or 編集を取得 | |
Intent intent = getIntent(); | |
reqCode = intent.getIntExtra(getString(R.string.request_code),-1); | |
int alarmID = -1; | |
if(reqCode == ConfirmationActivity.EDIT_REQ_CODE){ | |
// 編集モード | |
// 削除ボタンを追加する | |
Menu menu = toolbar.getMenu(); | |
menu.add(0,MENU_DELETE_ID,2,R.string.action_delete).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); | |
// 編集前のデータを取得 | |
alarmID = intent.getIntExtra(getString(R.string.alarm_id),-1); | |
ListItem item = Util.getAlarmsByID(alarmID, helper); | |
editAlarmName.setText(item.getAlarmName()); | |
if (currentApiVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { | |
timePicker.setHour(Integer.parseInt(item.getHour())); | |
timePicker.setMinute(Integer.parseInt(item.getMinitsu())); | |
} else { | |
timePicker.setCurrentHour(Integer.parseInt(item.getHour())); | |
timePicker.setCurrentMinute(Integer.parseInt(item.getMinitsu())); | |
} | |
}else { | |
// 新規 | |
// 何もしない | |
} | |
final int alarmIDForMenu = alarmID; | |
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { | |
@Override | |
public boolean onMenuItemClick(MenuItem item) { | |
int id = item.getItemId(); | |
if (id == R.id.action_save) { | |
// アラーム設定処理 | |
// 設定時刻を取得 | |
int hour; | |
int minute; | |
if (currentApiVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { | |
hour = timePicker.getHour(); | |
minute = timePicker.getMinute(); | |
} else { | |
hour = timePicker.getCurrentHour(); | |
minute = timePicker.getCurrentMinute(); | |
} | |
Calendar calendar = Calendar.getInstance(); | |
calendar.setTimeInMillis(System.currentTimeMillis()); | |
calendar.set(Calendar.HOUR_OF_DAY, hour); | |
calendar.set(Calendar.MINUTE, minute); | |
// データ登録 or 更新 | |
// TODO DB登録後にエラーが発生した場合の考慮が必要 | |
int requestCode = -1; | |
// アラーム名の設定 | |
String alarmName = editAlarmName.getText().toString(); | |
if(alarmName.equals("")){ | |
alarmName = "無題"; | |
} | |
// 時刻登録の準備 | |
String alarmTime = String.format("%02d", hour) + ":" | |
+ String.format("%02d", minute); | |
if(reqCode == ConfirmationActivity.EDIT_REQ_CODE){ | |
// 編集 | |
// データ更新処理 | |
requestCode = alarmIDForMenu; | |
try(SQLiteDatabase db = helper.getWritableDatabase()){ | |
ContentValues cv = new ContentValues(); | |
cv.put("name",alarmName); | |
cv.put("alarttime", alarmTime); | |
String[] params = {String.valueOf(requestCode)}; | |
db.update("alarms",cv,"alarmid = ?",params); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
} | |
}else { | |
// 新規 | |
// データ登録 | |
try(SQLiteDatabase db = helper.getWritableDatabase()){ | |
ContentValues cv = new ContentValues(); | |
cv.put("name",alarmName); | |
cv.put("alarttime", alarmTime); | |
requestCode = (int)db.insert("alarms",null,cv); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
} | |
} | |
// 参考 https://qiita.com/hiroaki-dev/items/e3149e0be5bfa52d6a51 | |
// アラームの設定 | |
ListItem listItem = new ListItem(); | |
listItem.setAlarmID(requestCode); | |
listItem.setAlarmName(alarmName); | |
listItem.setTime(alarmTime); | |
Util.setAlarm(InputActivity.this, listItem); | |
Toast.makeText(InputActivity.this,R.string.alarm_save_msg,Toast.LENGTH_SHORT).show(); | |
}else if(id == MENU_DELETE_ID){ | |
// 編集 | |
// アラーム削除処理 | |
Intent receiveIntent = getIntent(); | |
int alarmID = receiveIntent.getIntExtra(getString(R.string.alarm_id),-1); | |
alarmMgr = (AlarmManager)InputActivity.this.getSystemService(Context.ALARM_SERVICE); | |
Intent sendIntent = new Intent(InputActivity.this, AlarmReceiver.class); | |
alarmIntent = PendingIntent.getBroadcast(InputActivity.this, alarmID, sendIntent, 0); | |
alarmMgr.cancel(alarmIntent); | |
// データ削除処理 | |
try(SQLiteDatabase db = helper.getWritableDatabase()){ | |
String[] params = {String.valueOf(alarmID)}; | |
db.delete("alarms","alarmid = ?",params); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
} | |
Toast.makeText(InputActivity.this,R.string.alarm_delete_msg,Toast.LENGTH_SHORT).show(); | |
} | |
retnIntent = new Intent(); | |
setResult(RESULT_OK, retnIntent); | |
finish(); | |
return true; | |
} | |
}); | |
} | |
} |
アラームレシーバー
アラームを設定した時刻に呼び出されるレシーバーです。
翌日も同じ時刻にアラームを起動するために、AlarmManagerに設定を行い、アラーム画面を起動します。
package com.example.alarmclock.receiver; | |
import android.content.BroadcastReceiver; | |
import android.content.Context; | |
import android.content.Intent; | |
import com.example.alarmclock.activity.WakeUpActivity; | |
import com.example.alarmclock.listcomponent.ListItem; | |
import com.example.alarmclock.util.DatabaseHelper; | |
import com.example.alarmclock.util.Util; | |
public class AlarmReceiver extends BroadcastReceiver { | |
private static final String TAG = AlarmReceiver.class.getSimpleName(); | |
@Override | |
public void onReceive(Context context, Intent intent) { | |
// アラームを再登録 | |
// 参考 PutExtraは使用できない | |
// https://stackoverflow.com/questions/12506391/retrieve-requestcode-from-alarm-broadcastreceiver | |
// リクエストコードに紐づくデータを取得 | |
String requestCode = intent.getData().toString(); | |
DatabaseHelper helper = DatabaseHelper.getInstance(context); | |
ListItem item = Util.getAlarmsByID(Integer.parseInt(requestCode), helper); | |
// アラームを設定 | |
Util.setAlarm(context, item); | |
Intent startActivityIntent = new Intent(context, WakeUpActivity.class); | |
startActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
context.startActivity(startActivityIntent); | |
} | |
} |
アラーム画面
アラームが起動した時に表示される画面です。
ボタンを押下すると、アラームが停止します。
<?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=".activity.WakeUpActivity"> | |
<androidx.appcompat.widget.Toolbar | |
android:id="@+id/toolbarWakeUp" | |
android:layout_width="match_parent" | |
android:layout_height="?attr/actionBarSize" | |
android:background="?attr/colorPrimary" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:popupTheme="@style/AppTheme.PopupOverlay" | |
app:title="@string/title_wake_up" /> | |
<Button | |
android:id="@+id/stopBtn" | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:layout_marginTop="16dp" | |
android:text="停止" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toBottomOf="@+id/toolbarWakeUp" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> | |
© 2019 GitHub, Inc. |
package com.example.alarmclock.activity; | |
import android.content.Intent; | |
import android.os.Bundle; | |
import com.example.alarmclock.R; | |
import com.example.alarmclock.service.SoundService; | |
import com.google.android.material.floatingactionbutton.FloatingActionButton; | |
import com.google.android.material.snackbar.Snackbar; | |
import androidx.appcompat.app.AppCompatActivity; | |
import androidx.appcompat.widget.Toolbar; | |
import android.view.View; | |
import android.view.WindowManager; | |
import android.widget.Button; | |
// 参考 https://github.com/hiroaki-dev/AlarmSample/blob/master/app/src/main/java/me/hiroaki/alarmsample/PlaySoundActivity.java | |
public class WakeUpActivity extends AppCompatActivity { | |
Button stopBtn; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | | |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | | |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | | |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); | |
setContentView(R.layout.activity_wake_up); | |
Toolbar toolbar = findViewById(R.id.toolbarWakeUp); | |
setSupportActionBar(toolbar); | |
startService(new Intent(this, SoundService.class)); | |
stopBtn = (Button) findViewById(R.id.stopBtn); | |
stopBtn.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View view) { | |
stopService(new Intent(WakeUpActivity.this, SoundService.class)); | |
} | |
}); | |
} | |
} |
改善点
Androidでどのようなアーキテクチャを選べば良いのかを分からずに実装したので、クラス分けなどが適当になってしまいました。
次回はAndroidのアーキテクチャを学んだ上で、実装しようと思います。
Android Architecture Components 初級 ( MVVM + LiveData + Coroutines 編 )
2019/12/1 追記
Fragmentを使ってタブレット対応をしました。
目覚ましアプリのサンプル(タブレット対応版) | GitHub
【おすすめ記事】
コメント失礼いたします。
コードを拝見させていただいたのですが、
Androidのバージョンをお教えいただいてもよろしいでしょうか?
返信が遅くなり申し訳ありません。
targetSdkVersion は29です。