{shinyMobile}
is built on top of the Framework7 template (V8.3.3) and has
different purposes:
Both with the goal of developing mobile apps that look and feel like native apps.
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:
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!
{shinyMobile}
offers 3 themes:
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.
{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 panelf7MultiLayout()
: 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.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.
{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() |
{shinyMobile}
provides a set of containers to organize
the content of your app, including:
f7Accordion()
: an accordion containerf7Block()
: content block designed to add extra
formatting and required spacing for text contentf7Card()
: a card containerf7List()
: a list containerf7Panel()
: sidebar elementsf7Popup()
: a popup windowf7Sheet()
: a modal sheetf7Swiper()
: a swiper container (modern touch
slider)f7Tab()
: a tab container, to be used in combination
with f7Tabs()
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.
There’s also a set of components available to keep your users informed:
f7Dialog()
: a dialog windowf7Notif()
: a notificationf7Preloader()
: a preloaderf7Progressbar()
: a progress barf7Toast()
: a toast notificationThese 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).
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.
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.
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 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:
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.
{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.
f7SingleLayout()
is dedicated to build simple, one-page
apps or gadgets.
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)
})
}
)
Choose this layout to develop complex multi-tabbed apps (best choice for iOS/android Apps).
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
)
}
)
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.
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:
leftPanel
in the navbar with
f7Navbar(leftPanel = TRUE)
!f7Panel()
has side
set to
left
.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))
}
})
}
)
The layout for multiple pages is covered in a separate article.
{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.