# Getting Started

#### David Granjon & Veerle van Leemput

#### 2024-10-04

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

### 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:

    f7Gallery()

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

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

    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:

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

    f7Page(
      options = list(
        dark = FALSE,
        filled = FALSE,
        theme = "md"
      ),
      ...
    )

Code

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

Code

    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.

Code

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