Skip to content

Commit

Permalink
Merge pull request #62 from arkivanov/back-handler-2
Browse files Browse the repository at this point in the history
Added back-handler module, deprecated back-pressed module
  • Loading branch information
arkivanov committed Aug 2, 2022
2 parents a05c83d + bc8a6d9 commit 94d779c
Show file tree
Hide file tree
Showing 21 changed files with 659 additions and 26 deletions.
1 change: 1 addition & 0 deletions back-handler/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
40 changes: 40 additions & 0 deletions back-handler/api/android/back-handler.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
public final class com/arkivanov/essenty/backhandler/AndroidBackHandlerKt {
public static final fun BackHandler (Landroidx/activity/OnBackPressedDispatcher;)Lcom/arkivanov/essenty/backhandler/BackHandler;
public static final fun backHandler (Landroidx/activity/OnBackPressedDispatcherOwner;)Lcom/arkivanov/essenty/backhandler/BackHandler;
}

public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler {
public abstract fun back ()Z
public abstract fun isEnabled ()Z
}

public final class com/arkivanov/essenty/backhandler/BackDispatcherKt {
public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher;
}

public abstract interface class com/arkivanov/essenty/backhandler/BackHandler {
public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackHandler$Callback;)V
public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackHandler$Callback;)V
}

public final class com/arkivanov/essenty/backhandler/BackHandler$Callback {
public fun <init> (ZLkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V
public final fun getCallback ()Lkotlin/jvm/functions/Function0;
public final fun isEnabled ()Z
public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V
public final fun setEnabled (Z)V
}

public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner {
public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler;
}

public final class com/arkivanov/essenty/backhandler/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}

28 changes: 28 additions & 0 deletions back-handler/api/jvm/back-handler.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler {
public abstract fun back ()Z
public abstract fun isEnabled ()Z
}

public final class com/arkivanov/essenty/backhandler/BackDispatcherKt {
public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher;
}

public abstract interface class com/arkivanov/essenty/backhandler/BackHandler {
public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackHandler$Callback;)V
public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackHandler$Callback;)V
}

public final class com/arkivanov/essenty/backhandler/BackHandler$Callback {
public fun <init> (ZLkotlin/jvm/functions/Function0;)V
public synthetic fun <init> (ZLkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V
public final fun getCallback ()Lkotlin/jvm/functions/Function0;
public final fun isEnabled ()Z
public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V
public final fun setEnabled (Z)V
}

public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner {
public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler;
}

29 changes: 29 additions & 0 deletions back-handler/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import com.arkivanov.gradle.bundle
import com.arkivanov.gradle.setupBinaryCompatibilityValidator
import com.arkivanov.gradle.setupMultiplatform
import com.arkivanov.gradle.setupPublication
import com.arkivanov.gradle.setupSourceSets

plugins {
id("kotlin-multiplatform")
id("com.android.library")
id("com.arkivanov.gradle.setup")
}

setupMultiplatform()
setupPublication()
setupBinaryCompatibilityValidator()

kotlin {
setupSourceSets {
val android by bundle()

common.main.dependencies {
implementation(project(":utils-internal"))
}

android.main.dependencies {
implementation(deps.androidx.activity.activityKtx)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.arkivanov.essenty.backhandler

import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.addCallback

/**
* Creates a new instance of [BackHandler] and attaches it to the provided AndroidX [OnBackPressedDispatcher].
*/
fun BackHandler(onBackPressedDispatcher: OnBackPressedDispatcher): BackHandler =
AndroidBackHandler(onBackPressedDispatcher)

/**
* Creates a new instance of [BackHandler] and attaches it to the AndroidX [OnBackPressedDispatcher].
*/
fun OnBackPressedDispatcherOwner.backHandler(): BackHandler =
AndroidBackHandler(onBackPressedDispatcher)

internal class AndroidBackHandler(
delegate: OnBackPressedDispatcher,
) : AbstractBackHandler() {

private val delegateCallback = delegate.addCallback(enabled = false) { callCallbacks() }

override fun onEnabledChanged(isEnabled: Boolean) {
delegateCallback.isEnabled = isEnabled
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.arkivanov.essenty.backhandler

import androidx.activity.OnBackPressedDispatcher
import com.arkivanov.essenty.backhandler.BackHandler.Callback
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue

@Suppress("TestFunctionName")
class AndroidBackHandlerTest {

private val dispatcher = OnBackPressedDispatcher()
private val handler = AndroidBackHandler(dispatcher)

@Test
fun WHEN_created_THEN_hasEnabledCallbacks_false() {
assertFalse(dispatcher.hasEnabledCallbacks())
}

@Test
fun WHEN_enabled_callback_registered_THEN_hasEnabledCallbacks_true() {
handler.register(callback(isEnabled = true))

assertTrue(dispatcher.hasEnabledCallbacks())
}

@Test
fun WHEN_disabled_callback_registered_THEN_hasEnabledCallbacks_false() {
handler.register(callback(isEnabled = false))

assertFalse(dispatcher.hasEnabledCallbacks())
}

@Test
fun WHEN_multiple_callbacks_registered_and_one_enabled_THEN_hasEnabledCallbacks_true() {
handler.register(callback(isEnabled = false))
handler.register(callback(isEnabled = true))
handler.register(callback(isEnabled = false))

assertTrue(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_multiple_disabled_callbacks_WHEN_one_callback_enabled_THEN_hasEnabledCallbacks_true() {
val callback2 = callback(isEnabled = false)
handler.register(callback(isEnabled = false))
handler.register(callback2)
handler.register(callback(isEnabled = false))

callback2.isEnabled = true

assertTrue(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_disabled_except_one_THEN_hasEnabledCallbacks_true() {
val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true))
callbacks.forEach(handler::register)

callbacks.drop(1).forEach { it.isEnabled = false }

assertTrue(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_disabled_THEN_hasEnabledCallbacks_false() {
val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true))
callbacks.forEach(handler::register)

callbacks.forEach { it.isEnabled = false }

assertFalse(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_removed_THEN_hasEnabledCallbacks_false() {
val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true))
callbacks.forEach(handler::register)

callbacks.forEach(handler::unregister)

assertFalse(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_multiple_enabled_callbacks_WHEN_all_callbacks_removed_except_one_THEN_hasEnabledCallbacks_true() {
val callbacks = listOf(callback(isEnabled = true), callback(isEnabled = true), callback(isEnabled = true))
callbacks.forEach(handler::register)

callbacks.drop(1).forEach(handler::unregister)

assertTrue(dispatcher.hasEnabledCallbacks())
}

@Test
fun GIVEN_all_callbacks_disabled_WHEN_onBackPressed_THEN_callbacks_not_called() {
var isCalled = false
repeat(3) {
handler.register(callback(isEnabled = false) { isCalled = true })
}

dispatcher.onBackPressed()

assertFalse(isCalled)
}

@Test
fun GIVEN_all_callbacks_enabled_WHEN_onBackPressed_THEN_only_last_callback_called() {
val called = MutableList(3) { false }

repeat(called.size) { index ->
handler.register(callback(isEnabled = true) { called[index] = true })
}

dispatcher.onBackPressed()

assertContentEquals(listOf(false, false, true), called)
}

@Test
fun GIVEN_only_one_callback_enabled_WHEN_onBackPressed_THEN_only_enabled_callback_called() {
val called = MutableList(3) { false }

repeat(called.size) { index ->
handler.register(callback(isEnabled = index == 0) { called[index] = true })
}

dispatcher.onBackPressed()

assertContentEquals(listOf(true, false, false), called)
}

@Test
fun GIVEN_multiple_enabled_callbacks_registered_and_all_callbacks_removed_except_one_WHEN_onBackPressed_THEN_callback_called() {
val called = MutableList(3) { false }
val callbacks = List(called.size) { index -> callback(isEnabled = true) { called[index] = true } }
callbacks.forEach(handler::register)
callbacks.drop(1).forEach(handler::unregister)

dispatcher.onBackPressed()

assertContentEquals(listOf(true, false, false), called)
}

private fun callback(isEnabled: Boolean = true, callback: () -> Unit = {}): Callback =
Callback(isEnabled = isEnabled, callback = callback)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.arkivanov.essenty.backhandler

import com.arkivanov.essenty.backhandler.BackHandler.Callback

internal abstract class AbstractBackHandler : BackHandler {

private var set = emptySet<Callback>()
private val enabledChangedListener: (Boolean) -> Unit = { onEnabledChanged() }

override fun register(callback: Callback) {
check(callback !in set) { "Callback is already registered" }

this.set += callback
callback.addEnabledChangedListener(enabledChangedListener)
onEnabledChanged()
}

override fun unregister(callback: Callback) {
check(callback in set) { "Callback is not registered" }

callback.removeEnabledChangedListener(enabledChangedListener)
this.set -= callback
onEnabledChanged()
}

private fun onEnabledChanged() {
onEnabledChanged(set.any(Callback::isEnabled))
}

protected abstract fun onEnabledChanged(isEnabled: Boolean)

protected fun callCallbacks(): Boolean {
set.lastOrNull(Callback::isEnabled)?.also {
it.callback()
return true
}

return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.arkivanov.essenty.backhandler

import kotlin.js.JsName

/**
* Provides a way to manually trigger back button handlers.
*/
interface BackDispatcher : BackHandler {

/**
* Returns `true` if there is at least one enabled handler, `false` otherwise.
*/
val isEnabled: Boolean

/**
* Iterates through all registered callbacks in reverse order and triggers the first one enabled.
*
* @return `true` if any handler was triggered, `false` otherwise.
*/
fun back(): Boolean
}

/**
* Creates and returns a default implementation of [BackDispatcher].
*/
@JsName("backDispatcher")
fun BackDispatcher(): BackDispatcher = DefaultBackDispatcher()

0 comments on commit 94d779c

Please sign in to comment.