1  Whole app game

Published

2024-12-20

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '01')

Launch apps with launch()

launch(app = '01_whole-app-game')

Download apps with get_app()

get_app(app = '01_whole-app-game')

This chapter is modeled on the Whole Game chapter in R Packages, 2ed.1 We’ll go through the development of the monthAppPkg Shiny app-package (adapted from Mastering Shiny).2

1.1 A toy app-package

We will briefly discuss creating an R package with a Shiny application. Each topic will be explained in detail in the next chapters. In the end, you will have a Shiny application with all the features and functions of an R package.

1.2 Package metadata

Every R package requires a DESCRIPTION file. You can quickly create one using usethis::use_description().3

usethis::use_description(
  fields = list(
    Package = 'monthAppPkg',
    Title = 'An example app-pkg',
    Version = '0.0.0.9000',
    Description = 'A shiny application built inside an R package.',
    "Authors@R" = NULL,
    Author = utils::person(
      given = "Jane",
      family = "Doe",
      role = c("aut", "cre")
    ),
    Maintainer = utils::person(
      given = "Jane",
      family = "Doe",
      email = "Jane.Doeh@email.io"
    ),
    License = "GPL-3"
  )
)

The values above in the fields list avoids the boilerplate content from use_description().4

The essential seven fields are shown below:5

Package: monthAppPkg
Title: An example app-pkg
Version: 0.0.0.9000
Author: Jane Doe [aut, cre]
Maintainer: Jane Doe <Jane.Doeh@email.io>
Description: A shiny application built inside an R package.
License: GPL-3

You will get specific fields automatically for function documentation and dependency management.6

Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3

1.3 Data

To include the birthstones.csv data in monthAppPkg, we’ll create a data-raw/ folder with usethis::use_data_raw("stones"). Next, we’ll move the birthstones.csv file into data-raw/, load it into the Global Environment, and an R package data object witho usethis::use_data():

usethis::use_data_raw("stones")
✔ Setting active project to '/path/to/monthAppPkg'
✔ Creating 'data-raw/'
✔ Adding '^data-raw$' to '.Rbuildignore'
✔ Writing 'data-raw/stones.R'
• Modify 'data-raw/stones.R'
• Finish the data preparation script in 'data-raw/stones.R'
• Use `usethis::use_data()` to add prepared data to package

Move birthstones.csv to data-raw/birthstones.csv:

fs::file_move(path = "birthstones.csv", new_path = "data-raw/birthstones.csv")

Contents of data-raw/stones.R:

## code to prepare `stones` dataset goes here
library(vroom)
stones <- vroom::vroom("data-raw/birthstones.csv")
usethis::use_data(stones, overwrite = TRUE)
Rows: 12 Columns: 2                                                                                                                                
── Column specification ──────────────────────────────────────────────────
Delimiter: ","
chr (2): month, stone

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
usethis::use_data(stones)
✔ Adding 'R' to Depends field in DESCRIPTION
✔ Setting LazyData to 'true' in 'DESCRIPTION'
✔ Saving 'stones' to 'data/stones.rda'
• Document your data (see 'https://r-pkgs.org/data.html')

Data should be documented using roxygen2 which we’ll cover in the data chapter.7

1.4 Dependencies

Every Shiny app-package depends on the shiny package. usethis::use_package()8 adds it under the Imports field the DESCRIPTION file.

usethis::use_package("shiny")
✔ Adding 'shiny' to Imports field in DESCRIPTION
• Refer to functions with `shiny::fun()`

We’re advised to use explicit namespacing (i.e., pkg::fun()), but we can avoid this by importing all Shiny’s functions into our package namespace using the @import tag from roxygen2.9

1.5 Write code

Create new .R files under R/ using use_r():

usethis::use_r("monthFeedback")
✔ Setting active project to '/projects/apps/monthAppPkg'
• Modify 'R/monthFeedback.R'

Both UI and server module functions are stored in R/monthFeedback.R and R/birthstone.R. Tests should also be created for each function.

1.6 Loading

Shiny app development typically involves something like the following workflow:

  1. Write UI/server code
  2. Click Run App
  3. Rinse, repeat

When making the switch from app development to app-package development, calling load_all() is somewhat analogous to clicking on the Run App icon–you’ll do it often (more than any other devtools or usethis function).

devtools::load_all()

The output we’re looking for from load_all() is straightforward:

ℹ Loading monthAppPkg

load_all() is similar to calling library, but it’s specifically designed to be used during package development. Imagine sourcing all the functions in the R/ folder, but more sophisticated.

1.7 Write tests

Create tests for the code in the R/ folder using use_test()

usethis::use_test("monthFeedbackServer")

This will add test- files in tests/testthat/:

tests/
├── testthat/
   └── test-monthFeedbackServer.R
└── testthat.R

2 directories, 2 files

The first time you run use_test(), it will detect if your package has the testthat infrastructure (and create the necessary files if you don’t).10 use_test() will also adds the testthat package to the Suggests field in the DESCRIPTION and includes the edition (currently 3).

✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Adding '3' to Config/testthat/edition

Tests are covered in Mastering Shiny,11 on the Shiny website,12 and in various testing packages (like shinytest13 and shinytest214).

1.8 app.R

The contents of app.R have been changed to include a call to pkgload::load_all() the standalone app function (monthApp()), which is stored in the R/ folder.

pkgload::load_all(".")
monthApp()

pkgload needs to be listed under Imports in the DESCRIPTION file (just like we did with shiny above).

usethis::use_package("pkgload")
✔ Adding 'pkgload' to Imports field in DESCRIPTION
• Refer to functions with `pkgload::fun()`

Because we’re only going to use load_all() from pkgload, we’ll use explicit namespacing (i.e., pkg::fun()).15

1.8.1 use_package_doc()

The use_package_doc() creates the R/[[name]-package].R file, which can be used as a single location for declaring dependencies in monthAppPkg:

usethis::use_package_doc()
✔ Setting active project to 'projects/apps/monthAppPkg'
✔ Writing 'R/monthAppPkg-package.R'
• Modify 'R/monthAppPkg-package.R'

We’ll use @importFrom to add only the load_all() function to the NAMESPACE.

#' @keywords internal
"_PACKAGE"

## usethis namespace: start
#' @importFrom pkgload load_all
## usethis namespace: end
NULL

1.8.2 use_build_ignore()

R packages don’t typically have an app.R file in their root folder, so we’ll let devtools know this file should be ignored by creating a .Rbuildignore and include a pattern that excludes app.R whenever the package is built.

usethis::use_build_ignore("app.R")
✔ Adding '^app\\.R$' to '.Rbuildignore'

It’s best to let use_build_ignore() handle excluding any files or folders from your package builds because it automatically writes the correct regular expression pattern.

1.9 LICENSE

Use one of the usethis license functions to add a LICENSE file.

usethis::use_mit_license()

The license file should match the License field in the DESCRIPTION file (in this case, it’s MIT).16

✔ Adding 'MIT + file LICENSE' to License
✔ Writing 'LICENSE'
✔ Writing 'LICENSE.md'
✔ Adding '^LICENSE\\.md$' to '.Rbuildignore'

1.10 Document

After writing roxygen2 documentation for the data, modules, and standalone app function, calling devtools::document()() generates the .Rd files and NAMESPACE.17

devtools::document()

The output from document() tells us what files have been created (and if there were any errors in them).18

ℹ Updating monthAppPkg documentation
ℹ Loading monthAppPkg
Writing NAMESPACE
Writing NAMESPACE
Writing birthstoneUI.Rd
Writing birthstoneServer.Rd
Writing monthApp.Rd
Writing monthFeedbackUI.Rd
Writing monthFeedbackServer.Rd

1.10.1 Namespace

The NAMESPACE file contains the imported and exported functions from monthAppPkg:19

# Generated by roxygen2: do not edit by hand

export(birthstoneServer)
export(birthstoneUI)
export(monthApp)
export(monthFeedbackServer)
export(monthFeedbackUI)
import(shiny)
importFrom(pkgload,load_all)

1.11 RStudio project options

If you’re developing in RStudio, we need to update our .Rproj file to enable the Build pane and keyboard shortcuts:

file.edit("monthAppPkg.Rproj")

If your app-package was initially built as an RStudio project (i.e., not as a package), the following fields should be included at the bottom of monthAppPkg.Rproj:

BuildType: Package
PackageUseDevtools: Yes
PackageInstallArgs: --no-multiarch --with-keep.source
PackageRoxygenize: rd,collate,namespace

These options are also available under Tools > Project Options … > Build Tools

1.12 Git

The use_git() step is performed much earlier in R Packages, 2ed, but I’ve saved it for this step because using Git will prompt the IDE to re-initialize and display the Git pane (and it will also read our new settings in the .Rproj file).

✔ Setting active project to '/projects/apps/monthAppPkg'
✔ Initialising Git repo
✔ Adding '.Rproj.user', '.Rhistory', '.Rdata', '.httr-oauth', '.DS_Store', '.quarto' to '.gitignore'
There are 12 uncommitted files:
* '.gitignore'
* '.Rbuildignore'
* 'app.R'
* 'data/'
* 'DESCRIPTION'
* 'LICENSE'
* 'LICENSE.md'
* 'man/'
* 'monthAppPkg.Rproj'
* 'NAMESPACE'
* ...
Is it ok to commit them?

1: Absolutely not
2: Not now
3: Absolutely

Agree to commit these files:

Selection: 3
✔ Adding files
✔ Making a commit with message 'Initial commit'
• A restart of RStudio is required to activate the Git pane
Restart now?

1: Not now
2: Negative
3: Absolutely

Restarting RStudio will activate the Git and Build panes:

Git Pane

Git Pane

Build Pane

Build Pane

This will also activate the devtools keyboard shortcuts:

1.12.1 Keyboard shortcuts

The keyboard shortcuts are available in RStudio and Positron.

load_all()

Shift + Ctrl/Cmd + L

document()

Shift + Ctrl/Cmd + D

install()

Shift + Ctrl/Cmd + B

test()

Shift + Ctrl/Cmd + T

1.13 Install

Installing monthAppPkg with devtools::install() produces see the following output in the Build pane:

==> devtools::document(roclets = c('rd', 'collate', 'namespace'))

ℹ Updating monthAppPkg documentation
ℹ Loading monthAppPkg
Documentation completed

==> R CMD INSTALL --preclean --no-multiarch --with-keep.source monthAppPkg

* installing to library ‘/path/to/Library/R/x86_64/4.2/library’
* installing *source* package ‘monthAppPkg’ ...
** using staged installation
** R
** data
*** moving datasets to lazyload DB
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE (monthAppPkg)

Back in the Console, RStudio will restart and call library(monthAppPkg):

Restarting R session...

> library(monthAppPkg)

We can now launch the app using monthApp()

monthApp()

Our monthApp() application

Our monthApp() application

Launch app with the shinypak package:

launch('01_whole-app-game')

1.14 Additional files

The following sections cover additional files you should include in your ap-package (but are not required).

1.14.1 README

A README.md file is the initial point of contact for users and/or contributors looking for information about your app-package. use_readme_rmd() will create a README.Rmd (i.e., the file you’ll edit), which serves as the source document for your README.md.

usethis::use_readme_rmd()

The README.Rmd pattern is automatically added to the .Rbuildignore, and includes a Git ‘pre-commit’ hook:20

✔ Adding '^README\\.Rmd$' to '.Rbuildignore'
✔ Writing '.git/hooks/pre-commit'

1.14.2 NEWS.md

A NEWS.md is helpful for logging updates to your app-package and tracking release information.

usethis::use_news_md()

use_news_md() will also prompt me to add and commit this file to the Git repository:

There is 1 uncommitted file:
* 'NEWS.md'
Is it ok to commit it?

1: Negative
2: Yeah
3: Absolutely not

Selection: 2
✔ Adding files
✔ Making a commit with message 'Add NEWS.md'

The contents of the NEWS.md are below:21

# monthAppPkg (development version)

* Initial CRAN submission.

1.14.3 Vignettes

Vignettes can be used to store detailed tutorials, explanations of core concepts, use-cases, FAQs and troubleshooting, integration with other packages, etc.

use_vignette("monthAppPkg")

The first time we call use_vignette() will prompt usethis to add the following fields in the DESCRIPTION:

✔ Adding 'knitr' to Suggests field in DESCRIPTION
✔ Adding 'rmarkdown' to Suggests field in DESCRIPTION
✔ Adding 'knitr' to VignetteBuilder

The following files are also included in the .gitignore:

✔ Adding 'inst/doc' to '.gitignore'
✔ Creating 'vignettes/'
✔ Adding '*.html', '*.R' to 'vignettes/.gitignore'

1.15 Recap

In this chapter we’ve covered the steps used to create a package containing a Shiny application.

Please open an issue on GitHub


  1. The example app comes from the Packages chapter of Mastering Shiny↩︎

  2. I’ve stored the code for this application in the 01_whole-app-game branch of the sap repository (to avoid confusing it with the actual application repo for this chapter).↩︎

  3. The Whole Game chapter of R Packages, 2ed begins with the usethis::create_package() function, which calls usethis::use_description() internally.↩︎

  4. At the time this was written, there are over 4,000 hits with the boilerplate value for Description (i.e., "What the package does"), which is a sign of how much usethis has been adopted (and how often people forget to come back and edit their DESCRIPTION file).↩︎

  5. If you frequently develop R packages or Shiny apps, consider adding these fields to your .Rprofile.↩︎

  6. Always leave an empty final line in the DESCRIPTION.↩︎

  7. View the documented stones dataset here on GitHub.↩︎

  8. Whenever you use a function from another package, start by running usethis::use_package() to ensure it’s in the DESCRIPTION file.↩︎

  9. I’ve included @import shiny above the definition of our standalone app function (R/launch_app.R), which means I don’t need to add shiny:: when using Shiny functions belowR/.↩︎

  10. You can also set up the testthat infrastructure by calling usethis::use_testthat()↩︎

  11. The Testing chapter in Mastering Shiny covers unit tests with testthat, shiny::testServer(), and the shinytest package.↩︎

  12. See the ‘Server Function Testing’ article on the Shiny website for more information on testServer()↩︎

  13. Check the shinytest package website and video tutorial for more information on testing your app.↩︎

  14. shinytest2 is an updated verison of shinytest with excellent documentation and videos.↩︎

  15. We typically call devtools::load_all(), but using pkgload reduces the number of dependencies included with devtools. Read more about pkgload in the ‘Conscious uncoupling’ of devtools.↩︎

  16. use_mit_license() will automatically include the LICENSE.md file in the root folder (and includes the necessary pattern in the .Rbuildignore to exclude it from the package builds).↩︎

  17. You can include an Roxygen skeleton in the IDE by clicking on Code > Insert Roxygen Skeleton, or using the keyboard shortcut: Option/⌥ + Shift⇧ + Ctrl/Cmd + R↩︎

  18. The files created by document() rely on the roxygen2 package (and should not be edited manually).↩︎

  19. We’re importing the everything from shiny and only load_all from pkgload):↩︎

  20. This Git behavior is designed to prevent us from making changes to the README.Rmd and forgetting to re-render the README.md. If you find this behavior confusing or would like to disable it, run the following commands in the Terminal: rm .git/hooks/pre-commit↩︎

  21. The Initial CRAN submission bullet doesn’t apply to monthAppPkg, so I’ll remove it and re-commit/push the NEWS.md file.↩︎