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.
Code
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 heref7Navbar(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 contenttags$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 heref7Navbar( 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 heref7Navbar( 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(),# Pagespage_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.
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:
In practices, you can design a supercharged f7Text() like so:
Code
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.
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)})
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().
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:
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.