Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added back-handler module, deprecated back-pressed module #62

Merged
merged 1 commit into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()