📱

[趣味開発]タスク管理通知アプリの開発 実機確認編

2024/10/16に公開

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

はじめに

https://zenn.dev/articles/87c7169aca9f5c/
の続き

今回の記事では、ウィジェットを少し手直しして、実際にアプリで使い始めたいと思います。

手直し

修正前

・時間リセット機能の動作が安定しないので、ウィジェット上からリセットするように修正します。

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