📱

[趣味開発]タスク管理通知アプリの開発 アプリ機能追加編

2024/10/14に公開

[趣味開発]タスク管理通知アプリの開発 アプリ機能追加編

はじめに

https://zenn.dev/articles/96df3f5a12c944/
の続き

この記事で、最低限タスク管理ツールとして動くところまで作りたいと思います。
いつも通り生成AI(ChatGPT)に全力でヘルプしてもらいながら進めています。

ウィジェットの編集

以下の修正を実施

  • タスクの右側に完了ボタンを配置
  • すべてチェックボタンは使わなかったので削除

レイアウト(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" />
    </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)
                }
            }
        }
    }

    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)

            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()
        }
    }
}

修正後

アプリメイン画面の機能追加

  • 時間を設定し、その時間が来たらリセット
  • メイン画面でタスクの一覧が表示されるように
  • タスク一覧からタスクの削除が行えるように

MainActivity.kt

メイン画面に関する部分はMainActivity.ktでコーディング

package com.example.taskreminder

import android.app.AlarmManager
import android.app.PendingIntent
import android.app.TimePickerDialog
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
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
import java.util.Calendar

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)) }
    var selectedHour by remember { mutableStateOf(0) }
    var selectedMinute by remember { mutableStateOf(0) }

    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Top
    ) {
        // 時間設定ボタン
        Button(
            onClick = {
                TimePickerDialog(
                    context,
                    { _, hour, minute ->
                        selectedHour = hour
                        selectedMinute = minute

                        // アラームを設定して、その時間に全タスクのチェックをクリアする
                        setClearAllTasksAlarm(context, hour, minute)
                    },
                    selectedHour,
                    selectedMinute,
                    true
                ).show()
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("時間を設定")
        }

        Spacer(modifier = Modifier.height(16.dp))

        // 水平線を追加
        Divider(
            color = Color.Gray,
            thickness = 1.dp,
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 8.dp)
        )

        // タスクの入力フィールドと追加ボタンを横に配置
        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()
}

fun setClearAllTasksAlarm(context: Context, hour: Int, minute: Int) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, TaskReminderWidget::class.java).apply {
        action = "CLEAR_ALL_TASKS"
    }
    val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

    val calendar = Calendar.getInstance().apply {
        timeInMillis = System.currentTimeMillis()
        set(Calendar.HOUR_OF_DAY, hour)
        set(Calendar.MINUTE, minute)
        set(Calendar.SECOND, 0)

        // 設定した時間が過去なら翌日に設定
        if (before(Calendar.getInstance())) {
            add(Calendar.DAY_OF_MONTH, 1)
        }
    }

    alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, pendingIntent)
}

@Preview(showBackground = true)
@Composable
fun TaskInputScreenPreview() {
    val context = LocalContext.current
    val sharedPreferences = context.getSharedPreferences("TaskReminderPrefs", Context.MODE_PRIVATE)
    TaskReminderTheme {
        TaskInputScreen(sharedPreferences = sharedPreferences)
    }
}

修正後

実機テスト編に続く

Discussion