Getting Started
David Granjon & Veerle van Leemput
2024-10-04
Source:vignettes/shinyMobile.Rmd
shinyMobile.Rmd
Introduction
shinyMobile is built on top of the Framework7 template (V8.3.3) and has different purposes:
- Develop mobile-ready Shiny applications
- Turn your Shiny application into a progressive web app (PWA)
Both with the goal of developing mobile apps that look and feel like native apps.
Classic web apps, native apps and PWAs
Classic web apps are accessed via a browser and require an internet
connection. They are built with HTML, CSS, and JavaScript. They are
cross-platform and can be accessed from any device with a browser, which
is convenient. This means they work on any mobile device! And your
shiny
app will also work perfectly fine on a mobile device.
While this sounds nice, it doesn’t give your users the most wonderful
experience: a classic Shiny web app is not optimized for mobile devices.
To name a few limitations:
- There’s no fullscreen option and the app is always displayed in a browser with the surrounding browser UI
- No consideration for touch interfaces
- Not optimized for small screens
- Can’t be used when offline
So, what about native apps? Native apps are built for a specific platform (iOS or Android) and are installed on the device. They are developed with platform-specific languages (Swift for iOS, Kotlin for Android) and are distributed via the App Store, Google Play or other stores. Native apps are fast and responsive, and they can work offline. They can also access the device’s hardware and software features (camera, GPS, etc.). However, they are expensive to develop and maintain: you need to know multiple languages and maintain multiple codebases.
Luckily, there is a middle ground: Progressive Web Apps (PWAs). PWAs are web applications that are regular web pages or websites, but can appear to the user like traditional applications or native mobile applications. They combine the best of both worlds: they can be installed on the device, provide offline features, can be launched from the home screen, and have a fullscreen display. All with just one codebase!
Of course, turning your Shiny app into a PWA doesn’t get you there completely: you also need UI components that are designed for touch interfaces and optimized for small screens- something Framework7 provides. It only makes sense to bring Framework7 and PWA capabilities to Shiny, and that’s what shinyMobile does!
Themes
shinyMobile offers 3 themes:
- ios
- md
- auto
When set to auto, it automatically detects if the app is running with Android (using Material Design, MD) or iOS and accordingly adapts the layout. It will use the MD theme for all other devices. It is of course possible to apply the iOS theme on an android device and inversely, although not recommended.
Besides these themes, shinyMobile gives you the possibility to choose between a light or dark mode, which can be set in the app options that we’ll come back to later.
Layouts
shinyMobile brings 4 out-of-the-box layouts:
-
f7SingleLayout()
: develop simple apps (best choice for iOS/android Apps). -
f7TabLayout()
: develop complex multi-tabbed apps (best choice for iOS/android Apps). -
f7SplitLayout()
: for tablets with a sidebar, navbar and a main panel -
f7MultiLayout()
: a layout consisting of multiple pages that allows to have beautiful transitions between pages to provide a more native like experience. This layout is experimental.
UI elements
With over 50 core components, shinyMobile provides a wide range of UI elements to build your app. These components are designed for mobile usage and provide a native app-like experience. They include inputs, containers, buttons, lists, modals, popups, and more. We’ll pick a few to highlight here.
Inputs: brief comparison side by side with {shiny}
shinyMobile has its own custom input widgets with unique design for each theme (iOS/android). Below we summarise all known shiny inputs and their equivalent with shinyMobile.
Features (sample) | shiny | shinyMobile |
---|---|---|
Action button | actionButton() |
f7Button() f7Fab()
|
Autocomplete | ❌ | f7AutoComplete() |
Checkbox |
checkboxInput() ,
checkboxGroupInput()
|
f7Checkbox() ,
f7CheckboxGroup()
|
Color | ❌ | f7ColorPicker() |
Date |
dateInput() ,
dateRangeInput()
|
f7DatePicker() |
Download | downloadButton() |
f7DownloadButton() |
Numeric | numericInput() |
f7Stepper() |
Radio | radioButtons() |
f7Radio() |
Range slider | sliderInput() |
f7Slider() |
Select | selectInput() |
f7Select() , f7SmartSelect() ,
f7Picker()
|
Stepper | ❌ | f7Stepper() |
Text input |
textInput() ,
textAreaInput()
|
f7Text() , f7Password() ,
f7TextArea()
|
Toggle switch | ❌ (see bslib) | f7Toggle() |
Containers
shinyMobile provides a set of containers to organize the content of your app, including:
-
f7Accordion()
: an accordion container -
f7Block()
: content block designed to add extra formatting and required spacing for text content -
f7Card()
: a card container -
f7List()
: a list container -
f7Panel()
: sidebar elements -
f7Popup()
: a popup window -
f7Sheet()
: a modal sheet -
f7Swiper()
: a swiper container (modern touch slider) -
f7Tab()
: a tab container, to be used in combination withf7Tabs()
With these containers, you can organize your content in a way that makes sense for your app. Together with the layouts, you can create a wide variety of app designs for different purposes.
Notifications & progress
There’s also a set of components available to keep your users informed:
-
f7Dialog()
: a dialog window -
f7Notif()
: a notification -
f7Preloader()
: a preloader -
f7Progressbar()
: a progress bar -
f7Toast()
: a toast notification
These components can be used to provide feedback to the user, ask for input, or display information. The look and feel of these components are unique to the chosen theme (iOS/Android).
Gallery
Curious to see a full-blown app built with shinyMobile? Check out our demo gallery! You can run the gallery with the following code:
Create your first App
Page
Every shinyMobile app starts with a
f7Page()
.
f7Page()
accepts any of the following
shinyMobile layouts: f7SingleLayout()
,
f7TabLayout()
, f7SplitLayout()
or the
experimental f7MultiLayout()
, which we will discuss further
in the Layouts section.
The options
sets up the app look and feel, and there’s
plenty of options to choose from, which we’ll discuss below.
The allowPWA
parameter allows you to add the necessary
PWA dependencies to turn your app into a PWA.
App options
This is where you can customize the global app behavior:
options <- list(
theme = c("auto", "ios", "md"),
dark = TRUE,
skeletonsOnLoad = FALSE,
preloader = FALSE,
filled = FALSE,
color = "#007aff",
touch = list(
touchClicksDistanceThreshold = 5,
tapHold = TRUE,
tapHoldDelay = 750,
tapHoldPreventClicks = TRUE,
iosTouchRipple = FALSE,
mdTouchRipple = TRUE
),
iosTranslucentBars = FALSE,
navbar = list(
iosCenterTitle = TRUE,
hideOnPageScroll = TRUE
),
toolbar = list(
hideOnPageScroll = FALSE
),
pullToRefresh = FALSE
)
The default options are all set with the help of
f7DefaultOptions()
.
As stated above, you may choose between 3 themes (md
,
ios
or auto
) and there is support for a dark
or light mode. The dark
option supports 3 values:
TRUE
, FALSE
or "auto"
. In case of
"auto"
, the default, the app will automatically switch
between dark and light mode based on the user’s system settings.
The color options simply changes the color of elements such as buttons, panel triggers, tabs triggers, and more. Note that the behaviour is different on the MD and iOS themes: in the MD theme the color gets “blended in” with the background, while in the iOS theme the color is more prominently visible in the elements. Another option to get more control over the colors in the app is using filled. It allows you to fill the navbar and toolbar with the chosen color if enabled.
hideOnPageScroll allows to hide/show the navbar and toolbar which is useful to focus on the content. The tapHold parameter ensure that the “long-press” feature is activated. preloader is useful in case you want to display a loading screen.
Framework7 has many more options which can be passed through this options parameter- so you’re not limited to the list above.
Navbar
Before we dive into the different layouts, we’ll take a look at
components necessary for a layout- starting with the navbar. The
navbar is a mandatory element of any
shinyMobile layout. It contains a title, a subtitle and
triggers (if desired) for both right and left panels
(f7Panel()
).
f7Navbar(
...,
subNavbar = NULL,
title = NULL,
hairline = TRUE,
bigger = FALSE,
transparent = FALSE,
leftPanel = FALSE,
rightPanel = FALSE
)
For complex apps, you can even add a sub-navbar with
f7SubNavbar(...)
, which may contain any element like
f7Button()
or text. f7Navbar()
exposes styling
parameters such as hairline
(a subtle border),
bigger
(size of the navbar text) and
transparent
(for a transparent navbar).
Toolbar
This is an option if you decide not to embed a
f7SubNavbar()
in the navbar, but still would like to have
additional buttons or text. The toolbar is the right place to add things
like f7Button()
, f7Link()
or
f7Badge()
. Its location is controlled with the position
parameter (either top or bottom).
Besides simply using "top"
or "bottom"
, you
can also use different positions for iOS and MD themes by using:
"top-ios"
, "top-md"
,
"bottom-ios"
, or "bottom-md"
.
Under the hood, f7Tabs()
is a custom
f7Toolbar()
.
Panels
Panels are also called sidebars, f7Panel()
being the
corresponding function.
f7Panel(
...,
id = NULL,
title = NULL,
side = c("left", "right"),
effect = c("reveal", "cover", "push", "floating"),
resizable = FALSE
)
f7Panel()
can have different behaviors and this is
controlled via the effect
argument:
- reveal makes the body content move and resize.
- cover covers the body content.
- floating_ makes the panel float over the body content.
- push pushes the body content to the side.
The resizable argument allows to dynamically resize the panel.
Note that for the moment, there is no option to control the width of
each panel. As stated previously for f7SplitLayout()
, the
f7Panel()
may also be considered as a sidebar. In that
case, we may include f7PanelMenu()
. We’ll get into more
details about the split layout at the dedicated section.
Layouts
shinyMobile offers four layouts:
f7SingleLayout()
f7TabLayout()
f7SplitLayout()
-
f7MultiLayout()
(experimental)
The layout choice is crucial when you are developing an app. It
depends on the complexity of your visualizations and content. If your
plan is to develop a simple graph or table, you should go for the
f7SingleLayout()
option. For more complex design, the best
is f7TabLayout()
. f7SplitLayout()
is specific
for tablets apps.
Single Layout
f7SingleLayout()
is dedicated to build simple, one-page
apps or gadgets.
f7SingleLayout(
...,
navbar,
toolbar = NULL,
panels = NULL
)
Only the navbar is mandatory, other components such as the toolbar
are optional for the f7SingleLayout()
.
The app below runs with specific app options:
library(shiny)
library(shinyMobile)
library(apexcharter)
library(dplyr)
library(ggplot2)
data("economics_long")
economics_long <- economics_long %>%
group_by(variable) %>%
slice((n() - 100):n())
shinyApp(
ui = f7Page(
options = list(dark = FALSE, filled = FALSE, theme = "md"),
title = "My app",
f7SingleLayout(
navbar = f7Navbar(title = "Single Layout"),
toolbar = f7Toolbar(
position = "bottom",
f7Link(label = "Link 1", href = "https://www.google.com"),
f7Link(label = "Link 2", href = "https://www.google.com")
),
# main content
f7Card(
outline = TRUE,
raised = TRUE,
divider = TRUE,
title = "Card header",
apexchartOutput("areaChart")
)
)
),
server = function(input, output) {
output$areaChart <- renderApexchart({
apex(
data = economics_long,
type = "area",
mapping = aes(
x = date,
y = value01,
fill = variable
)
) %>%
ax_yaxis(decimalsInFloat = 2) %>% # number of decimals to keep
ax_chart(stacked = TRUE) %>%
ax_yaxis(max = 4, tickAmount = 4)
})
}
)
Tab Layout
Choose this layout to develop complex multi-tabbed apps (best choice for iOS/android Apps).
f7TabLayout(
...,
navbar,
messagebar = NULL,
panels = NULL
)
The … argument requires
f7Tabs(..., id = NULL, swipeable = FALSE, animated = TRUE)
.
The id argument is mandatory if you want to exploit the
updateF7Tabs()
function. f7Tabs()
expect to
have f7Tab(..., tabName, icon = NULL, active = FALSE)
passed inside.
The app below runs with specific options:
library(shiny)
library(shinyMobile)
library(apexcharter)
poll <- data.frame(
answer = c("Yes", "No"),
n = c(254, 238)
)
shinyApp(
ui = f7Page(
options = list(dark = FALSE, filled = FALSE, theme = "md"),
title = "My app",
f7TabLayout(
panels = tagList(
f7Panel(
title = "Left Panel",
side = "left",
f7PanelMenu(
inset = TRUE,
outline = TRUE,
# Use items as tab navigation only
f7PanelItem(
tabName = "tabset-Tab1",
title = "To Tab 1",
icon = f7Icon("folder"),
active = TRUE
),
f7PanelItem(
tabName = "tabset-Tab2",
title = "To Tab 2",
icon = f7Icon("keyboard")
),
f7PanelItem(
tabName = "tabset-Tab3",
title = "To Tab 3",
icon = f7Icon("layers_alt")
)
),
effect = "floating"
),
f7Panel(
title = "Right Panel",
side = "right",
f7Block("Blabla"),
effect = "floating"
)
),
navbar = f7Navbar(
title = "Tabs Layout",
hairline = TRUE,
leftPanel = TRUE,
rightPanel = TRUE
),
f7Tabs(
animated = TRUE,
id = "tabset",
f7Tab(
title = "Tab 1",
tabName = "Tab1",
icon = f7Icon("folder"),
active = TRUE,
f7Card(
outline = TRUE,
raised = TRUE,
divider = TRUE,
title = "Card header",
apexchartOutput("pie")
)
),
f7Tab(
title = "Tab 2",
tabName = "Tab2",
icon = f7Icon("keyboard"),
f7Card(
outline = TRUE,
raised = TRUE,
divider = TRUE,
title = "Card header",
apexchartOutput("scatter")
)
),
f7Tab(
title = "Tab 3",
tabName = "Tab3",
icon = f7Icon("layers_alt"),
f7Card(
outline = TRUE,
raised = TRUE,
divider = TRUE,
title = "Card header",
f7SmartSelect(
"variable",
"Variables to show:",
c(
"Cylinders" = "cyl",
"Transmission" = "am",
"Gears" = "gear"
),
openIn = "sheet",
multiple = TRUE
),
tableOutput("data")
)
)
)
)
),
server = function(input, output, session) {
# river plot
dates <- reactive(seq.Date(Sys.Date() - 30, Sys.Date(), by = input$by))
output$pie <- renderApexchart({
apex(
data = poll,
type = "pie",
mapping = aes(x = answer, y = n)
)
})
output$scatter <- renderApexchart({
apex(
data = mtcars,
type = "scatter",
mapping = aes(
x = wt,
y = mpg,
fill = cyl
)
)
})
# datatable
output$data <- renderTable(
{
mtcars[, c("mpg", input$variable), drop = FALSE]
},
rownames = TRUE
)
}
)
Split Layout
f7SplitLayout()
is the third layout introduced with
shinyMobile, similar to sidebarLayout
with
{shiny}. This template is focused for tablet use. It is composed of a
sidebar, and a main panel.
f7SplitLayout(
...,
navbar,
sidebar,
toolbar = NULL,
panels = NULL
)
The main content goes in the … parameter. Navigation
items are gathered in the sidebar slot. This sidebar is visible at a
certain visibleBreakpoint
. By default it is set to 1024,
meaning that the sidebar will be collapsed onscreen smaller than 1024px.
This means you don’t have to worry about your split layout being opened
on a smaller mobile phone.
The sidebar
is composed of f7Panel()
with
and f7PanelMenu()
and one or more
f7PanelItem()
:
f7Panel(
title = "Sidebar",
side = "left",
effect = "push",
options = list(
visibleBreakpoint = 1024
),
f7PanelMenu(
id = "menu",
f7PanelItem(
tabName = "tab1",
title = "Tab 1",
icon = f7Icon("email"),
active = TRUE
),
f7PanelItem(
tabName = "tab2",
title = "Tab 2",
icon = f7Icon("home")
)
)
)
Two important notes:
- Do not forget to allow the
leftPanel
in the navbar withf7Navbar(leftPanel = TRUE)
! -
f7Panel()
hasside
set toleft
.
The id argument in f7PanelMenu()
is
important if you want to get the currently selected item or update the
select tab. Each f7PanelItem()
has a mandatory
tabName. The associated input will be
input$menu
in that example, with tab1
for
value since the first tab was set to an active state. To adequately link
the body and the sidebar, you must wrap the body content in
f7Items()
containing as many f7Item()
as
sidebar items. The tabName must correspond.
library(shiny)
library(ggplot2)
library(shinyMobile)
library(apexcharter)
library(thematic)
fruits <- data.frame(
name = c("Apples", "Oranges", "Bananas", "Berries"),
value = c(44, 55, 67, 83)
)
thematic_shiny(font = "auto")
new_mtcars <- reshape(
data = head(mtcars),
idvar = "model",
varying = list(c("drat", "wt")),
times = c("drat", "wt"),
direction = "long",
v.names = "value",
drop = c("mpg", "cyl", "hp", "dist", "qsec", "vs", "am", "gear", "carb")
)
shinyApp(
ui = f7Page(
title = "Split layout",
f7SplitLayout(
sidebar = f7Panel(
title = "Sidebar",
side = "left",
effect = "push",
options = list(
visibleBreakpoint = 700
),
f7PanelMenu(
id = "menu",
strong = TRUE,
f7PanelItem(
tabName = "tab1",
title = "Tab 1",
icon = f7Icon("equal_circle"),
active = TRUE
),
f7PanelItem(
tabName = "tab2",
title = "Tab 2",
icon = f7Icon("equal_circle")
),
f7PanelItem(
tabName = "tab3",
title = "Tab 3",
icon = f7Icon("equal_circle")
)
),
uiOutput("selected_tab")
),
navbar = f7Navbar(
title = "Split Layout",
hairline = FALSE,
leftPanel = TRUE
),
toolbar = f7Toolbar(
position = "bottom",
f7Link(label = "Link 1", href = "https://www.google.com"),
f7Link(label = "Link 2", href = "https://www.google.com")
),
# main content
f7Items(
f7Item(
tabName = "tab1",
f7Button("toggleSheet", "Plot parameters"),
f7Sheet(
id = "sheet1",
label = "Plot Parameters",
orientation = "bottom",
swipeToClose = TRUE,
backdrop = TRUE,
f7Slider(
"obs",
"Number of observations:",
min = 0, max = 1000,
value = 500
)
),
br(),
plotOutput("distPlot")
),
f7Item(
tabName = "tab2",
apexchartOutput("radar")
),
f7Item(
tabName = "tab3",
f7Toggle(
inputId = "plot_show",
label = "Show Plot?",
checked = TRUE
),
apexchartOutput("multi_radial")
)
)
)
),
server = function(input, output, session) {
observeEvent(input$toggleSheet, {
updateF7Sheet(id = "sheet1")
})
observeEvent(input$obs, {
if (input$obs < 500) {
f7Notif(
text = paste0(
"The slider value is only ", input$obs, ". Please
increase it"
),
icon = f7Icon("bolt_fill"),
title = "Alert",
titleRightText = Sys.Date()
)
}
})
output$radar <- renderApexchart({
apex(
data = new_mtcars,
type = "radar",
mapping = aes(
x = model,
y = value,
group = time
)
)
})
output$selected_tab <- renderUI({
HTML(paste0("Currently selected tab: ", strong(input$menu)))
})
output$distPlot <- renderPlot({
dist <- rnorm(input$obs)
hist(dist)
})
output$multi_radial <- renderApexchart({
if (input$plot_show) {
apex(data = fruits, type = "radialBar", mapping = aes(x = name, y = value))
}
})
}
)
Gadgets
shinyMobile is particularly well suited to build shiny gadgets. Gadgets are small, interactive tools that can be used as part of your data analysis workflow in R.
To convert an existing app to a gadget, wrap it in the
shiny::runGadget()
function.
library(shiny)
library(shinyMobile)
runGadget(shinyAppDir(system.file("examples/tab_layout", package = "shinyMobile")))