Coder Social home page Coder Social logo

icerockdev / moko-errors Goto Github PK

View Code? Open in Web Editor NEW
52.0 8.0 6.0 291 KB

Automated exceptions handler for mobile (android & ios) Kotlin Multiplatform development.

Home Page: https://moko.icerock.dev/

License: Apache License 2.0

Kotlin 100.00%
android ios kotlin kotlin-native kotlin-multiplatform moko coroutines error-handling

moko-errors's Introduction

moko-errors

GitHub license Download kotlin-version

Mobile Kotlin errors

This is a Kotlin MultiPlatform library that provides automatic exception handling and automatic error displaying to a screen.

Table of Contents

Features

  • ExceptionHandler implements safe code execution and automatic exception display using ErrorPresenter.
  • ExceptionMappersStorage singleton object, storage that stores a set of exception converters to error classes required for ErrorPresenter objects.
  • ErrorPresenter classes implements a strategy for displaying exceptions in a user-friendly form on the platforms. Converts the exception class to an error object to display. There are several ErrorPresenter implementations: AlertErrorPresenter - displays errors text in alert dialogs, ToastErrorPresenter - displays errors text in toasts for Android and in alert dialog for iOS, SnackBarErrorPresenter - displays errors text in snackbar for Android and in alert dialog for iOS, SelectorErrorPresenter - for selecting error presenter by some custom condition.

Requirements

  • Gradle version 6.8+
  • Android API 16+
  • iOS version 11.0+

Installation

root build.gradle

allprojects {
    repositories {
        mavenCentral()
    }
}

project build.gradle

dependencies {
    commonMainApi("dev.icerock.moko:errors:0.6.0")
}

Usage

ExceptionMappersStorage

Registration of simple custom exceptions mapper in the singleton storage:

ExceptionMappersStorage
    .register<IllegalArgumentException, StringDesc> {   // Will map all IllegalArgumentException instances to StringDesc
        "Illegal argument was passed!".desc()
    }
    .register<HttpException, Int> {                     // Will map all HttpException instances to Int
        it.code
    }

Registration of custom exception mapper with condition:

ExceptionMappersStorage.condition<StringDesc>(              // Registers exception mapper Throwable -> StringDesc
    condition = { it is CustomException && it.code == 10 }, // Condition that maps Throwable -> Boolean
    mapper = { "Custom error happened!".desc() }            // Mapper for Throwable that matches to the condition
)

For every error type you should to set fallback (default) value using method setFallbackValue
(except StringDesc class which already has default value).

ExceptionMappersStorage
    .setFallbackValue<Int>(520) // Sets for Int error type default value as 520

// Creates new mapper that for any unregistered exception will return the fallback value - 520
val throwableToIntMapper: (Throwable) -> Int = ExceptionMappersStorage.throwableMapper()

Using factory method throwableMapper you can create exception mappers automaticlly:

val throwableToIntMapper: (Throwable) -> Int = ExceptionMappersStorage.throwableMapper()

If a default value is not found when creating a mapper using factory method throwableMapper, an exception will be thrown FallbackValueNotFoundException

The registration can be done in the form of an endless chain:

ExceptionMappersStorage
    .condition<StringDesc>(
        condition = { it is CustomException && it.code == 10 },
        mapper = { "Custom error happened!".desc() }
    )
    .register<IllegalArgumentException, StringDesc> {
        "Illegal argument was passed!".desc()
    }
    .register<HttpException, Int> {
        it.code
    }
    .setFallbackValue<Int>(520)

ExceptionHandler

E.g. declare ExceptionHandler property in some ViewModel class:

class SimpleViewModel(
    val exceptionHandler: ExceptionHandler
) : ViewModel() {
    // ...
}

Bind ExceptionHandler in the platform code.

On Android in an Activity of Fragment:

viewModel.exceptionHandler.bind(
    lifecycleOwner = this,
    activity = this
)

On iOS in a ViewController:

viewModel.exceptionHandler.bind(viewController: self)

Creating instances of ExceptionHandler class which uses (Throwable) -> StringDesc mappers:

ExceptionHandler<StringDesc>(
    errorPresenter = errorsPresenterInstance,                    // Concrete ErrorPresenter implementation
    exceptionMapper = ExceptionMappersStorage.throwableMapper(), // Create mapper (Throwable) -> StringDesc from ExceptionMappersStorage
    onCatch = {                                                  // Optional global catcher
        println("Got exception: $it")                            // E.g. here we can log all exceptions that are handled by ExceptionHandler
    }
)

And use it to safe requests in ViewModel:

fun onSendRequest() {
    viewModelScope.launch {
        exceptionHandler.handle {
            serverRequest()     // Some dangerous code that can throw an exception
        }.finally {             // Optional finally block
            // Some code        
        }.execute()             // Starts code execution in `handle` lambda
    }
}

Also you can add some custom catch handlers for ExceptionHandler that work as a catch in the try/catch operator:

fun onSendRequest() {
    viewModelScope.launch {
        exceptionHandler.handle {
            serverRequest()
        }.catch<IllegalArgumentException> {     // Specifying exception class
            // Some custom handler code
            false                               // true - cancels ErrorPresenter; false - allows execution of ErrorsPresenter
        }.execute()                             // Starts code execution in `handle` lambda
    }
}

ErrorPresenter

There are ErrorPresenter interface implementations:

  • AlertErrorPresenter - displays errors text in alert dialogs;
  • ToastErrorPresenter - displays errors text in toasts for Android (for iOS shows alert dialog);
  • SnackBarErrorPresenter - displays errors text in snackbar for Android (for iOS shows alert dialog);
  • SelectorErrorPresenter - for selecting error presenter by some custom condition.

You need to pass some ErrorPresenter to ErrorHandler instance. E.g. creation of error presenters in common code:

val alertErrorPresenter = AlertErrorPresenter(
    alertTitle = "Error".desc(),
    positiveButtonText = "OK".desc()
)
val toastErrorPresenter = ToastErrorPresenter(
    duration = ToastDuration.LONG
)

SelectorErrorPresenter - special presenter that select some error presenter by custom condition lambda which should return some ErrorPresenter to be used for showing errors:

val selectorErrorPresenter = SelectorErrorPresenter { throwable ->
    when (throwable) {
        is CustomException -> alertErrorPresenter
        else -> toastErrorPresenter
    }
}

And pass some ErrorPresenter to ErrorHandler:

val exceptionHandler = ExceptionHandler(
    errorPresenter = selectorErrorPresenter,
    exceptionMapper = ExceptionMappersStorage.throwableMapper()
)

Samples

Please see more examples in the sample directory.

Set Up Locally

Contributing

All development (both new features and bug fixes) is performed in the develop branch. This way master always contains the sources of the most recently released version. Please send PRs with bug fixes to the develop branch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly in master.

The develop branch is pushed to master on release.

For more details on contributing please see the contributing guide.

License

Copyright 2020 IceRock MAG Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

moko-errors's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

moko-errors's Issues

Sometimes handeled errors are not displayed on iOS

It seems that there is a problem with ARC on iOS that kills the anonymous object of the listener:

actual class ExceptionHandlerBinderImpl<T : Any> actual constructor(
    private val errorPresenter: ErrorPresenter<T>,
    private val eventsDispatcher: EventsDispatcher<ErrorEventListener<T>>
) : ExceptionHandlerBinder {
    override fun bind(viewController: UIViewController) {
        eventsDispatcher.listener = object : ErrorEventListener<T> { // anonymous object
            override fun showError(throwable: Throwable, data: T) {
                errorPresenter.show(throwable, viewController, data)
            }
        }
    }
}

Add errors-permissions addition with DeniedAlwaysException processing

when we got from moko-perimissions DeniedAlwaysException we should show have ability to show user alert "Please enable push notifications in settings, we use it for ***" with buttons "Go to settings" and "Close". "Go to settings" should route to settings of our application in iphone settings app

Remove requirement of call execute

// now
    viewModelScope.launch {
        exceptionHandler.handle {
            serverRequest()
        }.catch<IllegalArgumentException> {     // Specifying a specific exception class
            // Some custom handler code
            false                               // true - cancels ErrorPresenter; false - allows execution of ErrorsPresenter
        }.execute()                             // Starts code execution in `handle` lambda
    }
    // suggestion
    viewModelScope.launch {
        isLoading = true
        val result = exceptionHandler {
        	action {
                serverRequest()
	        }
    	    catch<IllegalAccessException> { exc ->
        	    // Some custom handler code
            	false                               // true - cancels ErrorPresenter; false - allows execution of ErrorsPresenter
	        }
    	    catch<IllegalArgumentException> { exc ->
        	    // Some custom handler code
	            false                               // true - cancels ErrorPresenter; false - allows execution of ErrorsPresenter
	        }
    	    finally {
        	    isLoading = false
	        }
	    }
    }

Update moko-resources dependency without cinterop-pluralizedString

In moko-resources 0.21.0 release cinterop was removed as unused. But Kotlin/Native have own list of dependencies inside klib. All libraries, that depends on moko-resources, have inside own manifest file in klib dependency to dev.icerock.moko:resources-cinterop-pluralizedString. So gradle download new version of moko-resources (0.21.0) and try to compile project, but Kotlin/Native see own dependencies list and see that moko-mvvm depends on dev.icerock.moko:resources-cinterop-pluralizedString but that library not exist anymore and gradle not download it.
As result we see:

error: could not find "dev.icerock.moko:resources-cinterop-pluralizedString" in [/Users/amikhailov/.konan/kotlin-native-prebuilt-macos-aarch64-1.8.10/bin, /Users/amikhailov/.konan/klib, /Users/amikhailov/.konan/kotlin-native-prebuilt-macos-aarch64-1.8.10/klib/common, /Users/amikhailov/.konan/kotlin-native-prebuilt-macos-aarch64-1.8.10/klib/platform/ios_arm64]

need to publish new version with updated moko resources

Execute the error presenter manually

It's will be useful possibility to manually execute error presenter manually for external exceptions, something like this:

fun handleException(exception: Exception) {
    excehptionHandler.showError(cause = exception) // manually runs error presesnter
}

catch in ExceptionHandler require class equality

If we use this code:

exceptionHandler.handle { 
    request()
}.catch<Exception> { 
    false
}.execute()

catch blog will not be execute if throw some of subclasses of Exception. it's imprudent behavior, because try-catch allow to catch all subclasses of some type. We should improve it to same logic.


to implement this i think we should remove KClass map and use lambda with conditions.

inline fun <reified E : Throwable> catch(noinline catcher: (E) -> Boolean)

become

inline fun <reified E : Throwable> catch(
    noinline catcher: (E) -> Boolean
): ExceptionHandlerContext<R> {
    return catch(condition = { it is E }, catcher = catcher)
}

Add errors-test module with test utilities

When i try to test ViewModel that use ExceptionHandler i need to create test version of ExceptionHandler.
here draft:

import dev.icerock.moko.errors.handler.ExceptionHandler
import dev.icerock.moko.errors.handler.ExceptionMapper

expect class TestsExceptionHandler<T : Any>(
    exceptionMapper: ExceptionMapper<T>
) : ExceptionHandler {
    val catchedExceptions: List<Throwable>
}

android:

import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LifecycleOwner
import dev.icerock.moko.errors.handler.ExceptionHandler
import dev.icerock.moko.errors.handler.ExceptionHandlerContext
import dev.icerock.moko.errors.handler.ExceptionMapper
import dev.icerock.moko.mvvm.dispatcher.EventsDispatcher

actual class TestsExceptionHandler<T : Any> actual constructor(
    private val exceptionMapper: ExceptionMapper<T>
) : ExceptionHandler {

    private val _catchedExceptions = mutableListOf<Throwable>()
    actual val catchedExceptions: List<Throwable> = _catchedExceptions

    override fun bind(lifecycleOwner: LifecycleOwner, activity: FragmentActivity) {
        TODO("Not yet implemented")
    }

    override fun <R> handle(block: suspend () -> R): ExceptionHandlerContext<R> {
        return ExceptionHandlerContext.invoke(
            exceptionMapper = exceptionMapper,
            eventsDispatcher = EventsDispatcher(),
            onCatch = _catchedExceptions::add,
            block = block
        )
    }
}

ios:

import dev.icerock.moko.errors.handler.ExceptionHandler
import dev.icerock.moko.errors.handler.ExceptionHandlerContext
import dev.icerock.moko.errors.handler.ExceptionMapper
import dev.icerock.moko.mvvm.dispatcher.EventsDispatcher
import platform.UIKit.UIViewController

actual class TestsExceptionHandler<T : Any> actual constructor(
    private val exceptionMapper: ExceptionMapper<T>
) : ExceptionHandler {
    private val _catchedExceptions = mutableListOf<Throwable>()
    actual val catchedExceptions: List<Throwable> = _catchedExceptions

    override fun <R> handle(block: suspend () -> R): ExceptionHandlerContext<R> {
        return ExceptionHandlerContext.invoke(
            exceptionMapper = exceptionMapper,
            eventsDispatcher = EventsDispatcher(),
            onCatch = _catchedExceptions::add,
            block = block
        )
    }

    override fun bind(viewController: UIViewController) {
        TODO("Not yet implemented")
    }
}

Add fallback factory

Now you can only specify a fallback value:

ExceptionMappersStorage.setFallbackValue(MR.strings.common_unknownError.desc())

Another option is to add a fallback factory:

ExceptionMappersStorage.setFallbackFactory { error : Throwable ->
    if (error.message.isNullOrBlank()) {
        MR.strings.common_unknownError.desc()
    } else {
        error.message!!.desc()
    }
}

Add reference to ExceptionHandler in HandlerResult

It will be useful if HandlerResult will contains reference to parent ExceptionHandler that allow to create useful extension like next to build chains:

val lastExecutionResult = exceptionHandler.handle {
    repository.request1()
}.execute()
.next {
    repository.request2()
}.catch<IllegalStateException> {
    false
}.execute()

Retry action support in error presenters

In some cases we need to allow Retry action when show error to user. for example when we submit some changes to server. now user can only close error message. need to improve it

Add fast getting of mapped value

inline fun <reified E : Throwable, reified T : Any> E.mapThrowable(): T {
    return ExceptionMappersStorage.throwableMapper<E, T>(T::class)(this)
}

usage:

val myException = IllegalArgumentException("fail!")
val message: StringDesc = myException.mapThrowable()

Catching Throwable's

In the current version of the library in ExceptionHandlerContextImpl to catch exceptions is used catch (e: Throwable) construction. Therefore, all possible exceptions are handled when the method execute is called. This method of exception handling can lead to undefined behavior, because e.g. OOM or CancellationException will be handled. So, we need to find a solution for such cases - do exceptions rethrow or something else.

The EventsDispatcher requires moko-mvvm to be included.

Hello! I faced the following problem:
The EventsDispatcher requires moko.mvvm to be included.
Error:
"Cannot access class 'dev.icerock.moko.mvvm.dispatcher.EventsDispatcher'. Check your module's classpath for missing or conflicting dependencies."

I do not want to include moko-mvvm in my project, only moko-errors.
The solution would be move EventDispatcher to another lib to achive moko-errors independs from moko-mvvm.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.