Skip to content

Commit

Permalink
feat: ユーザの操作に関するエラーをアプリ全体で処理できるようにした
Browse files Browse the repository at this point in the history
  • Loading branch information
pantasystem committed Jan 9, 2024
1 parent b0f6265 commit 11d76ef
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import net.pantasystem.milktea.common_android.resource.StringSource
import javax.inject.Inject
import javax.inject.Singleton

/**
* アプリ全体で共通としてユーザの操作によって発生したエラーをハンドリングするクラスです。
Expand All @@ -13,9 +14,10 @@ import javax.inject.Inject
* 複数のレイヤーから呼び出してしまうと、重複して同じエラーを報告してしまう可能性が考えられるため、ViewModelから呼び出すことを前提としています。
* また古い実装の場合このクラスを経由しないケースがあるため、このクラスに必ずエラーが報告されないケースもあるため、エラーの発生元を確認するようにしてください。
*/
class UserActionAppGlobalErrorHandler @Inject constructor() {
@Singleton
class UserActionAppGlobalErrorStore @Inject constructor() {
private val _errorFlow = MutableSharedFlow<AppGlobalError>(
extraBufferCapacity = 100,
extraBufferCapacity = 999,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val errorFlow = _errorFlow.asSharedFlow()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package net.pantasystem.milktea.common_android_ui.error

import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.flowWithLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.UserActionAppGlobalErrorStore
import net.pantasystem.milktea.common.APIError
import net.pantasystem.milktea.common_android.resource.getString
import net.pantasystem.milktea.common_android_ui.APIErrorStringConverter
import net.pantasystem.milktea.common_android_ui.R
import net.pantasystem.milktea.model.account.UnauthorizedException
import java.io.IOException
import javax.inject.Inject

class UserActionAppGlobalErrorListener @Inject constructor(
private val userActionAppGlobalErrorStore: UserActionAppGlobalErrorStore,
@ApplicationContext private val context: Context,
) {

operator fun invoke(
lifecycle: Lifecycle,
fragmentManager: FragmentManager,
) {
userActionAppGlobalErrorStore.errorFlow.onEach { appGlobalError ->
when (appGlobalError.level) {
AppGlobalError.ErrorLevel.Info -> {}
AppGlobalError.ErrorLevel.Warning -> {
// toastで表示する
Toast.makeText(
context,
appGlobalError.message.getString(context),
Toast.LENGTH_SHORT
)
.show()
}

AppGlobalError.ErrorLevel.Error -> {
val title = when (val error = appGlobalError.throwable) {
is IOException -> {
context.getString(R.string.network_error)
}

is APIError -> {
APIErrorStringConverter()(error).getString(context)
}

is UnauthorizedException -> {
context.getString(R.string.unauthorized_error)
}

else -> "Unknown error"

}
val dialogInstance = UserActionAppGlobalErrorDialog.newInstance(
title = title,
message = appGlobalError.message.getString(context)
)
fragmentManager.findFragmentByTag("error_dialog")?.let {
fragmentManager.beginTransaction().remove(it).commit()
}
dialogInstance.show(fragmentManager, "error_dialog")
}
}
}.flowWithLifecycle(
lifecycle,
Lifecycle.State.RESUMED,
).launchIn(lifecycle.coroutineScope)
}
}

class UserActionAppGlobalErrorDialog : DialogFragment() {
companion object {
const val EXTRA_TITLE = "title"
const val EXTRA_MESSAGE = "message"

fun newInstance(title: String, message: String): UserActionAppGlobalErrorDialog {
return UserActionAppGlobalErrorDialog().apply {
arguments = Bundle().apply {
putString(EXTRA_TITLE, title)
putString(EXTRA_MESSAGE, message)
}
}
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(
arguments?.getString(EXTRA_TITLE) ?: ""
)
.setMessage(
arguments?.getString(EXTRA_MESSAGE) ?: ""
)
.setPositiveButton(android.R.string.ok) { _, _ ->
dismiss()
}
.create()
}
}
1 change: 1 addition & 0 deletions modules/features/drive/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
implementation project(path: ':modules:common_compose')
implementation project(path: ':modules:common_navigation')
implementation project(path: ':modules:common_android')
implementation project(path: ':modules:common_android_ui')
testImplementation libs.junit
androidTestImplementation libs.androidx.test.ext.junit
androidTestImplementation libs.androidx.test.espresso.core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
import net.pantasystem.milktea.common.ui.ApplyTheme
import net.pantasystem.milktea.common_android.platform.PermissionUtil
import net.pantasystem.milktea.common_android_ui.error.UserActionAppGlobalErrorListener
import net.pantasystem.milktea.common_compose.MilkteaStyleConfigApplyAndTheme
import net.pantasystem.milktea.common_navigation.DriveNavigation
import net.pantasystem.milktea.common_navigation.DriveNavigationArgs
Expand Down Expand Up @@ -53,6 +54,9 @@ class DriveActivity : AppCompatActivity() {
@Inject
internal lateinit var configRepository: LocalConfigRepository

@Inject
internal lateinit var userActionAPpGlobalErrorListener: UserActionAppGlobalErrorListener

@OptIn(
ExperimentalPagerApi::class,
ExperimentalMaterialApi::class,
Expand All @@ -62,6 +66,10 @@ class DriveActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setTheme.invoke()

userActionAPpGlobalErrorListener(
lifecycle = lifecycle,
fragmentManager = supportFragmentManager
)


setContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import kotlinx.coroutines.launch
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.UserActionAppGlobalErrorStore
import net.pantasystem.milktea.common.Logger
import net.pantasystem.milktea.common.PageableState
import net.pantasystem.milktea.common.convert
import net.pantasystem.milktea.common.mapCancellableCatching
import net.pantasystem.milktea.common.runCancellableCatching
import net.pantasystem.milktea.common_android.resource.StringSource
import net.pantasystem.milktea.common_navigation.EXTRA_ACCOUNT_ID
import net.pantasystem.milktea.common_navigation.EXTRA_INT_SELECTABLE_FILE_MAX_SIZE
import net.pantasystem.milktea.model.account.Account
Expand Down Expand Up @@ -53,6 +56,7 @@ class DriveViewModel @Inject constructor(
private val filePagingStore: FilePropertyPagingStore,
private val filePropertyRepository: DriveFileRepository,
private val uriToAppFileUseCase: UriToAppFileUseCase,
private val userActionAppGlobalErrorHandler: UserActionAppGlobalErrorStore,
loggerFactory: Logger.Factory,
buildDriveUiState: DriveUiStateBuilder,
) : ViewModel() {
Expand Down Expand Up @@ -242,6 +246,14 @@ 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,
)
)
}
}
}
Expand All @@ -251,6 +263,14 @@ 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,
)
)
}
}
}
Expand All @@ -263,6 +283,14 @@ 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,
)
)
}
}
}
Expand All @@ -274,6 +302,14 @@ 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,
)
)
}
}
}
Expand Down Expand Up @@ -318,6 +354,14 @@ 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,
)
)
}
}
}
Expand Down

0 comments on commit 11d76ef

Please sign in to comment.