📱
[趣味開発]タスク管理通知アプリの開発 実機確認編
[趣味開発]タスク管理通知アプリの開発 アプリ機能追加編
はじめに
の続き
今回の記事では、ウィジェットを少し手直しして、実際にアプリで使い始めたいと思います。
手直し
修正前
・時間リセット機能の動作が安定しないので、ウィジェット上からリセットするように修正します。
MainActivity.kt
package com.example.taskreminder
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.taskreminder.ui.theme.TaskReminderTheme
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TaskReminderTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TaskInputScreen(
modifier = Modifier.padding(innerPadding),
sharedPreferences = getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
)
}
}
}
}
}
@Composable
fun TaskInputScreen(modifier: Modifier = Modifier, sharedPreferences: SharedPreferences) {
var task by remember { mutableStateOf("") }
val context = LocalContext.current
val taskList = remember { mutableStateOf(getTaskList(sharedPreferences)) }
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Top
) {
// タスクの入力フィールドと追加ボタンを横に配置
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = task,
onValueChange = { task = it },
label = { Text("タスクを入力") },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (task.isNotEmpty()) {
// 既存のタスクリストを取得し、タスクを追加
val updatedTaskList = taskList.value.toMutableList()
updatedTaskList.add(task)
// 更新されたタスクリストを保存
saveTaskList(sharedPreferences, updatedTaskList)
taskList.value = updatedTaskList
// ウィジェットを更新
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetComponent = ComponentName(context, TaskReminderWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
for (appWidgetId in appWidgetIds) {
TaskReminderWidget.updateAppWidget(context, appWidgetManager, appWidgetId)
}
task = ""
}
},
modifier = Modifier.wrapContentWidth()
) {
Text("追加")
}
}
Spacer(modifier = Modifier.height(16.dp))
// タスクのリストを表示
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(taskList.value) { taskItem ->
TaskItem(
task = taskItem,
onDeleteClick = {
// タスクを削除する処理
val updatedTaskList = taskList.value.toMutableList()
updatedTaskList.remove(taskItem)
saveTaskList(sharedPreferences, updatedTaskList)
taskList.value = updatedTaskList
// ウィジェットを更新
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetComponent = ComponentName(context, TaskReminderWidget::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
for (appWidgetId in appWidgetIds) {
TaskReminderWidget.updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
)
}
}
}
}
@Composable
fun TaskItem(task: String, onDeleteClick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(8.dp)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = task,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = { onDeleteClick() }) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "タスクを削除"
)
}
}
}
}
fun getTaskList(sharedPreferences: SharedPreferences): MutableList<String> {
val gson = Gson()
val json = sharedPreferences.getString("taskList", null)
val type = object : TypeToken<MutableList<String>>() {}.type
return if (json != null) {
gson.fromJson(json, type)
} else {
mutableListOf()
}
}
fun saveTaskList(sharedPreferences: SharedPreferences, taskList: MutableList<String>) {
val gson = Gson()
val json = gson.toJson(taskList)
val editor = sharedPreferences.edit()
editor.putString("taskList", json)
editor.apply()
}
@Preview(showBackground = true)
@Composable
fun TaskInputScreenPreview() {
val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
TaskReminderTheme {
TaskInputScreen(sharedPreferences = sharedPreferences)
}
}
task_reminder_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:background="#CCFFFFFF">
<!-- 上部にボタンを配置するためのLinearLayout -->
<LinearLayout
android:id="@+id/topButtonsLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:orientation="horizontal">
<!-- + ボタン -->
<ImageButton
android:id="@+id/addTaskButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_input_add"
android:contentDescription="Add Task" />
<!-- クリアボタン -->
<Button
android:id="@+id/clearAllTasksButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="リセット"
android:contentDescription="Clear All Tasks" />
</LinearLayout>
<!-- タスクコンテナ -->
<LinearLayout
android:id="@+id/taskContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/topButtonsLayout"
android:layout_marginTop="16dp" />
</RelativeLayout>
TaskReminderWidget.kt
package com.example.taskreminder
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Paint
import android.widget.RemoteViews
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class TaskReminderWidget : AppWidgetProvider() {
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == "ADD_TASK") {
val addTaskIntent = Intent(context, MainActivity::class.java)
addTaskIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(addTaskIntent)
} else if (intent.action == "TASK_COMPLETED") {
val taskIndex = intent.getIntExtra("taskIndex", -1)
if (taskIndex != -1) {
val sharedPreferences = context.getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
val completedTasks = getCompletedTasks(sharedPreferences)
if (completedTasks.contains(taskIndex)) {
completedTasks.remove(taskIndex) // 取り消し線を解除
} else {
completedTasks.add(taskIndex) // 取り消し線を追加
}
saveCompletedTasks(sharedPreferences, completedTasks)
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context.packageName, javaClass.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
} else if (intent.action == "CLEAR_ALL_TASKS") {
val sharedPreferences = context.getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
saveCompletedTasks(sharedPreferences, mutableSetOf()) // 完了済みタスクリストをクリア
val appWidgetManager = AppWidgetManager.getInstance(context)
val thisAppWidget = ComponentName(context.packageName, javaClass.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {}
override fun onDisabled(context: Context) {}
companion object {
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.task_reminder_widget)
// SharedPreferencesからタスクリストを取得
val sharedPreferences = context.getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
val taskList = getTaskList(sharedPreferences)
val completedTasks = getCompletedTasks(sharedPreferences)
// タスクコンテナをクリアして、チェックボックスとタスクを追加
views.removeAllViews(R.id.taskContainer)
taskList.forEachIndexed { index, task ->
// 各タスクごとにレイアウトを設定
val taskView = RemoteViews(context.packageName, R.layout.task_item)
taskView.setTextViewText(R.id.taskNameText, task)
// タスクが完了済みの場合は取り消し線を追加
if (completedTasks.contains(index)) {
taskView.setInt(R.id.taskNameText, "setPaintFlags", Paint.STRIKE_THRU_TEXT_FLAG)
taskView.setTextViewText(R.id.taskCompleteButton, "戻す")
} else {
taskView.setInt(R.id.taskNameText, "setPaintFlags", 0)
taskView.setTextViewText(R.id.taskCompleteButton, "完了")
}
// 完了/戻すボタンのPendingIntentを設定
val intent = Intent(context, TaskReminderWidget::class.java)
intent.action = "TASK_COMPLETED"
intent.putExtra("taskIndex", index)
val pendingIntent = PendingIntent.getBroadcast(context, index, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
taskView.setOnClickPendingIntent(R.id.taskCompleteButton, pendingIntent)
// タスクコンテナに追加
views.addView(R.id.taskContainer, taskView)
}
// + ボタンのインテントを設定
val addTaskIntent = Intent(context, TaskReminderWidget::class.java)
addTaskIntent.action = "ADD_TASK"
val addTaskPendingIntent = PendingIntent.getBroadcast(context, 0, addTaskIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.addTaskButton, addTaskPendingIntent)
// クリアボタンのインテントを設定
val clearAllTasksIntent = Intent(context, TaskReminderWidget::class.java)
clearAllTasksIntent.action = "CLEAR_ALL_TASKS"
val clearAllTasksPendingIntent = PendingIntent.getBroadcast(context, 0, clearAllTasksIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
views.setOnClickPendingIntent(R.id.clearAllTasksButton, clearAllTasksPendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
// タスクリストを取得
fun getTaskList(sharedPreferences: SharedPreferences): MutableList<String> {
val gson = Gson()
val json = sharedPreferences.getString("taskList", null)
val type = object : TypeToken<MutableList<String>>() {}.type
return if (json != null) {
gson.fromJson<MutableList<String>>(json, type)
} else {
mutableListOf()
}
}
// 完了済みタスクリストを取得
fun getCompletedTasks(sharedPreferences: SharedPreferences): MutableSet<Int> {
return sharedPreferences.getStringSet("completedTasks", mutableSetOf())?.map { it.toInt() }?.toMutableSet() ?: mutableSetOf()
}
// 完了済みタスクリストを保存
fun saveCompletedTasks(sharedPreferences: SharedPreferences, completedTasks: MutableSet<Int>) {
val editor = sharedPreferences.edit()
editor.putStringSet("completedTasks", completedTasks.map { it.toString() }.toSet())
editor.apply()
}
}
}
実機での確認
ビルド
・Android Studioでプロジェクトをビルド:
メニューから「Build」 > 「Build Bundle(s) / APK(s)」 > 「Build APK(s)」を選択
・Google Driveなど経由でAPKをAndroidに転送
・Android上でインストール
実機確認
しばらく、実機で使ってみたいと思います
Discussion