library(shiny)
shinyMobile has been enabling the creation of exceptional R Shiny apps for both iOS and Android for nearly five years, thanks to the impressive open-source Framework7 template that drives its capabilities.
This year shinyMobile gets a major update to v2.2.0. I’d like to warmly thank Veerle van Leemput and Michael S. Czahor from AthlyticZ for providing significant support during this marathon.
What’s new
shinyMobile 1.0.0 and above have been running on an old version of Framework7 (v5). shinyMobile 2.0.0 has been upgraded to run the newer Framework7 v8. With this comes a significant number of changes, but we believe these are all for the best!
Major changes
New multi pages experimental support
We are very excited to bring this feature out for this new release.
Under the hood, this is possible owing to the {brochure}
package from Colin
Fay as well as the internal Framework7
router component.
What does this mean?
You can now develop real multi pages Shiny applications and have
different url endpoints and redirections. For instance,
https://my-app/home
can be the home page while
https://my-app/settings
brings to the settings page.
How does this work?
At the time of writting of this blog post, you must install a patched
{brochure}
version with
devtools::install_github("DivadNojnarg/brochure")
.
In the below code, we basically have 3 pages having their own content and a common layout for consistency. The router ensure beautiful transitions from one page to another. We invite you to look at the getting started article which provides more technical details.
library(shiny)
# Needs a specific version of brochure for now.
# This allows to pass wrapper functions with options
# as list. We need it because of the f7Page options parameter
# and to pass the routes list object for JS.
# devtools::install_github("DivadNojnarg/brochure")
library(brochure)
library(shinyMobile)
# Allows to use the app on a server like
# shinyapps.io where basepath is /app_name
# instead of "/" or "".
make_link <- function(path = NULL, basepath = "") {
if (is.null(path)) {
if (nchar(basepath) > 0) {
return(basepath)
} else {
return("/")
}
}
sprintf("%s/%s", basepath, path)
}
links <- lapply(2:3, function(i) {
tags$li(
f7Link(
routable = TRUE,
label = sprintf("Link to page %s", i),
href = make_link(i)
)
)
})
page_1 <- function() {
page(
href = "/",
ui = function(request) {
shiny::tags$div(
class = "page",
# top navbar goes here
f7Navbar(title = "Home page"),
# NOTE: when the main toolbar is enabled in
# f7MultiLayout, we can't use individual page toolbars.
# f7Toolbar(
# position = "bottom",
# tags$a(
# href = "/2",
# "Second page",
# class = "link"
# )
# ),
# Page content
tags$div(
class = "page-content",
f7List(
inset = TRUE,
strong = TRUE,
outline = TRUE,
dividers = TRUE,
mode = "links",
links
),
f7Block(
f7Text("text", "Text input", "default"),
f7Select("select", "Select", colnames(mtcars)),
textOutput("res"),
textOutput("res2")
)
)
)
}
)
}
page_2 <- function() {
page(
href = "/2",
ui = function(request) {
shiny::tags$div(
class = "page",
# top navbar goes here
f7Navbar(
title = "Second page",
# Allows to go back to main
leftPanel = tagList(
tags$a(
href = make_link(),
class = "link back",
tags$i(class = "icon icon-back"),
tags$span(
class = "if-not-md",
"Back"
)
)
)
),
shiny::tags$div(
class = "page-content",
f7Block(f7Button(inputId = "update", label = "Update stepper")),
f7List(
strong = TRUE,
inset = TRUE,
outline = FALSE,
f7Stepper(
inputId = "stepper",
label = "My stepper",
min = 0,
max = 10,
size = "small",
value = 4,
wraps = TRUE,
autorepeat = TRUE,
rounded = FALSE,
raised = FALSE,
manual = FALSE
)
),
f7Block(textOutput("test"))
)
)
}
)
}
page_3 <- function() {
page(
href = "/3",
ui = function(request) {
shiny::tags$div(
class = "page",
# top navbar goes here
f7Navbar(
title = "Third page",
# Allows to go back to main
leftPanel = tagList(
tags$a(
href = make_link(),
class = "link back",
tags$i(class = "icon icon-back"),
tags$span(
class = "if-not-md",
"Back"
)
)
)
),
shiny::tags$div(
class = "page-content",
f7Block("Nothing to show yet ...")
)
)
}
)
}
brochureApp(
basepath = make_link(),
# Pages
page_1(),
page_2(),
page_3(),
# Important: in theory brochure makes
# each page having its own shiny session/ server function.
# That's not what we want here so we'll have
# a global server function.
server = function(input, output, session) {
output$res <- renderText(input$text)
output$res2 <- renderText(input$select)
output$test <- renderText(input$stepper)
observeEvent(input$update, {
updateF7Stepper(
inputId = "stepper",
value = 0.1,
step = 0.01,
size = "large",
min = 0,
max = 1,
wraps = FALSE,
autorepeat = FALSE,
rounded = TRUE,
raised = TRUE,
color = "pink",
manual = TRUE,
decimalPoint = 2
)
})
},
wrapped = f7MultiLayout,
wrapped_options = list(
basepath = make_link(),
# Common toolbar
toolbar = f7Toolbar(
f7Link(icon = f7Icon("house"), href = make_link(), routable = TRUE)
),
options = list(
dark = TRUE,
theme = "md",
routes = list(
# Important: don't remove keepAlive
# for pages as this allows
# to save the input state when switching
# between pages. If FALSE, each time a page is
# changed, inputs are reset.
list(
path = make_link(),
url = make_link(),
name = "home",
keepAlive = TRUE
),
list(
path = make_link("2"),
url = make_link("2"),
name = "2",
keepAlive = TRUE
),
list(
path = make_link("3"),
url = make_link("3"),
name = "3",
keepAlive = TRUE
)
)
)
)
)
Updated material design style
By updating to the latest Framework7 version, we now benefit from a totally revamped Android (md) design, which looks more modern.
library(shiny)
library(shinyMobile)
shinyAppDir(system.file("examples/gallery", package = "shinyMobile"))
Refined inputs layout and style
Whenever you have multiple inputs, we now recommend to wrap all of them
within f7List()
so as to benefit from new styling options such as
outline, inset, strong. Internally, we use a function able to detect
whether the input is inside a f7List()
. If this is the case, you can
style this list by passing parameters like
f7List(outline = TRUE, inset = TRUE, ...)
. If not, the input is
internally wrapped in a list to have correct rendering, but no styling
is possible. Besides, some inputs like f7Text()
can have custom
styling (add an icon, clear button, outline style), which is independent
from the external list wrapper style. Hence, we don’t recommend doing
f7List(outline = TRUE, f7Text(outline = TRUE))
since it won’t render
well and instead use f7List(outline = TRUE, f7Text())
.
Besides, independently from f7List()
, some inputs having more specific
styling options:
-
f7AutoComplete()
. -
f7Text()
,f7Password()
,f7TextArea()
. -
f7Select()
. -
f7Picker()
,f7ColorPicker()
andf7DatePicker()
. -
f7Radio()
andf7CheckboxGroup()
.
In practices, you can design a supercharged f7Text()
like so:
f7Text(
inputId = "text",
label = "Text input",
value = "Some text",
placeholder = "Your text here",
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
)
This adds a description to the input (below its main content), as well
as the outline style option and an icon on the left side. clearable
is
TRUE by default meaning that all text-based inputs can be cleared.
floating
is an effect that makes the label move in and out the input
area depending on the content state. When empty, the label is inside and
when there is text, the label is pushed outside into its usual location.
f7Stepper()
and f7Toggle()
label is now displayed on the left.
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
options = list(dark = FALSE, theme = "ios"),
title = "Inputs Layout",
f7SingleLayout(
navbar = f7Navbar(
title = "Inputs Layout",
hairline = FALSE
),
f7List(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE,
f7Text(
inputId = "text",
label = "Text input",
value = "Some text",
placeholder = "Your text here"
),
f7TextArea(
inputId = "textarea",
label = "Text area input",
value = "Some text",
placeholder = "Your text here"
),
f7Select(
inputId = "select",
label = "Make a choice",
choices = 1:3,
selected = 1
),
f7AutoComplete(
inputId = "myautocomplete",
placeholder = "Some text here!",
openIn = "dropdown",
label = "Type a fruit name",
choices = c(
"Apple",
"Apricot",
"Avocado",
"Banana",
"Melon",
"Orange",
"Peach",
"Pear",
"Pineapple"
)
),
f7Stepper(
inputId = "stepper",
label = "My stepper",
min = 0,
color = "default",
max = 10,
value = 4
),
f7Toggle(
inputId = "toggle",
label = "Toggle me"
),
f7Picker(
inputId = "picker",
placeholder = "Some text here!",
label = "Picker Input",
choices = c("a", "b", "c"),
options = list(sheetPush = TRUE)
),
f7DatePicker(
inputId = "date",
label = "Pick a date",
value = Sys.Date()
),
f7ColorPicker(
inputId = "mycolorpicker",
placeholder = "Some text here!",
label = "Select a color"
)
),
f7CheckboxGroup(
inputId = "checkbox",
label = "Checkbox group",
choices = c("a", "b", "c"),
selected = "a",
style = list(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE
)
),
f7Radio(
inputId = "radio",
label = "Radio group",
choices = c("a", "b", "c"),
selected = "a",
style = list(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE
)
)
)
),
server = function(input, output) {
}
)
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
options = list(dark = FALSE, theme = "ios"),
title = "Inputs Layout",
f7SingleLayout(
navbar = f7Navbar(
title = "Inputs Layout",
hairline = FALSE
),
f7List(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE,
f7Text(
inputId = "text",
label = "Text input",
value = "Some text",
placeholder = "Your text here"
),
f7TextArea(
inputId = "textarea",
label = "Text area input",
value = "Some text",
placeholder = "Your text here"
),
f7Select(
inputId = "select",
label = "Make a choice",
choices = 1:3,
selected = 1
),
f7AutoComplete(
inputId = "myautocomplete",
placeholder = "Some text here!",
openIn = "dropdown",
label = "Type a fruit name",
choices = c(
"Apple",
"Apricot",
"Avocado",
"Banana",
"Melon",
"Orange",
"Peach",
"Pear",
"Pineapple"
)
),
f7Stepper(
inputId = "stepper",
label = "My stepper",
min = 0,
color = "default",
max = 10,
value = 4
),
f7Toggle(
inputId = "toggle",
label = "Toggle me"
),
f7Picker(
inputId = "picker",
placeholder = "Some text here!",
label = "Picker Input",
choices = c("a", "b", "c"),
options = list(sheetPush = TRUE)
),
f7DatePicker(
inputId = "date",
label = "Pick a date",
value = Sys.Date()
),
f7ColorPicker(
inputId = "mycolorpicker",
placeholder = "Some text here!",
label = "Select a color"
)
),
f7CheckboxGroup(
inputId = "checkbox",
label = "Checkbox group",
choices = c("a", "b", "c"),
selected = "a",
style = list(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE
)
),
f7Radio(
inputId = "radio",
label = "Radio group",
choices = c("a", "b", "c"),
selected = "a",
style = list(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE
)
)
)
),
server = function(input, output) {
}
)
Moreover, we added a new way to pass options to f7Radio()
and
f7CheckboxGroup()
, namely f7CheckboxChoice()
and f7RadioChoice()
(note: you can’t use update_*
functions on them yet), so that you can
pass more metadata and a description to each option (instead of just the
choice name in basic shiny inputs):
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
title = "Update radio",
f7SingleLayout(
navbar = f7Navbar(title = "Update f7Radio"),
f7Block(
f7Radio(
inputId = "radio",
label = "Custom choices",
choices = list(
f7RadioChoice(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nulla sagittis tellus ut turpis condimentum,
ut dignissim lacus tincidunt",
title = "Choice 1",
subtitle = "David",
after = "March 16, 2024"
),
f7RadioChoice(
"Cras dolor metus, ultrices condimentum sodales sit
amet, pharetra sodales eros. Phasellus vel felis tellus.
Mauris rutrum ligula nec dapibus feugiat",
title = "Choice 2",
subtitle = "Veerle",
after = "March 17, 2024"
)
),
selected = 2,
style = list(
outline = TRUE,
strong = TRUE,
inset = TRUE,
dividers = TRUE
)
),
textOutput("res")
)
)
),
server = function(input, output, session) {
output$res <- renderText(input$radio)
}
)
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
title = "Update radio",
f7SingleLayout(
navbar = f7Navbar(title = "Update f7Radio"),
f7Block(
f7Radio(
inputId = "radio",
label = "Custom choices",
choices = list(
f7RadioChoice(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nulla sagittis tellus ut turpis condimentum,
ut dignissim lacus tincidunt",
title = "Choice 1",
subtitle = "David",
after = "March 16, 2024"
),
f7RadioChoice(
"Cras dolor metus, ultrices condimentum sodales sit
amet, pharetra sodales eros. Phasellus vel felis tellus.
Mauris rutrum ligula nec dapibus feugiat",
title = "Choice 2",
subtitle = "Veerle",
after = "March 17, 2024"
)
),
selected = 2,
style = list(
outline = TRUE,
strong = TRUE,
inset = TRUE,
dividers = TRUE
)
),
textOutput("res")
)
)
),
server = function(input, output, session) {
output$res <- renderText(input$radio)
}
)
New f7Treeview()
component
The new release welcomes a brand new input widget. As its name suggests,
f7Treewiew()
enables sorting items hierarchically within a collapsible
nested list of items. This is ideal, for instance, to select files
within multiple folders, as an alternative to the classic fileInput()
.
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
title = "My app",
f7SingleLayout(
navbar = f7Navbar(title = "f7Treeview"),
# group treeview with selectable items
f7BlockTitle("Selectable items"),
f7Block(
f7Treeview(
id = "treeview1",
selectable = TRUE,
f7TreeviewGroup(
title = "Selected images",
icon = f7Icon("folder_fill"),
itemToggle = TRUE,
lapply(
1:3,
function(i) {
f7TreeviewItem(
label = paste0("image", i, ".png"),
icon = f7Icon("photo_fill")
)
}
)
)
),
textOutput("selected")
),
# group treeview with checkbox items
f7BlockTitle("Checkbox"),
f7Block(
f7Treeview(
id = "treeview2",
withCheckbox = TRUE,
f7TreeviewGroup(
title = "Selected images",
icon = f7Icon("folder_fill"),
itemToggle = TRUE,
lapply(
1:3,
function(i) {
f7TreeviewItem(
label = paste0("image", i, ".png"),
icon = f7Icon("photo_fill")
)
}
)
)
),
textOutput("selected2")
)
)
),
server = function(input, output) {
output$selected <- renderText(input$treeview1)
output$selected2 <- renderText(input$treeview2)
}
)
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
title = "My app",
f7SingleLayout(
navbar = f7Navbar(title = "f7Treeview"),
# group treeview with selectable items
f7BlockTitle("Selectable items"),
f7Block(
f7Treeview(
id = "treeview1",
selectable = TRUE,
f7TreeviewGroup(
title = "Selected images",
icon = f7Icon("folder_fill"),
itemToggle = TRUE,
lapply(
1:3,
function(i) {
f7TreeviewItem(
label = paste0("image", i, ".png"),
icon = f7Icon("photo_fill")
)
}
)
)
),
textOutput("selected")
),
# group treeview with checkbox items
f7BlockTitle("Checkbox"),
f7Block(
f7Treeview(
id = "treeview2",
withCheckbox = TRUE,
f7TreeviewGroup(
title = "Selected images",
icon = f7Icon("folder_fill"),
itemToggle = TRUE,
lapply(
1:3,
function(i) {
f7TreeviewItem(
label = paste0("image", i, ".png"),
icon = f7Icon("photo_fill")
)
}
)
)
),
textOutput("selected2")
)
)
),
server = function(input, output) {
output$selected <- renderText(input$treeview1)
output$selected2 <- renderText(input$treeview2)
}
)
New f7Form()
Shiny does not provide HTML forms handling out of the box (a
form being composed of
multiple input elements). That’s why we introduce f7Form()
. Contrary
to basic shiny inputs, we don’t get one input value per element but a
single input value with a nested list for all inputs within the form,
thereby allowing a reduction in the number of inputs on the server side.
updateF7Form()
can quickly update any input from the form. As a side
note, the current list of supported inputs is:
f7Text()
f7TextArea()
f7Password()
f7Select()
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
f7SingleLayout(
navbar = f7Navbar(title = "Inputs form"),
f7Block(f7Button("update", "Click me")),
f7BlockTitle("A list of inputs in a form"),
f7List(
inset = TRUE,
dividers = FALSE,
strong = TRUE,
f7Form(
id = "myform",
f7Text(
inputId = "text",
label = "Text input",
value = "Some text",
placeholder = "Your text here",
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
),
f7TextArea(
inputId = "textarea",
label = "Text Area",
value = "Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua",
placeholder = "Your text here",
resize = TRUE,
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
),
f7Password(
inputId = "password",
label = "Password:",
placeholder = "Your password here",
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
)
)
),
verbatimTextOutput("form")
)
),
server = function(input, output, session) {
output$form <- renderPrint(input$myform)
observeEvent(input$update, {
updateF7Form(
"myform",
data = list(
"text" = "New text",
"textarea" = "New text area",
"password" = "New password"
)
)
})
}
)
library(shiny)
library(shinyMobile)
shinyApp(
ui = f7Page(
f7SingleLayout(
navbar = f7Navbar(title = "Inputs form"),
f7Block(f7Button("update", "Click me")),
f7BlockTitle("A list of inputs in a form"),
f7List(
inset = TRUE,
dividers = FALSE,
strong = TRUE,
f7Form(
id = "myform",
f7Text(
inputId = "text",
label = "Text input",
value = "Some text",
placeholder = "Your text here",
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
),
f7TextArea(
inputId = "textarea",
label = "Text Area",
value = "Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua",
placeholder = "Your text here",
resize = TRUE,
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
),
f7Password(
inputId = "password",
label = "Password:",
placeholder = "Your password here",
style = list(
description = "A cool text input",
outline = TRUE,
media = f7Icon("house"),
clearable = TRUE,
floating = TRUE
)
)
)
),
verbatimTextOutput("form")
)
),
server = function(input, output, session) {
output$form <- renderPrint(input$myform)
observeEvent(input$update, {
updateF7Form(
"myform",
data = list(
"text" = "New text",
"textarea" = "New text area",
"password" = "New password"
)
)
})
}
)
Breaking changes
Some components have disappeared from Framework7 and we had to deprecate
them as they no longer work. Other long time deprecated {shinyMobile}
elements are also removed to cleanup the codebase. We invite you to
review the
changelog
to see a list of all changes in this release.
Soft deprecation
Some function parameters have changed and are now deprecated with
{lifecycle}
. You’ll see a warning message if you use them and we
invite you to accordingly update your code.
Conclusion
Since the release of {shiny}
in 2012, many packages have been released
that substantially improve its layout and design such as {bslib}
,
{bs4Dash}
or {shiny.fluent}
. Yet not much was done for mobile
development. {shinyMobile}
tries to fill this gap by exposing people
to the rich Framework7 mobile-first template. Coupled with progressive
web app support (PWA), you can run Shiny apps on a mobile that look very
close to native apps with a desktop icon, a launch screen and running
fullscreen, that is without the web browser navigation bar. By including
an experimental implementation of the multipage navigation as described
earlier, we move one step closer to the native apps. Finally, owing to
the progress on the webR side,
it isn’t impossible that one day, we might run a totally offline Shiny
app on mobile.