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

New Alert builder system #53

Merged
merged 15 commits into from
Nov 12, 2019
24 changes: 11 additions & 13 deletions alerts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ It shows `AlertDialog` on Android and `UIAlertController` on iOS.
### Usage
The `BaseAlertBuilder` abstract class has implementations on the Android as `AlertBuilder` and iOS as `AlertsAlertBuilder`.
It has methods:
- `alert(initialize: BaseAlertBuilder.() -> Unit): AlertInterface` — builder to create `AlertInterface`
- `setTitle(title: String?)` — sets optional title for the alert
- `setMessage(message: String?)` — sets an optional message for the alert
- `setPositiveButton(title: String, handler: AlertActionHandler)` — sets a positive button for the alert
- `setNegativeButton(title: String, handler: AlertActionHandler)` — sets a negative button for the alert
- `setNeutralButton(title: String, handler: AlertActionHandler)` — sets a neutral button for the alert
- `addActions(actions: List<Alert.Action>)` — adds a list of actions for the alert
- `create(): AlertInterface` — returns created `AlertInterface`

On Android this builder needs a `Context` object:

Expand All @@ -36,21 +36,19 @@ The `AlertInterface` has methods to show and dismiss alert:
Building and displaying alert on Android:

```kotlin
AlertBuilder(context)
.setTitle("Hello, Kaluga")
.setPositiveButton("OK") { println("OK pressed") }
.setNegativeButton("Cancel") { println("Cancel pressed") }
.setNeutralButton("Details") { println("Details pressed") }
.create()
.show()
AlertBuilder(context).alert {
setTitle("Hello, Kaluga")
setPositiveButton("OK") { println("OK pressed") }
setNegativeButton("Cancel") { println("Cancel pressed") }
setNeutralButton("Details") { println("Details pressed") }
}.show()
```

On iOS:

```swift
AlertsAlertBuilder(viewController: viewController)
.setTitle(title: "Hello, Kaluga")
.setPositiveButton(title: "OK", handler: { debugPrint("OK pressed") } )
.create()
.show(animated: true) { debugPrint("Presented") }
AlertsAlertBuilder(viewController: viewController).alert {
$0.setTitle(title: "Hello, Kaluga")
$0.setPositiveButton(title: "OK") { debugPrint("OK pressed") }
}.show(animated: true) { debugPrint("Presented") }
```
68 changes: 47 additions & 21 deletions alerts/src/androidLibAndroidTest/kotlin/MockAlertsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runBlockingTest
import org.junit.*
import org.junit.Test
import kotlin.test.*
Expand Down Expand Up @@ -41,29 +42,54 @@ class MockAlertsTest {
@Test
fun testAlertBuilderExceptionNoActions() {
assertFailsWith<IllegalArgumentException> {
AlertBuilder(activityRule.activity)
.setTitle("OK")
.create()
AlertBuilder(activityRule.activity).alert {
setTitle("OK")
}
}
}

@Test
fun testAlertBuilderExceptionNoTitleOrMessage() {
assertFailsWith<IllegalArgumentException> {
AlertBuilder(activityRule.activity)
.setPositiveButton("OK") { }
.create()
AlertBuilder(activityRule.activity).alert {
setPositiveButton("OK")
}
}
}

@Test
fun testBuilderReuse() = runBlockingTest {

val builder = AlertBuilder(activityRule.activity)

CoroutineScope(Dispatchers.Main).launch {
builder.alert {
setTitle("Test")
setPositiveButton("OK")
}.show()

device.wait(Until.findObject(By.text("Test")), DEFAULT_TIMEOUT)
device.findObject(By.text("OK")).click()
assertTrue(device.wait(Until.gone(By.text("Test")), DEFAULT_TIMEOUT))

builder.alert {
setTitle("Hello")
setNegativeButton("Cancel")
}.show()

device.wait(Until.findObject(By.text("Hello")), DEFAULT_TIMEOUT)
device.findObject(By.text("Cancel")).click()
assertTrue(device.wait(Until.gone(By.text("Hello")), DEFAULT_TIMEOUT))
}
}

@Test
fun testAlertShow() = runBlocking {
CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main) {
AlertBuilder(activityRule.activity)
.setTitle("Hello")
.setPositiveButton("OK") { }
.create()
.show()
AlertBuilder(activityRule.activity).alert {
setTitle("Hello")
setPositiveButton("OK")
}.show()
}
device.wait(Until.findObject(By.text("Hello")), DEFAULT_TIMEOUT)
device.findObject(By.text("OK")).click()
Expand All @@ -74,10 +100,10 @@ class MockAlertsTest {
fun testAlertFlowWithCoroutines() = runBlocking {
CoroutineScope(Dispatchers.Main).launch {
val action = Alert.Action("OK")
val presenter = AlertBuilder(activityRule.activity)
.setTitle("Hello")
.addActions(listOf(action))
.create()
val presenter = AlertBuilder(activityRule.activity).alert {
setTitle("Hello")
addActions(listOf(action))
}

val result = withContext(coroutineContext) { presenter.show() }
assertEquals(result, action)
Expand All @@ -90,17 +116,17 @@ class MockAlertsTest {
@Test
fun testAlertFlowCancel() = runBlocking {
val coroutine = CoroutineScope(Dispatchers.Main).launch {
val presenter = AlertBuilder(activityRule.activity)
.setTitle("Hello")
.setPositiveButton("OK") { }
.setNegativeButton("Cancel") { }
.create()
val presenter = AlertBuilder(activityRule.activity).alert {
setTitle("Hello")
setPositiveButton("OK")
setNegativeButton("Cancel")
}

val result = coroutineContext.run { presenter.show() }
assertNull(result)
}
device.wait(Until.findObject(By.text("Hello")), DEFAULT_TIMEOUT)
// On cancel we expect dialog to be dismissed
// On cancel call, we expect the dialog to be dismissed
coroutine.cancel()
assertTrue(device.wait(Until.gone(By.text("Hello")), DEFAULT_TIMEOUT))
}
Expand Down
27 changes: 24 additions & 3 deletions alerts/src/commonMain/kotlin/Alerts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ abstract class BaseAlertPresenter(private val alert: Alert) : AlertActions {
dismissAlert(animated)
}

protected abstract fun dismissAlert(animated: Boolean = true)
internal abstract fun dismissAlert(animated: Boolean = true)

protected abstract fun showAlert(
internal abstract fun showAlert(
animated: Boolean = true,
afterHandler: (Alert.Action?) -> Unit = {},
completion: () -> Unit = {}
Expand Down Expand Up @@ -189,13 +189,34 @@ abstract class BaseAlertBuilder {
*/
fun addActions(actions: List<Alert.Action>) = apply { this.actions.addAll(actions) }

/**
* Builds an alert using DSL syntax
*
* @param initialize
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation

* @return
*/
fun alert(initialize: BaseAlertBuilder.() -> Unit): AlertInterface {
reset()
initialize()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern makes the builder non threadsafe.

That might be acceptable, but what will happen if we pop more than one dialog? Will the handlers also be reset? (seems like they are stored in actions).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably make a test for this behaviour, regardless of what happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good point

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might work now. The multithreaded testcase is hard, but the testcase for building two alerts in a row can still be added I think (unless I missed it somewhere).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely

return create()
}

/**
* Adds an [action] to the alert
*
* @param action The action object
*/
private fun addAction(action: Alert.Action) = apply { this.actions.add(action) }

/**
* Reset builder into initial state
*/
private fun reset() = apply {
thoutbeckers marked this conversation as resolved.
Show resolved Hide resolved
this.title = null
this.message = null
this.actions.clear()
}

/**
* Creates an alert based on [title], [message] and [actions] properties
*
Expand All @@ -214,7 +235,7 @@ abstract class BaseAlertBuilder {
*
* @return The AlertInterface object
*/
abstract fun create(): AlertInterface
internal abstract fun create(): AlertInterface
}

expect class AlertBuilder : BaseAlertBuilder
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import com.splendo.kaluga.alerts.Alert
import com.splendo.kaluga.alerts.AlertBuilder
import com.splendo.kaluga.example.R
import kotlinx.coroutines.MainScope
Expand Down Expand Up @@ -31,9 +32,13 @@ Copyright 2019 Splendo Consulting B.V. The Netherlands
@SuppressLint("SetTextI18n")
class AlertsActivity : AppCompatActivity(R.layout.activity_alerts) {

private lateinit var alertBuilder: AlertBuilder

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

alertBuilder = AlertBuilder(this)

btn_simple_alert.setOnClickListener {
MainScope().launch {
showAlert()
Expand All @@ -48,29 +53,29 @@ class AlertsActivity : AppCompatActivity(R.layout.activity_alerts) {
}

private suspend fun showAlert() {
AlertBuilder(this)
.setTitle("Hello, Kaluga")
.setPositiveButton("OK") { println("OK pressed") }
.setNegativeButton("Cancel") { println("Cancel pressed") }
.setNeutralButton("Details") { println("Details pressed") }
.create()
.show()
val okAction = Alert.Action("OK", Alert.Action.Style.POSITIVE)
val cancelAction = Alert.Action("Cancel", Alert.Action.Style.NEGATIVE)
val alert = alertBuilder.alert {
setTitle("Hello, Kaluga")
addActions(listOf(okAction, cancelAction))
}
when (alert.show()) {
okAction -> println("OK pressed")
cancelAction -> println("Cancel pressed")
}
}

private fun showDismissibleAlert() {

val coroutine = MainScope().launch {
val presenter = AlertBuilder(this@AlertsActivity)
.setTitle("Hello")
.setMessage("Wait for 3 sec...")
.setPositiveButton("OK") { println("OK pressed") }
.create()

presenter.show()
alertBuilder.alert {
setTitle("Hello")
setMessage("Wait for 3 sec...")
setPositiveButton("OK") { println("OK pressed") }
}.show()
}

Handler().postDelayed({
coroutine.cancel()
}, 3000)
}, 3_000)
}
}
}
21 changes: 13 additions & 8 deletions example/ios/Demo/Alerts/AlertsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import KotlinNativeFramework

class AlertsViewController: UITableViewController {

lazy var alertsBuilder = AlertsAlertBuilder(viewController: self)

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

Expand All @@ -32,22 +34,25 @@ class AlertsViewController: UITableViewController {
}

fileprivate func showAlert() {
KotlinNativeFramework()
.makeAlert(from: self, title: "Hello, Kaluga", message: nil, actions: [
alertsBuilder.alert { builder in
builder.setTitle(title: "Hello")
builder.setMessage(message: "From Kaluga")
builder.addActions(actions: [
AlertsAlert.Action(title: "Default", style: .default_) { debugPrint("OK") },
AlertsAlert.Action(title: "Destructive", style: .destructive) { debugPrint("Not OK") },
AlertsAlert.Action(title: "Cancel", style: .cancel) { debugPrint("Cancel") },
])
.show(animated: true) { }
}.show(animated: true) { }
}

fileprivate func showWithDismiss() {
let presenter = KotlinNativeFramework().makeAlert(from: self, title: "Wait for 3 sec...", message: "Automatic dismissible", actions: [
AlertsAlert.Action(title: "OK", style: .cancel) {},
])
presenter.show(animated: true) {
let alert = alertsBuilder.alert { builder in
builder.setTitle(title: "Wait for 3 sec...")
builder.setPositiveButton(title: "OK") { }
}
alert.show(animated: true) {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
presenter.dismiss(animated: true)
alert.dismiss(animated: true)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import platform.UIKit.UILabel
import ru.pocketbyte.hydra.log.HydraLog
import platform.UIKit.UIViewController

fun alert(viewController: UIViewController, buildAlert: AlertBuilder.() -> Unit): AlertInterface {
val builder = AlertBuilder(viewController)
return builder.alert { builder.buildAlert() }
}

class KotlinNativeFramework {
private val loc = LocationFlowable()

Expand All @@ -42,33 +47,20 @@ class KotlinNativeFramework {
// expose a dependency to Swift as an example
fun logger(): ru.pocketbyte.hydra.log.Logger = HydraLog.logger

fun makeAlert(from: UIViewController, title: String? = null, message: String? = null, actions: List<Alert.Action>): AlertInterface {
return AlertBuilder(from)
.setTitle(title)
.setMessage(message)
.addActions(actions)
.create()
}

fun loadingIndicator(view: UIViewController): LoadingIndicator {
return IOSLoadingIndicator
.Builder(view)
.create()
}
fun loadingIndicator(view: UIViewController) = IOSLoadingIndicator
.Builder(view)
.create()

fun location(label: UILabel, locationManager: CLLocationManager) {
loc.addCLLocationManager(locationManager)
LocationPrinter(loc).printTo {
label.text = it
}
debug("proceed executing after location coroutines")

}

fun permissions(nsBundle: NSBundle): Permissions {
return Permissions.Builder()
.bundle(nsBundle)
.build()
}

fun permissions(nsBundle: NSBundle) = Permissions
.Builder()
.bundle(nsBundle)
.build()
}