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

Support the Emoji Search sample on desktop. #1755

Merged
merged 11 commits into from
Feb 22, 2024
Merged
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version

kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }

kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
Expand Down Expand Up @@ -69,7 +70,7 @@ assertk = "com.willowtreeapps.assertk:assertk:0.28.0"
robolectric = { module = "org.robolectric:robolectric", version = "4.11.1" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
okio-assetfilesystem = { module = "com.squareup.okio:okio-assetfilesystem", version.ref = "okio" }
okHttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" }
paging-compose-common = { module = "app.cash.paging:paging-compose-common", version = "3.3.0-alpha02-0.4.0" }
zipline = { module = "app.cash.zipline:zipline", version.ref = "zipline" }
zipline-gradlePlugin = { module = "app.cash.zipline:zipline-gradle-plugin", version.ref = "zipline" }
Expand Down
2 changes: 1 addition & 1 deletion redwood-treehouse-host/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ kotlin {

androidMain {
dependencies {
api libs.okHttp
api libs.okhttp
implementation libs.androidx.activity
}
}
Expand Down
79 changes: 79 additions & 0 deletions samples/emoji-search/composeui/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'org.jetbrains.compose'

redwoodBuild {
embedZiplineApplication(projects.samples.emojiSearch.presenterTreehouse)
}

android {
namespace 'com.example.redwood.emojisearch.composeui'

defaultConfig {
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

buildFeatures {
// Needed to pass application ID to UIAutomator tests.
buildConfig = true
}
}

compose {
desktop {
application {
mainClass = "com.example.redwood.emojisearch.composeui.Main"
}
}
}

kotlin {
androidTarget()
jvm("desktop")
JakeWharton marked this conversation as resolved.
Show resolved Hide resolved

sourceSets {
commonMain {
dependencies {
implementation projects.samples.emojiSearch.launcher
implementation projects.samples.emojiSearch.presenterTreehouse
implementation projects.samples.emojiSearch.schema.widget.protocol
implementation projects.redwoodLayoutComposeui
implementation projects.redwoodLazylayoutComposeui
implementation projects.redwoodTreehouseHost
implementation projects.redwoodTreehouseHostComposeui
implementation projects.redwoodWidgetCompose
implementation libs.coil.compose
implementation libs.coil.network
implementation libs.ktor.engine.okhttp
implementation libs.jetbrains.compose.material
implementation libs.jetbrains.compose.ui
implementation libs.jetbrains.compose.ui.tooling.preview
implementation libs.zipline.loader
}
}
androidMain {
dependencies {
implementation libs.androidx.activity.compose
implementation libs.androidx.appCompat
implementation libs.androidx.core
implementation libs.google.material
implementation libs.kotlinx.coroutines.android
implementation libs.okio.assetfilesystem
}
}
desktopMain {
dependencies {
implementation projects.samples.emojiSearch.presenter
implementation projects.redwoodComposeui
implementation compose.desktop.currentOs
implementation libs.kotlinx.coroutines.swing
}
}
androidInstrumentedTest {
dependencies {
implementation libs.androidx.test.runner
implementation projects.samples.emojiSearch.androidTests
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
Expand Down Expand Up @@ -63,14 +65,15 @@ class EmojiSearchActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
applySingletonImageLoader()

val treehouseApp = createTreehouseApp()
val treehouseContentSource = TreehouseContentSource(EmojiSearchPresenter::launch)

val widgetSystem = WidgetSystem { json, protocolMismatchHandler ->
EmojiSearchProtocolFactory<@Composable () -> Unit>(
provider = EmojiSearchWidgetFactories(
EmojiSearch = AndroidEmojiSearchWidgetFactory(),
EmojiSearch = ComposeUiEmojiSearchWidgetFactory(),
RedwoodLayout = ComposeUiRedwoodLayoutWidgetFactory(),
RedwoodLazyLayout = ComposeUiRedwoodLazyLayoutWidgetFactory(),
),
Expand Down Expand Up @@ -141,7 +144,14 @@ class EmojiSearchActivity : ComponentActivity() {
appScope = scope,
spec = EmojiSearchAppSpec(
manifestUrl = manifestUrlFlow,
hostApi = RealHostApi(this@EmojiSearchActivity, httpClient),
hostApi = RealHostApi(
client = httpClient,
openUrl = { url ->
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse(url))
startActivity(intent)
},
),
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import androidx.compose.runtime.Composable
import com.example.redwood.emojisearch.widget.EmojiSearchWidgetFactory
import com.example.redwood.emojisearch.widget.Image
import com.example.redwood.emojisearch.widget.Text
import com.example.redwood.emojisearch.widget.TextInput

class AndroidEmojiSearchWidgetFactory : EmojiSearchWidgetFactory<@Composable () -> Unit> {
class ComposeUiEmojiSearchWidgetFactory : EmojiSearchWidgetFactory<@Composable () -> Unit> {
override fun TextInput(): TextInput<@Composable () -> Unit> = ComposeUiTextInput()
override fun Text(): Text<@Composable () -> Unit> = ComposeUiText()
override fun Image(): Image<@Composable () -> Unit> = ComposeUiImage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DefaultMonotonicFrameClock
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
Expand All @@ -30,6 +31,7 @@ internal class ComposeUiText : Text<@Composable () -> Unit> {
override var modifier: Modifier = Modifier

override val value = @Composable {
DefaultMonotonicFrameClock
Text(
text = text,
color = MaterialTheme.colors.onBackground,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import androidx.compose.material.Text
import androidx.compose.material.TextField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat.startActivity
import com.example.redwood.emojisearch.treehouse.HostApi
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
Expand All @@ -36,8 +32,8 @@ class HttpException(response: Response) :
RuntimeException("HTTP ${response.code} ${response.message}")

class RealHostApi(
private val context: Context,
private val client: OkHttpClient,
private val openUrl: (url: String) -> Unit,
) : HostApi {
override suspend fun httpCall(url: String, headers: Map<String, String>): String {
return suspendCancellableCoroutine { continuation ->
Expand Down Expand Up @@ -67,8 +63,6 @@ class RealHostApi(
}

override fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.setData(Uri.parse(url))
startActivity(context, intent, null)
openUrl.invoke(url)

Choose a reason for hiding this comment

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

👍

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Square, Inc.
* Copyright (C) 2022 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -13,17 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.redwood.emojisearch.android.composeui
package com.example.redwood.emojisearch.composeui

import android.app.Application
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.fetch.NetworkFetcher

class EmojiSearchApplication : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(context)
fun applySingletonImageLoader() {
SingletonImageLoader.setSafe { context ->
ImageLoader.Builder(context)
.components {
add(NetworkFetcher.Factory())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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.
*/
package com.example.redwood.emojisearch.composeui

import com.example.redwood.emojisearch.presenter.Navigator
import java.awt.Desktop
import java.net.URI

class DesktopNavigator : Navigator {
override fun openUrl(url: String) {
Desktop.getDesktop().browse(URI(url))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2022 Square, Inc.
*
* 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.
*/
package com.example.redwood.emojisearch.composeui

import com.example.redwood.emojisearch.presenter.HttpClient
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CompletionHandler
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException

class JvmHttpClient(
private val okHttpClient: OkHttpClient = OkHttpClient(),
) : HttpClient {

override suspend fun call(url: String, headers: Map<String, String>): String {
val request = Request.Builder()
.url(url)
.headers(headers.toHeaders())
.build()
val response = okHttpClient.newCall(request).await()
return response.body?.string().orEmpty()
}
}

private suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
val callback = ContinuationCallback(this, continuation)
enqueue(callback)
continuation.invokeOnCancellation(callback)
}
}

private class ContinuationCallback(
private val call: Call,
private val continuation: CancellableContinuation<Response>,
) : Callback, CompletionHandler {

override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}

override fun onFailure(call: Call, e: IOException) {
if (!call.isCanceled()) {
continuation.resumeWithException(e)
}
}

override fun invoke(cause: Throwable?) {
try {
call.cancel()
} catch (_: Throwable) {}
}
}
Loading
Loading