JohnPollard-hybrid-app-RailsConf2024.pptx

JohnPollard37 341 views 63 slides May 12, 2024
Slide 1
Slide 1 of 63
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63

About This Presentation

RailsConf 2024 - Insights Gained From Developing A Hybrid Application Using Turbo-Native and Strada.


Slide Content

INSIGHTS GAINED FROM DEVELOPING A
HYBRID APPLICATION USING
TURBO-NATIVE AND STRADA

JOHN POLLARD

VP of Software Development
Buzzsprout

AD DE

Buzzcast

Episodes FanMail DynamicContent Magic Mastering Cohost Al

Upload a New Episode

wancerenews in 20 days (May 29) Upgracet

a
3 | Snapcast: Foreshadowed Podcasting Features aa
eae
Apr26,2024 You've Got Fan Mail! Announcing Buzzsprout's Text Inbox Feature 10217
‘Aor19,2024 # Snapcast: Can You Guess Buzzsprout Next Feature? 431

Apr12,2024 Top

tories From Podcast Movement Evolutions 2024 1:15:20

You can upload another 99 hours and 52 minutes of content this cycle

= m

740 a
524 o

1,320 o

INSIGHTS GAINED
FROM DEVELOPING A
HYBRID APPLICATION
USING TURBO-NATIVE

AND STRADA

SESSION GOAL

Develop your mobile application
faster than it took us

THE FIRST QUESTION
YOU MUST ANSWER

> CAN

“ PROGRAMMERS
MAKE MONEY ON
& OnlyFans?

SHOULD THE MOBILE APP HAVE
FEATURE PARITY
WITH THE WEBSITE?

BEE on
FEATURE
parry

@ Website @ Mobile

NATIVE

NATIVE

WE ARE A
SMALL TEAM.

WRITE ONCE

Write —— Test —— Build @
Write —— Test —— Build ©)

Native

— > Build @
——_> Build (4)

Hybrid Write ———> Test

Tick (Time &
Budget Tracking)

Productivity

ED a

4+ 4

Years Old Productivity

What's New Version History
Version 2.4 6y ago
This app has been updated by Apple to display
the Apple Watch app icon.

-Updated for iPhone X
-Added left & right swipe support

Buzzsprout
® Track & manage
NO your podcast
®

TestFlight version 1.7 is installed
View in TestFlight

M
4.9 a+ 4
IO os Years Old Productivity Buz

What's New Version History
Version 1.6 mo ago

You can now easily refer your friends to
Buzzsprout right from the app.

Preview

THIS IS ALL POSSIBLE
BECAUSE OF...

hetwire es cn Fans vom wre sony wer Gon) …

© verni dei

=.

Why choose Hotwire?

Savebigwith @Hot Rates 4 Over $2 lion ona
nite, 2 y

Hotel Tonight

Jacksonville, FL Jacksonville, FL Jacksonville, FL

$121 mer OL 112 #109 rex

Jacksonville, FL

HOTW/RE

HTML OVER THE WIRE

fin STRADA

( ‘TURBO Z STIMULUS

HOW SHOULD
USERS LOG IN?

+ OAuth

WAYS T0 + Token Based (JWT)
LOGIN + mike

+ Cookie Based

WHAT ITEMS TO PUT IN THE
NAVIGATION MENU?

DESKTOP MENUS

Buzzcast

Hacker News new | past | comments | ask | show | jobs | submit

9 ITEMS MAX

MUST CONDENSE

Buzzcast
Players Website Director Monetization
Fantail Cohost Al

Episodes DynamicContent Magic Mastering

a > q

Home Stats Episodes More

MAGIC E

sea wee
D.
2 Buzzsprout Ads o
2 ©
Roms Spies
e s
Dane Fame
5
Bor raed
Podcast
con

503

Advanced

Hosts

Por

My Profile

& Hop

© Privacy Policy

D Terms of Service

EACH NAVIGATION TAB IS ITS
OWN TURBO SESSION WITH ITS
OWN NAVIGATION HISTORY.

THINK OF EACH NAVIGATION TAB
AS A BROWSER TAB.

INDEPENDENT
HISTORY

HOW TO SET UP THE NATIGATION BAR

+ Path configuration file
e server hosted file for Turbo native
e https://github.com/hotwired/turbo-
ios/blob/main/Docs/PathConfiguration.md

o API
© server response
e can be dynamic

+ Hardcoded

© written in Kotlin/Swift

PATH CONFIGURATION

"settings": {
"tabs": [

"ios _system.. image_name": "house"

"title": "Conversations",
"path": "/conversation
"jos_system_image_name":
},
{

"title": "Notifications",
"path": "/notifications",
"jos_system_image_name": "bell"

"envelope"

"tato!

"navbar. tabs

{

tort

url

text

mn

API CONFIGURATION

Lfpack Digits
ut

wa, buzzoprout.oon/raile/acLive-stora

home",
tps: //www.burzsprout .con/2325279/dashboarc"

nttpe: Flu. buzzeprout.con/2825279 /etate

08 /2325279/nots fications”

nttps: //www.buzzsprout .con/2325279/nenu!

e/wol fpuck-digital-proview.

Pe

TRAVERSING WITH
STACKED NAVIGATION

HTTP

Stateless

Navigation can be decided
during request

Can defer navigation
decisions using redirects

Don't care where request
came from

NATIVE

Stateful

Navigation must be decided
at time of request

Can't use redirects to decide
how to present next screen

Might have to unwind
navigation

MOBILE VS WEB

NAVIGATION
HELPERS

https:/sithub.com/hotwired/turbo-
rails/blob/ man /app/controllers/turbo/native/navigation.ro

included do
helper_method :turbo_native_app?
end

# Turbo Native applications are identified by having the string "Turbo Native" as part
def turbo_native_app?

request.user_agent. to_s.match?(/Turbo Native/)
end

# Tell the Turbo Native app to dismiss a modal (if presented) or pop a screen off of th
def recede_or_redirect_to(url, options)

turbo_native_action_or_redirect url, :recede, :to, options
end

# Tell the Turbo Native app to ignore this navigation, otherwise redirect to the given
def resume_or_redirect_to(url, *soptions)

turbo_native_action_or_redirect url, :resume, :to, options
end

# Tell the Turbo Native app to refresh the current screen, otherwise redirect to the gi
def refresh_or_redirect_to(url, *xoptions)

turbo_native_action_or_redirect url, :refresh, :to, options
end

EXAMPLE

class Admin::TextMessagesController < Admin::BaseController
before_action :get_text_message

def mark_as_read
@text_message.update! (status: :read)
recede_or_redirect_to(text_messages_path)
end

private

def get_text_message
Qtext_message = @podcast.text_messages.find(params[:id])
end
end

EXAMPLE
IN ACTION

COMPLEX
EXAMPLE

PAGE CACHED

@ tite Empowerment

Buzzsprout A

Dynamic Content

Fan Mail

Podcast info

Refera Friend

PAGE CACHED <%= turbo_exempts_page_from_cache %>

PATH
CONFIGURATION

{

+

settings": { },

"rules": [
{

"patterns": [
"/neu$",
"Jedit$",
"/edit_advanced$",
"/podcast/support$",
"/my/profile$"

]

]

"properties": {
"presentation": "modal"
}

iy

Add variant to tailwind.config.js

module.exports = {
plugins: [

require('@tailwindess/forms'),

DISPLAYING addVariant('native', ({ modifySelectors, separator }) = {
modifySelectors(({ className }) = {
DIFFERENT return ‘.is-native .${e(*nativeS{separator}s{className}*)}*;
DE
CONTENT »
1

<div class="hative:hidden">
<label tebindex="@" for="unessigned.file" class="btn btn_small dynamic-libraryadd">Uploe
</div>

2. Remove it.

<% if !turbo_native_app? %>

<% end %>

DISPLAYING + sevescrie-snorue tag ‘native.nin’ 37 umo ottue app? +
DIFFERENT
CONTENT

3. Asynchronously load it.

<div data-controller="content-loader"
data-content-loader-url-value="/messages .html"></div>

"webcredentials": {
"apps": ["com.buzzsprout.app"]

"details": [
E
“appIDs": ["com.buzzsprout.app"],
Moa E

DEEP | "7": "/tuilio/*",
LINKS cui,

g
U/Mz "/R/text.message*",
"include": true

https://developer.android.com/training/app-links }

https://developer.apple.com/documentation/xcode/supporting-associated-domains

BUILDING COMPONENTS
STIMULUS & STRADA

jother kule podcast

Another kule pode:

Episode Downloads

189

import { BridgeCoaponent } from "fhotutrod/strede"

export default class extends BridgeComponent {
static conponent = "page"
static targets = ['title']

titleTargetConnected(elenent) {

this. titleELenent nt

£his.obsorver = this.#createlntersectiondbserver()
this.obsorver.observe(this,titleElenent)

3

titleTargetDiscomected() £
this.obsorver.discomest()

3

PAGE TITLE

MupdateTopNav(options = { title:
this.send('update", options)
i

, show-nav_shadow: false }) {

Bere:

elntersectionübserver() {
return new Intersectionübserver((entries) = {
this. scrolledDutofView = lentries[0].isIntersecting
this. #updateToplav({ title: this.#scrolledTitle,
shou-nav-shadou: this,scrolledDutofview })
}, € roctMargin: “@px @px 56% @px" })
}

get #scrolleutitle() {
return this.scrolledDut0fView ?

app/javascript/native/page_component.js 2

this.titleEloment.textContent : "*

PAGE TITLE

<body <%= "data-controller=native--page" if turbo_native_app? %>>
<h1 data-native--page-target="title">Fan Mail</h1>
<body>

Another kule podcast

Another kule podcas

Episode Downloads

PAGE MENU

import { BridgeComponent } from "Rhotwired/strada"

import { NativeElement } from "../../lib/strade/native_element. js"

export default class extends BridgeComponent {
static component = "menu"
static targets = ['item"]
static values = { actionSheet: Boolean }

connect() {
super.conneotQ)
const items = Nativeflement.makeMenultens(this.itenTargets)

this.send("onLoad", { items: itens, kind: this.kind }, message = {
message. date. selectedindex
neu NativeElenent (this. itenTargets[selectedIndex])

const selectedindex
const nativeElement

nativeElement.click()
»

get kind() {
return this.actionSheetValue ?

ction-sheet" : "menu"

app/javascript/native/nav_menu_component.js

PAGE MENU

native--nav-menu">

<div data-edit-menu-container date-controller:
<&= link-to "Share", "4", data: { native--nav_menu-target: "item",
native icon-name: "square.and.arrow.up",
controller: "native--share",
action: "click->native--share#onClick",
native__share_url_value: episode_canonical_url } %>
<%= link-to "Edit", edit-episode-url , date: { native--nav-menu-target: "item", native-icon-name: "pencil" } %>
link-to "Change Date", edit-episode_publish.date_path, data: { native_nav_menu_target: "item",
native_icon-name: "calendar" } %>
<%= link-to "Unpublish", update-publish-status_episode_path, method: :put,
remote: true,
data: { native _nav_menu_target:
netive-icon-name: “calendar.badge.minus"
action: "ajax:success>native--nav-menuttconnect" } %>

item",

<% if current_role.admin.or_ovner? %>
<K= buttonto "Delete", episode-path, method: :delete, form: { cless: "inline-block" },
data: { native__nav_menu_target: "item",
“destructive”, } %>

native_menu-iten-styl

<% end %>
</div>

ACTION
SHEET

import { BridgeComponent } from
import { NativeElement } from".

hotwired/strada"
/.. [Lib/strada/native_elenent.js"

export default class extends BridgeComponent {
static component = “action-shest"
static targets = ["iten"]

shou(event) {
event. stopInmediatePropagation()
event. preventDefault()

this.send("onClick", this.#messagedeta, message > {
ACTION a
const nativeElement = new NativeElement(this. itenTargets[selectedindex])

SHEET ——

»
3

get #messageData() {
return {
title: this.deta.get("title"),
items: NetiveElenent.makeMenultens(this. itenTargats)

app/javascript/native/action_sheet_component.js

ACTION SHEET

<div data-controller="native--action-sheet">
<%= link-to 'Edit', edit-role-path(edit-role), data: { native_-action-sheet_target: "item" } %>
<%= button_to "Remove?", role-path(edit-role), method: :delete
data: { native--action-sheet-target: "item", native_menu-iten-style: "destructive"} %>

</div>

Podcast Downloads

a

SHARING o
IMAGES

Episodes Published

1U Ways LU BeCUIIIG a Vell
news commentator

SHARING Re ne
TEXT

SHARING

import { BridgeComponent } from “Qhotwired/strade"

export default class extends BridgeComponent {
static conponent = "share"
static values = { title: String, url: String, text: String, image: String, video: String }

ontlick(ovent) {
ovont.stoplemodiatoPropagation()
event..preventDefaul ()
this.send(“onClick", { title: this.titlevalue, url: this.urlValue, text: this.textvalue, im:
}
}

+ this.imageValue, video: this.videoValue })

app/javascript/native/sharing_component,js

<button classa"nicden native:iniine-olock" <button data-contreLler="native--share"

data-controller date-action="click->native--share#onClick"

share
<foutton>

</button>

APP REVIEWS

Can prompt at any time, but Apple & Google prefer
users are seen a request to review after a positive
interaction.

Apple limits the review prompt to a user a
maximum of three times within a 365-day period.

Google Play enforces a time-bound quota on how
often a user can be shown the review dialog.

Ifyou don't ask for reviews, most of your ratings
will be bad reviews.

SUBMITTING THE APP

IF AT FIRST YOU DON'T SUCCEED

TRY, & TRY, &
TRY, & TRY
AGAIN

WHAT WE LEARNED?

El What big upfront decisions you a What path configuration file
must make and how it affects does and what routes are
developement provided by Turbo Native

a How to show & hide content for Aa How to write native
the native app and desktop components with Stimulus and

Strada
El How mobile navigation works

especially with Turbo Native a The joys of submitting an app to
Apple

QUESTIONS?

[email protected]

twitter: @johnlpollard
Tags