Getting Started

David Granjon & Veerle van Leemput

2024-05-27

Introduction

{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, 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:



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:

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:

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 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.

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).

Create your first App

Page

Every {shinyMobile} app starts with a f7Page().

f7Page(
  ...,
  options = list(...),
  title = NULL,
  allowPWA = FALSE
)

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.

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).

f7Toolbar(
  ...,
  position = c("top", "bottom"),
  icons = FALSE,
  scrollable = FALSE
)

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:


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:


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:

f7Page(
  options = list(
    dark = FALSE,
    filled = FALSE,
    theme = "md"
  ),
  ...
)
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:

f7Page(
  options = list(
    dark = FALSE,
    filled = FALSE,
    theme = "md"
  ),
  ...
)
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 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))
      }
    })
  }
)

Multi Layout

The layout for multiple pages is covered in a separate article.

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")))