Skip to content

Commit

Permalink
feat: エラーが発生した時にその操作をリトライできるようにした
Browse files Browse the repository at this point in the history
  • Loading branch information
pantasystem committed Jan 11, 2024
1 parent b9f6473 commit a683a54
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package net.pantasystem.milktea.app_store.handler
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import net.pantasystem.milktea.common_android.resource.StringSource
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -20,9 +22,29 @@ class UserActionAppGlobalErrorStore @Inject constructor() {
extraBufferCapacity = 999,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)

private val _userActionFlow = MutableSharedFlow<UserActionAppGlobalErrorAction>(
extraBufferCapacity = 999,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val errorFlow = _errorFlow.asSharedFlow()
fun dispatch(e: AppGlobalError) {
val userActionFlow = _userActionFlow.asSharedFlow()
fun dispatch(e: AppGlobalError): String {
_errorFlow.tryEmit(e)
return e.id
}

fun onAction(action: UserActionAppGlobalErrorAction) {
_userActionFlow.tryEmit(action)
}

suspend fun awaitUserAction(id: String) = userActionFlow.first {
id == it.errorId
}

suspend fun dispatchAndAwaitUserAction(e: AppGlobalError, type: UserActionAppGlobalErrorAction.Type): Boolean {
val id = dispatch(e.copy(retryable = true))
return awaitUserAction(id).type == type
}
}

Expand All @@ -31,12 +53,15 @@ class UserActionAppGlobalErrorStore @Inject constructor() {
* @param level エラーのレベルです。
* @param message エラーのメッセージです。
* @param throwable エラーの発生元となった例外です。
* @param retryable エラーが発生した場合にリトライ可能かどうかを示します。
*/
data class AppGlobalError(
val tag: String,
val level: ErrorLevel,
val message: StringSource,
val throwable: Throwable? = null,
val retryable: Boolean = false,
val id: String = UUID.randomUUID().toString(),
) {
enum class ErrorLevel {
/**
Expand All @@ -54,4 +79,17 @@ data class AppGlobalError(
*/
Error,
}
}
/**
* ユーザーがエラーに対して行なったアクションを表すsealed interface
*/
data class UserActionAppGlobalErrorAction(
val errorId: String,
val tag: String,
val type: Type,
) {
enum class Type {
Dismiss, Retry, Cancel
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package net.pantasystem.milktea.common_android_ui.error

import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
Expand All @@ -10,10 +11,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.flowWithLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import net.pantasystem.milktea.app_store.handler.AppGlobalError
import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorAction
import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorStore
import net.pantasystem.milktea.common.APIError
import net.pantasystem.milktea.common_android.resource.getString
Expand Down Expand Up @@ -66,6 +69,9 @@ class UserActionAppGlobalErrorListener @Inject constructor(
title = appGlobalError.message.getString(context),
message = body,
detail = appGlobalError.throwable?.toString(),
id = appGlobalError.id,
retryable = appGlobalError.retryable,
tag = appGlobalError.tag,
)
fragmentManager.findFragmentByTag("error_dialog")?.let {
fragmentManager.beginTransaction().remove(it).commit()
Expand All @@ -80,19 +86,31 @@ class UserActionAppGlobalErrorListener @Inject constructor(
}
}

@AndroidEntryPoint

class UserActionAppGlobalErrorDialog : DialogFragment() {

@Inject
internal lateinit var userActionAppGlobalErrorStore: UserActionAppGlobalErrorStore

companion object {
const val EXTRA_TITLE = "title"
const val EXTRA_MESSAGE = "message"
const val EXTRA_DETAIL = "detail"
const val EXTRA_ERROR_ID = "error_id"
const val EXTRA_RETRYABLE = "retryable"
const val EXTRA_TAG = "error_tag"

fun newInstance(title: String?, message: String, detail: String?): UserActionAppGlobalErrorDialog {
fun newInstance(id: String, tag: String, title: String?, message: String, detail: String?, retryable: Boolean): UserActionAppGlobalErrorDialog {
return UserActionAppGlobalErrorDialog().apply {
arguments = Bundle().apply {
putString(EXTRA_TITLE, title)
putString(EXTRA_MESSAGE, message)
// 1MB未満にする
putString(EXTRA_DETAIL, detail?.take(1024 * 1024 - 1))
putBoolean(EXTRA_RETRYABLE, retryable)
putString(EXTRA_ERROR_ID, id)
putString(EXTRA_TAG, tag)
}
}
}
Expand All @@ -114,18 +132,59 @@ class UserActionAppGlobalErrorDialog : DialogFragment() {
title = null,
message = detail,
detail = null,
id = arguments?.getString(EXTRA_ERROR_ID) ?: "",
retryable = arguments?.getBoolean(EXTRA_RETRYABLE) ?: false,
tag = arguments?.getString(EXTRA_TAG) ?: "",
)
d.show(parentFragmentManager, "error_dialog")
}
}
}
if (arguments?.getBoolean(EXTRA_RETRYABLE) == true) {
dialog.setPositiveButton(R.string.retry) { _, _ ->
userActionAppGlobalErrorStore.onAction(
UserActionAppGlobalErrorAction(
errorId = arguments?.getString(EXTRA_ERROR_ID) ?: "",
tag = arguments?.getString(EXTRA_TAG) ?: "",
type = UserActionAppGlobalErrorAction.Type.Retry,
)
)
}
dialog.setNegativeButton(R.string.cancel) { _, _ ->
dismiss()
}
} else {
dialog.setPositiveButton(android.R.string.ok) { _, _ ->
dismiss()
}
}
}
.setMessage(
arguments?.getString(EXTRA_MESSAGE) ?: ""
)
.setPositiveButton(android.R.string.ok) { _, _ ->
dismiss()
}

.create()
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
userActionAppGlobalErrorStore.onAction(
UserActionAppGlobalErrorAction(
errorId = arguments?.getString(EXTRA_ERROR_ID) ?: "",
tag = arguments?.getString(EXTRA_TAG) ?: "",
type = UserActionAppGlobalErrorAction.Type.Dismiss,
)
)
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
userActionAppGlobalErrorStore.onAction(
UserActionAppGlobalErrorAction(
errorId = arguments?.getString(EXTRA_ERROR_ID) ?: "",
tag = arguments?.getString(EXTRA_TAG) ?: "",
type = UserActionAppGlobalErrorAction.Type.Cancel,
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import net.pantasystem.milktea.app_store.account.AccountStore
import net.pantasystem.milktea.app_store.drive.DriveDirectoryPagingStore
import net.pantasystem.milktea.app_store.drive.FilePropertyPagingStore
import net.pantasystem.milktea.app_store.handler.AppGlobalError
import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorAction
import net.pantasystem.milktea.app_store.handler.UserActionAppGlobalErrorStore
import net.pantasystem.milktea.common.Logger
import net.pantasystem.milktea.common.PageableState
Expand Down Expand Up @@ -246,14 +247,18 @@ class DriveViewModel @Inject constructor(
filePropertyRepository.toggleNsfw(id)
} catch (e: Exception) {
logger.info("nsfwの更新に失敗しました", e = e)
userActionAppGlobalErrorHandler.dispatch(
AppGlobalError(
"DriveViewModel.toggleNsfw",
AppGlobalError.ErrorLevel.Error,
StringSource("Nsfw update failed"),
e,
)
)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.toggleNsfw",
AppGlobalError.ErrorLevel.Error,
StringSource("Nsfw update failed"),
e,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
toggleNsfw(id)
}

}
}
}
Expand All @@ -263,14 +268,17 @@ class DriveViewModel @Inject constructor(
viewModelScope.launch {
filePropertyRepository.delete(id).onFailure { e ->
logger.info("ファイルの削除に失敗しました", e = e)
userActionAppGlobalErrorHandler.dispatch(
AppGlobalError(
"DriveViewModel.deleteFile",
AppGlobalError.ErrorLevel.Error,
StringSource("File delete failed"),
e,
)
)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.deleteFile",
AppGlobalError.ErrorLevel.Error,
StringSource("File delete failed"),
e,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
deleteFile(id)
}
}
}
}
Expand All @@ -283,14 +291,17 @@ class DriveViewModel @Inject constructor(
.update(comment = newCaption)
).onFailure {
logger.info("キャプションの更新に失敗しました。", e = it)
userActionAppGlobalErrorHandler.dispatch(
AppGlobalError(
"DriveViewModel.updateCaption",
AppGlobalError.ErrorLevel.Error,
StringSource("Caption update failed"),
it,
)
)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.updateCaption",
AppGlobalError.ErrorLevel.Error,
StringSource("Caption update failed"),
it,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
updateCaption(id, newCaption)
}
}
}
}
Expand All @@ -302,14 +313,17 @@ class DriveViewModel @Inject constructor(
.update(name = name)
).onFailure {
logger.error("update file name failed", it)
userActionAppGlobalErrorHandler.dispatch(
AppGlobalError(
"DriveViewModel.updateFileName",
AppGlobalError.ErrorLevel.Error,
StringSource("File name update failed"),
it,
)
)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.updateFileName",
AppGlobalError.ErrorLevel.Error,
StringSource("File name update failed"),
it,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
updateFileName(id, name)
}
}
}
}
Expand Down Expand Up @@ -354,14 +368,17 @@ class DriveViewModel @Inject constructor(
filePagingStore.onCreated(e.id)
} catch (e: Exception) {
logger.info("ファイルアップロードに失敗した")
userActionAppGlobalErrorHandler.dispatch(
AppGlobalError(
"DriveViewModel.uploadFile",
AppGlobalError.ErrorLevel.Error,
StringSource("File upload failed"),
e,
)
)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.uploadFile",
AppGlobalError.ErrorLevel.Error,
StringSource("File upload failed"),
e,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
uploadFile(file)
}
}
}
}
Expand Down Expand Up @@ -391,6 +408,17 @@ class DriveViewModel @Inject constructor(
)
).onFailure {
logger.error("error create folder", it)
if (userActionAppGlobalErrorHandler.dispatchAndAwaitUserAction(
AppGlobalError(
"DriveViewModel.createDirectory",
AppGlobalError.ErrorLevel.Error,
StringSource("Folder create failed"),
it,
),
UserActionAppGlobalErrorAction.Type.Retry
)) {
createDirectory(folderName)
}
}.onSuccess {
directoryPagingStore.onCreated(it)
}
Expand Down

0 comments on commit a683a54

Please sign in to comment.