Skip to content

Commit bf945e1

Browse files
authored
Merge pull request #288 from hotwired/strada-sample
Strada sample components in the Demo app
2 parents f40d830 + 6f4bd8c commit bf945e1

18 files changed

+472
-5
lines changed

demo/build.gradle

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
apply plugin: 'com.android.application'
22
apply plugin: 'kotlin-android'
3+
apply plugin: 'kotlinx-serialization'
4+
5+
buildscript {
6+
repositories {
7+
google()
8+
mavenCentral()
9+
}
10+
11+
dependencies {
12+
classpath "org.jetbrains.kotlin:kotlin-serialization:1.8.0"
13+
}
14+
}
315

416
android {
517
compileSdkVersion 33
@@ -13,6 +25,10 @@ android {
1325
vectorDrawables.useSupportLibrary = true
1426
}
1527

28+
buildFeatures {
29+
viewBinding = true
30+
}
31+
1632
buildTypes {
1733
release {
1834
minifyEnabled false
@@ -49,11 +65,13 @@ android {
4965

5066
dependencies {
5167
implementation fileTree(include: ['*.jar'], dir: 'libs')
52-
implementation 'com.google.android.material:material:1.8.0'
68+
implementation 'com.google.android.material:material:1.9.0'
5369
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
54-
implementation 'androidx.recyclerview:recyclerview:1.3.0'
70+
implementation 'androidx.recyclerview:recyclerview:1.3.1'
5571
implementation 'androidx.browser:browser:1.5.0'
72+
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
5673
implementation 'com.github.bumptech.glide:glide:4.15.1'
74+
implementation 'dev.hotwire:strada:1.0.0-beta2'
5775

5876
implementation project(':turbo')
5977
}

demo/src/main/assets/json/configuration.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
},
2727
{
2828
"patterns": [
29-
"/signin$"
29+
"/signin$",
30+
"/strada-form$"
3031
],
3132
"properties": {
3233
"context": "modal",

demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import androidx.browser.customtabs.CustomTabsIntent
77
import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON
88
import androidx.navigation.NavOptions
99
import androidx.navigation.navOptions
10+
import dev.hotwire.strada.BridgeDestination
1011
import dev.hotwire.turbo.config.TurboPathConfigurationProperties
1112
import dev.hotwire.turbo.config.context
1213
import dev.hotwire.turbo.demo.R
1314
import dev.hotwire.turbo.demo.util.BASE_URL
1415
import dev.hotwire.turbo.nav.TurboNavDestination
1516
import dev.hotwire.turbo.nav.TurboNavPresentationContext.MODAL
1617

17-
interface NavDestination : TurboNavDestination {
18+
interface NavDestination : TurboNavDestination, BridgeDestination {
1819
val menuProgress: MenuItem?
1920
get() = toolbarForNavigation()?.menu?.findItem(R.id.menu_progress)
2021

@@ -38,6 +39,10 @@ interface NavDestination : TurboNavDestination {
3839
}
3940
}
4041

42+
override fun bridgeWebViewIsReady(): Boolean {
43+
return session.isReady
44+
}
45+
4146
private fun isNavigable(location: String): Boolean {
4247
return location.startsWith(BASE_URL)
4348
}

demo/src/main/kotlin/dev/hotwire/turbo/demo/features/numbers/NumbersAdapter.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.hotwire.turbo.demo.features.numbers
22

3+
import android.annotation.SuppressLint
34
import android.view.LayoutInflater
45
import android.view.View
56
import android.view.ViewGroup
@@ -11,6 +12,7 @@ class NumbersAdapter(val callback: NumbersFragmentCallback) : RecyclerView.Adapt
1112
private val type = R.layout.adapter_numbers_row
1213

1314
private var items = emptyList<Int>()
15+
@SuppressLint("NotifyDataSetChanged")
1416
set(value) {
1517
field = value
1618
notifyDataSetChanged()

demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,52 @@ package dev.hotwire.turbo.demo.features.web
22

33
import android.os.Bundle
44
import android.view.View
5+
import dev.hotwire.strada.BridgeDelegate
56
import dev.hotwire.turbo.demo.R
67
import dev.hotwire.turbo.demo.base.NavDestination
8+
import dev.hotwire.turbo.demo.strada.bridgeComponentFactories
79
import dev.hotwire.turbo.demo.util.SIGN_IN_URL
810
import dev.hotwire.turbo.fragments.TurboWebFragment
911
import dev.hotwire.turbo.nav.TurboNavGraphDestination
12+
import dev.hotwire.turbo.views.TurboWebView
1013
import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE
1114
import dev.hotwire.turbo.visit.TurboVisitOptions
1215

1316
@TurboNavGraphDestination(uri = "turbo://fragment/web")
1417
open class WebFragment : TurboWebFragment(), NavDestination {
18+
private val bridgeDelegate by lazy {
19+
BridgeDelegate(
20+
location = location,
21+
destination = this,
22+
componentFactories = bridgeComponentFactories
23+
)
24+
}
25+
1526
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
1627
super.onViewCreated(view, savedInstanceState)
1728
setupMenu()
29+
viewLifecycleOwner.lifecycle.addObserver(bridgeDelegate)
30+
}
31+
32+
override fun onDestroyView() {
33+
super.onDestroyView()
34+
viewLifecycleOwner.lifecycle.removeObserver(bridgeDelegate)
35+
}
36+
37+
override fun onColdBootPageStarted(location: String) {
38+
bridgeDelegate.onColdBootPageStarted()
39+
}
40+
41+
override fun onColdBootPageCompleted(location: String) {
42+
bridgeDelegate.onColdBootPageCompleted()
43+
}
44+
45+
override fun onWebViewAttached(webView: TurboWebView) {
46+
bridgeDelegate.onWebViewAttached(webView)
47+
}
48+
49+
override fun onWebViewDetached(webView: TurboWebView) {
50+
bridgeDelegate.onWebViewDetached()
1851
}
1952

2053
override fun onFormSubmissionStarted(location: String) {

demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package dev.hotwire.turbo.demo.main
33
import android.os.Bundle
44
import android.webkit.WebView
55
import androidx.appcompat.app.AppCompatActivity
6+
import dev.hotwire.strada.KotlinXJsonConverter
7+
import dev.hotwire.strada.Strada
68
import dev.hotwire.turbo.BuildConfig
79
import dev.hotwire.turbo.activities.TurboActivity
810
import dev.hotwire.turbo.config.Turbo
@@ -21,8 +23,11 @@ class MainActivity : AppCompatActivity(), TurboActivity {
2123
}
2224

2325
private fun configApp() {
26+
Strada.config.jsonConverter = KotlinXJsonConverter()
27+
2428
if (BuildConfig.DEBUG) {
2529
Turbo.config.debugLoggingEnabled = true
30+
Strada.config.debugLoggingEnabled = true
2631
WebView.setWebContentsDebuggingEnabled(true)
2732
}
2833
}

demo/src/main/kotlin/dev/hotwire/turbo/demo/main/MainSessionNavHostFragment.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.hotwire.turbo.demo.main
22

33
import androidx.appcompat.app.AppCompatActivity
44
import androidx.fragment.app.Fragment
5+
import dev.hotwire.strada.Bridge
56
import dev.hotwire.turbo.config.TurboPathConfiguration
67
import dev.hotwire.turbo.demo.features.imageviewer.ImageViewerFragment
78
import dev.hotwire.turbo.demo.features.numbers.NumberBottomSheetFragment
@@ -43,7 +44,12 @@ class MainSessionNavHostFragment : TurboSessionNavHostFragment() {
4344

4445
override fun onSessionCreated() {
4546
super.onSessionCreated()
47+
48+
// Configure WebView
4649
session.webView.settings.userAgentString = session.webView.customUserAgent
4750
session.webView.initDayNightTheme()
51+
52+
// Initialize Strada bridge with new WebView instance
53+
Bridge.initialize(session.webView)
4854
}
4955
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.hotwire.turbo.demo.strada
2+
3+
import dev.hotwire.strada.BridgeComponentFactory
4+
5+
val bridgeComponentFactories = listOf(
6+
BridgeComponentFactory("form", ::FormComponent),
7+
BridgeComponentFactory("menu", ::MenuComponent),
8+
BridgeComponentFactory("overflow-menu", ::OverflowMenuComponent)
9+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dev.hotwire.turbo.demo.strada
2+
3+
import android.util.Log
4+
import android.view.LayoutInflater
5+
import android.view.Menu
6+
import android.view.MenuItem
7+
import androidx.appcompat.widget.Toolbar
8+
import androidx.fragment.app.Fragment
9+
import dev.hotwire.strada.BridgeComponent
10+
import dev.hotwire.strada.BridgeDelegate
11+
import dev.hotwire.strada.Message
12+
import dev.hotwire.turbo.demo.R
13+
import dev.hotwire.turbo.demo.base.NavDestination
14+
import dev.hotwire.turbo.demo.databinding.FormComponentSubmitBinding
15+
import kotlinx.serialization.SerialName
16+
import kotlinx.serialization.Serializable
17+
18+
/**
19+
* Bridge component to display a submit button in the native toolbar,
20+
* which will submit the form on the page when tapped.
21+
*/
22+
class FormComponent(
23+
name: String,
24+
private val delegate: BridgeDelegate<NavDestination>
25+
) : BridgeComponent<NavDestination>(name, delegate) {
26+
27+
private val submitButtonItemId = 37
28+
private var submitMenuItem: MenuItem? = null
29+
private val fragment: Fragment
30+
get() = delegate.destination.fragment
31+
private val toolbar: Toolbar?
32+
get() = fragment.view?.findViewById(R.id.toolbar)
33+
34+
override fun onReceive(message: Message) {
35+
when (message.event) {
36+
"connect" -> handleConnectEvent(message)
37+
"submitEnabled" -> handleSubmitEnabled()
38+
"submitDisabled" -> handleSubmitDisabled()
39+
else -> Log.w("TurboDemo", "Unknown event for message: $message")
40+
}
41+
}
42+
43+
private fun handleConnectEvent(message: Message) {
44+
val data = message.data<MessageData>() ?: return
45+
showToolbarButton(data)
46+
}
47+
48+
private fun handleSubmitEnabled() {
49+
toggleSubmitButton(true)
50+
}
51+
52+
private fun handleSubmitDisabled() {
53+
toggleSubmitButton(false)
54+
}
55+
56+
private fun showToolbarButton(data: MessageData) {
57+
val menu = toolbar?.menu ?: return
58+
val inflater = LayoutInflater.from(fragment.requireContext())
59+
val binding = FormComponentSubmitBinding.inflate(inflater)
60+
val order = 999 // Show as the right-most button
61+
62+
binding.formSubmit.apply {
63+
text = data.title
64+
setOnClickListener {
65+
performSubmit()
66+
}
67+
}
68+
69+
menu.removeItem(submitButtonItemId)
70+
submitMenuItem = menu.add(Menu.NONE, submitButtonItemId, order, data.title).apply {
71+
actionView = binding.root
72+
setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
73+
}
74+
}
75+
76+
private fun toggleSubmitButton(enable: Boolean) {
77+
val layout = submitMenuItem?.actionView ?: return
78+
79+
FormComponentSubmitBinding.bind(layout).apply {
80+
formSubmit.isEnabled = enable
81+
}
82+
}
83+
84+
private fun performSubmit(): Boolean {
85+
return replyTo("connect")
86+
}
87+
88+
@Serializable
89+
data class MessageData(
90+
@SerialName("submitTitle") val title: String
91+
)
92+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package dev.hotwire.turbo.demo.strada
2+
3+
import android.util.Log
4+
import android.view.LayoutInflater
5+
import androidx.fragment.app.Fragment
6+
import androidx.recyclerview.widget.LinearLayoutManager
7+
import com.google.android.material.bottomsheet.BottomSheetDialog
8+
import dev.hotwire.strada.BridgeComponent
9+
import dev.hotwire.strada.BridgeDelegate
10+
import dev.hotwire.strada.Message
11+
import dev.hotwire.turbo.demo.base.NavDestination
12+
import dev.hotwire.turbo.demo.databinding.MenuComponentBottomSheetBinding
13+
import kotlinx.serialization.SerialName
14+
import kotlinx.serialization.Serializable
15+
16+
/**
17+
* Bridge component to display a native bottom sheet menu, which will
18+
* send the selected index of the tapped menu item back to the web.
19+
*/
20+
class MenuComponent(
21+
name: String,
22+
private val delegate: BridgeDelegate<NavDestination>
23+
) : BridgeComponent<NavDestination>(name, delegate) {
24+
25+
private val fragment: Fragment
26+
get() = delegate.destination.fragment
27+
28+
override fun onReceive(message: Message) {
29+
when (message.event) {
30+
"display" -> handleDisplayEvent(message)
31+
else -> Log.w("TurboDemo", "Unknown event for message: $message")
32+
}
33+
}
34+
35+
private fun handleDisplayEvent(message: Message) {
36+
val data = message.data<MessageData>() ?: return
37+
showBottomSheet(data.title, data.items)
38+
}
39+
40+
private fun showBottomSheet(title: String, items: List<Item>) {
41+
val view = fragment.view?.rootView ?: return
42+
val inflater = LayoutInflater.from(view.context)
43+
val bottomSheet = BottomSheetDialog(view.context)
44+
val binding = MenuComponentBottomSheetBinding.inflate(inflater)
45+
46+
binding.toolbar.title = title
47+
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
48+
binding.recyclerView.adapter = MenuComponentAdapter().apply {
49+
setData(items)
50+
setListener {
51+
bottomSheet.dismiss()
52+
onItemSelected(it)
53+
}
54+
}
55+
56+
bottomSheet.apply {
57+
setContentView(binding.root)
58+
show()
59+
}
60+
}
61+
62+
private fun onItemSelected(item: Item) {
63+
replyTo("display", SelectionMessageData(item.index))
64+
}
65+
66+
@Serializable
67+
data class MessageData(
68+
@SerialName("title") val title: String,
69+
@SerialName("items") val items: List<Item>
70+
)
71+
72+
@Serializable
73+
data class Item(
74+
@SerialName("title") val title: String,
75+
@SerialName("index") val index: Int
76+
)
77+
78+
@Serializable
79+
data class SelectionMessageData(
80+
@SerialName("selectedIndex") val selectedIndex: Int
81+
)
82+
}

0 commit comments

Comments
 (0)