Androidアプリ開発サンプルコード | 目覚まし時計

【注目記事】

・SEの転職体験談 | 受託開発から自社製品開発へ
・SIer勤務のSEがパソナキャリアに相談してみた
・SIer勤務のSEがマイナビエージェントに相談してみた

アラームの作成・編集・削除ができるシンプルな目覚ましアプリを作成しました。

ソースはGitHubにアップロードしました。

目覚ましアプリのサンプル | GitHub

なぜ作ったか

目覚まし時計アプリを作る前に、Androidアプリ開発の入門書を読みました。

『はじめてのAndroidアプリ開発 第3版』レビュー

入門書の内容を身に着けるため実際にアプリをつくろうと思い、簡単そうな目覚まし時計アプリを作ることにしました。

どれくらいかかったか

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


【おすすめ記事】


2 Comments

KKK

コメント失礼いたします。
コードを拝見させていただいたのですが、
Androidのバージョンをお教えいただいてもよろしいでしょうか?

返信する

999mura へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です