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
1 change: 1 addition & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions Components/src/androidLibMain/kotlin/dispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.splendo.kaluga

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

/*

Copyright 2019 Splendo Consulting B.V. The Netherlands

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.

*/

actual val MainQueueDispatcher: CoroutineDispatcher = Dispatchers.Main
23 changes: 23 additions & 0 deletions Components/src/commonMain/kotlin/dispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.splendo.kaluga

import kotlinx.coroutines.CoroutineDispatcher

/*

Copyright 2019 Splendo Consulting B.V. The Netherlands

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.

*/

expect val MainQueueDispatcher: CoroutineDispatcher
43 changes: 43 additions & 0 deletions Components/src/iosMain/kotlin/dispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.splendo.kaluga

import kotlinx.coroutines.*
import platform.darwin.*
import kotlin.coroutines.CoroutineContext

/*

Copyright 2019 Splendo Consulting B.V. The Netherlands

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.

*/

@UseExperimental(InternalCoroutinesApi::class)
internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher(), Delay {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems based on an old example. Kotlin common has classes like MainScope and Dispatchers that can take care of this, as per the current examples.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also thought that. I tried MainScope().launch and GlobalScope.launch(Dispatchers.Main) and expected to run them in main queue on iOS. But in all cases I've got an exception:

kotlin.IllegalStateException: There is no event loop. Use runBlocking { ... } to start one.

Looks like this one Kotlin/kotlinx.coroutines#470 is still open and not included into library, and we have to keep this workaround. Or I missed something...

Copy link
Collaborator

@thoutbeckers thoutbeckers Nov 8, 2019

Choose a reason for hiding this comment

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

Look at the Components examples, you need to wrap the launch in runBlocking for now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps we should make a convenience launch method, because despite of the error message people are confused by this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still not clear how to use it then. We have suspend alert to build alert and suspend show to show. If we wrap all with runBlocking it will never return because show is waiting for user's input.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You wrap the launch inside runBlocking. runBlocking will not wait for what happens inside launch.

Did you check the example of Components?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean example of LocationPrinter?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd look at all of them, also to see how common testing is done (#56), but yeah just search launch or MainScope and you can see the usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, in AlertFactory in shared example we have three functions.
Last one works with runBlocking { MainScope().launch { ... }} fine.
But this approach doesn't work if I have delay call inside with the same event loop complains. And this doesn't work for CancelableContinuation (when we call resume) for the same reasons.


// Dispatch block on given queue
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue) { block.run() }
}

// Support Delay
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatchQueue) {
with(continuation) {
resumeUndispatched(Unit)
}
}
}
}

actual val MainQueueDispatcher: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())
24 changes: 24 additions & 0 deletions Components/src/jsMain/kotlin/dispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.splendo.kaluga

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

/*

Copyright 2019 Splendo Consulting B.V. The Netherlands

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.

*/

actual val MainQueueDispatcher: CoroutineDispatcher = Dispatchers.Main
24 changes: 24 additions & 0 deletions Components/src/jvmMain/kotlin/dispatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.splendo.kaluga

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

/*

Copyright 2019 Splendo Consulting B.V. The Netherlands

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.

*/

actual val MainQueueDispatcher: CoroutineDispatcher = Dispatchers.Main
52 changes: 37 additions & 15 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:
- `suspend 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 @@ -33,24 +33,46 @@ The `AlertInterface` has methods to show and dismiss alert:

### Example

Building and displaying alert on Android:
Create an Alert:

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

On iOS:
In order to show alert use `alert.show()` call. Alert will be dismissed after the user pressed a button.
Dialog is cancelable, so the user also can tap outside of the alert or press back button to dismiss.
You also can dismiss it in code with `alert.dismiss()` call.

Before use on iOS platform you have to wrap `suspend` calls inside shared Kotlin code like this:

```kotlin
fun showAlert(builder: AlertBuilder, title: String) = GlobalScope.launch(MainQueueDispatcher) {
// Create OK action
val okAction = Alert.Action("OK", Alert.Action.Style.POSITIVE)
// Create Cancel action
val cancelAction = Alert.Action("Cancel", Alert.Action.Style.NEGATIVE)
// Create an Alert with title, message and actions
val alert = builder.alert {
setTitle(title)
setMessage("This is sample message")
addActions(listOf(okAction, cancelAction))
}
// Show and handle actions
when (alert.show()) {
okAction -> log(LogLevel.DEBUG, "OK pressed")
cancelAction -> log(LogLevel.DEBUG, "Cancel pressed")
}
}
```

Then call with platform-specific builder:

```swift
AlertsAlertBuilder(viewController: viewController)
.setTitle(title: "Hello, Kaluga")
.setPositiveButton(title: "OK", handler: { debugPrint("OK pressed") } )
.create()
.show(animated: true) { debugPrint("Presented") }
let builder = AlertBuilder(viewController: viewController)
SharedKt.showAlert(builder, "Hello from iOS")
```
78 changes: 52 additions & 26 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 @@ -39,45 +40,70 @@ class MockAlertsTest {
}

@Test
fun testAlertBuilderExceptionNoActions() {
fun testAlertBuilderExceptionNoActions() = runBlockingTest {
assertFailsWith<IllegalArgumentException> {
AlertBuilder(activityRule.activity)
.setTitle("OK")
.create()
AlertBuilder(activityRule.activity).alert {
setTitle("OK")
}
}
}

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

@Test
fun testAlertShow() = runBlocking {
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() = runBlockingTest {
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()
assertTrue(device.wait(Until.gone(By.text("Hello")), DEFAULT_TIMEOUT))
}

@Test
fun testAlertFlowWithCoroutines() = runBlocking {
fun testAlertFlowWithCoroutines() = runBlockingTest {
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 @@ -88,19 +114,19 @@ class MockAlertsTest {
}

@Test
fun testAlertFlowCancel() = runBlocking {
fun testAlertFlowCancel() = runBlockingTest {
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
25 changes: 17 additions & 8 deletions alerts/src/androidLibMain/kotlin/Alerts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ Copyright 2019 Splendo Consulting B.V. The Netherlands
*/

actual class AlertBuilder(private val context: Context) : BaseAlertBuilder() {

override fun create(): AlertInterface {
return AlertInterface(createAlert(), context)
}
override fun create() = AlertInterface(createAlert(), context)
}

actual class AlertInterface(
Expand Down Expand Up @@ -54,14 +51,26 @@ actual class AlertInterface(
alertDialog = AlertDialog.Builder(context)
.setTitle(alert.title)
.setMessage(alert.message)
.create()
.apply {
alert.actions.forEach { action ->
setButton(transform(action.style), action.title) { _, _ ->
action.handler()
if (alert.style == Alert.Style.ACTION_LIST) {
val actions = alert.actions.toTypedArray()
val titles = actions.map { it.title }.toTypedArray()
setItems(titles) { _, which ->
val action = alert.actions[which].apply { handler() }
afterHandler(action)
}
}
}
.create()
.apply {
if (alert.style == Alert.Style.ALERT) {
alert.actions.forEach { action ->
setButton(transform(action.style), action.title) { _, _ ->
action.handler()
afterHandler(action)
}
}
}
setOnDismissListener { alertDialog = null }
setOnCancelListener { afterHandler(null) }
show()
Expand Down
Loading