diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt
new file mode 100644
index 0000000000..cd81944c05
--- /dev/null
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/MobiusExceptionLogger.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 - present Instructure, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package com.instructure.student.mobius.common
+
+import com.crashlytics.android.Crashlytics
+import com.instructure.student.BuildConfig
+import com.spotify.mobius.First
+import com.spotify.mobius.Next
+import com.spotify.mobius.android.AndroidLogger
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+/**
+ * Intercepts exceptions in the mobius loop's update and init operations and logs them to Crashlytics.
+ * For debug builds the exception will be logged locally and then thrown.
+ */
+class MobiusExceptionLogger : AndroidLogger("Mobius") {
+ override fun afterUpdate(model: MODEL, event: EVENT, result: Next) {
+ if (BuildConfig.DEBUG) super.afterUpdate(model, event, result)
+ }
+
+ override fun afterInit(model: MODEL, result: First) {
+ if (BuildConfig.DEBUG) super.afterInit(model, result)
+ }
+
+ override fun beforeInit(model: MODEL) {
+ if (BuildConfig.DEBUG) super.beforeInit(model)
+ }
+
+ override fun beforeUpdate(model: MODEL, event: EVENT) {
+ if (BuildConfig.DEBUG) super.beforeUpdate(model, event)
+ }
+
+ override fun exceptionDuringInit(model: MODEL, exception: Throwable) {
+ if (BuildConfig.DEBUG) {
+ super.exceptionDuringInit(model, exception)
+ // Must throw as a separate message, otherwise Mobius might consume the exception
+ GlobalScope.launch(Dispatchers.Main) { throw exception }
+ } else {
+ Crashlytics.logException(exception)
+ }
+ }
+
+ override fun exceptionDuringUpdate(model: MODEL, event: EVENT, exception: Throwable) {
+ if (BuildConfig.DEBUG) {
+ super.exceptionDuringUpdate(model, event, exception)
+ // Must throw as a separate message, otherwise Mobius might consume the exception
+ GlobalScope.launch(Dispatchers.Main) { throw exception }
+ } else {
+ Crashlytics.logException(exception)
+ }
+ }
+}
diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
index 9122b2927c..b2cdc4541c 100644
--- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
+++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt
@@ -22,14 +22,19 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
+import com.crashlytics.android.Crashlytics
import com.instructure.interactions.FragmentInteractions
import com.instructure.interactions.Navigation
+import com.instructure.student.BuildConfig
import com.instructure.student.mobius.common.*
import com.spotify.mobius.*
import com.spotify.mobius.android.MobiusAndroid
import com.spotify.mobius.android.runners.MainThreadWorkRunner
import com.spotify.mobius.functions.Consumer
import kotlinx.android.extensions.LayoutContainer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
abstract class MobiusFragment, VIEW_STATE> : Fragment(), FragmentInteractions {
var overrideInitModel: MODEL? = null
@@ -89,6 +94,7 @@ abstract class MobiusFragment : CoroutineConnection(
protected var consumer = ConsumerQueueWrapper()
+ private val connectionWrapper by lazy { ExceptionLoggerConnectionWrapper(this) }
+
override fun connect(output: Consumer): Connection {
consumer.attach(output)
- return this
+ return connectionWrapper
}
override fun dispose() {
@@ -213,4 +221,28 @@ abstract class EffectHandler : CoroutineConnection(
fun logEvent(eventName: String) {
// TODO
}
+
+ /**
+ * Catches exceptions in the [accept] function of the provided [connection] and logs them to Crashlytics.
+ * For debug builds the exception will be logged locally and then thrown.
+ */
+ private class ExceptionLoggerConnectionWrapper(private val connection: Connection) : Connection {
+ override fun accept(value: T) {
+ try {
+ connection.accept(value)
+ } catch (e: Throwable) {
+ if (BuildConfig.DEBUG) {
+ e.printStackTrace()
+ // Must throw as a separate message, otherwise Mobius might silently consume the exception
+ GlobalScope.launch(Dispatchers.Main) { throw e }
+ } else {
+ Crashlytics.logException(e)
+ }
+ }
+ }
+
+ override fun dispose() {
+ connection.dispose()
+ }
+ }
}