-
Notifications
You must be signed in to change notification settings - Fork 565
/
scaling-packages.Rmd
401 lines (305 loc) · 17.4 KB
/
scaling-packages.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# Packages {#scaling-packaging}
```{r, include = FALSE}
source("common.R")
options(tibble.print_min = 6, tibble.print_max = 6)
```
If you are creating a large or long-term Shiny app, I highly recommend that you organise your app in the same way as an R package.
This means that you:
- Put all R code in the `R/` directory.
- Write a function that starts your app (i.e. calls `shinyApp()` with your UI and server).
- Create a `DESCRIPTION` file in the root directory of your app.
This structure gets your toes into the water of package development.
It's a long way from a complete package, but it's still useful because it activates new tools that make it easier to work with larger app.
The package structure will pay off further when we talk about testing in Chapter \@ref(scaling-testing), because you get tools that make it easy to run the tests and to see what code is tested.
In the long run, it also helps you document complex apps using [roxygen2](https://roxygen2.r-lib.org), although we won't discuss that in this book.
It's easy to think of packages as giant complicated things like Shiny, ggplot2, or dplyr.
But packages can also be very simple.
The core idea of a package is that it's a set of conventions for organising your code and related artefacts: if you follow those conventions, you get a bunch of tools for free.
In this chapter, I'll show you the most important conventions, and then provide a few hints as to next steps.
As you start working with app-packages, you may find that you enjoy the process of package development and want to learn more.
I'd suggested starting with [*R Packages*](https://r-pkgs.org){.uri} to get the lay of the package development land, then continuing on to [*Engineering Shiny*](http://engineering-shiny.org/){.uri}, by Colin Fay, Sébastien Rochette, Vincent Guyader, Cervan Girard, to learn more about the intersection of R packages and Shiny apps.
```{r setup}
library(shiny)
```
## Converting an existing app
Converting an app to a package requires some upfront work.
Assuming that you have an app called `myApp` and it already lives in a directory called `myApp/`, you'll need to do the following things:
- Create an `R` directory and move `app.R` into it.
- Transform your app into a standalone function by wrapping:
```{r}
library(shiny)
myApp <- function(...) {
ui <- fluidPage(
...
)
server <- function(input, output, session) {
...
}
shinyApp(ui, server, ...)
}
```
- Call `usethis::use_description()` to create a description file.
In many cases, you'll never need to look at this file, but you need it to activate RStudio's "package development mode" which provides the keyboard shortcuts we'll use later.
- If you don't already have one, create an RStudio project by calling `usethis::use_rstudio()`.
- Restart RStudio and re-open your project.
You can now press `Cmd/Ctrl + Shift + L` to run `devtools::load_all()` and load all the package code and data.
This means that you can now:
- Remove any calls to `source()`, since `load_all()` automatically sources all `.R` files in `R/`.
- If you are loading datasets using `read.csv()` or similar, you can instead use `usethis::use_data(mydataset)` to save the data in the `data/` directory.
`load_all()` automatically loads the data for you.
To make this process more concrete, we'll next work through a simple case study before coming back to the other benefits of this work in Section \@ref(package-workflow).
### Single file
Imagine I have a relatively complex app that currently lives in a single `app.R`:
```{r, message = FALSE}
library(shiny)
monthFeedbackUI <- function(id) {
textOutput(NS(id, "feedback"))
}
monthFeedbackServer <- function(id, month) {
stopifnot(is.reactive(month))
moduleServer(id, function(input, output, session) {
output$feedback <- renderText({
if (month() == "October") {
"You picked a great month!"
} else {
"Eh, you could do better."
}
})
})
}
stones <- vroom::vroom("birthstones.csv")
birthstoneUI <- function(id) {
p(
"The birthstone for ", textOutput(NS(id, "month"), inline = TRUE),
" is ", textOutput(NS(id, "stone"), inline = TRUE)
)
}
birthstoneServer <- function(id, month) {
stopifnot(is.reactive(month))
moduleServer(id, function(input, output, session) {
stone <- reactive(stones$stone[stones$month == month()])
output$month <- renderText(month())
output$stone <- renderText(stone())
})
}
months <- c(
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
)
ui <- navbarPage(
"Sample app",
tabPanel("Pick a month",
selectInput("month", "What's your favourite month?", choices = months)
),
tabPanel("Feedback", monthFeedbackUI("tab1")),
tabPanel("Birthstone", birthstoneUI("tab2"))
)
server <- function(input, output, session) {
monthFeedbackServer("tab1", reactive(input$month))
birthstoneServer("tab2", reactive(input$month))
}
shinyApp(ui, server)
```
This code creates simple three page app that uses modules to keep the pages isolated.
It's a toy app, but it's still realistic --- the main difference compared to a real app is that here the individual UI and server components are much simpler.
### Module files
Before turning it into a package, my first step is to pull the two modules out into their own files following the advice in Section \@ref(naming-conventions):
- `R/monthFeedback.R`:
```{r}
monthFeedbackUI <- function(id) {
textOutput(NS(id, "feedback"))
}
monthFeedbackServer <- function(id, month) {
stopifnot(is.reactive(month))
moduleServer(id, function(input, output, session) {
output$feedback <- renderText({
if (month() == "October") {
"You picked a great month!"
} else {
"Eh, you could do better."
}
})
})
}
```
- `R/birthstone.R`:
```{r}
birthstoneUI <- function(id) {
p(
"The birthstone for ", textOutput(NS(id, "month"), inline = TRUE),
" is ", textOutput(NS(id, "stone"), inline = TRUE)
)
}
birthstoneServer <- function(id, month) {
stopifnot(is.reactive(month))
moduleServer(id, function(input, output, session) {
stone <- reactive(stones$stone[stones$month == month()])
output$month <- renderText(month())
output$stone <- renderText(stone())
})
}
```
That leaves me with the following `app.R`:
```{r}
library(shiny)
stones <- vroom::vroom("birthstones.csv")
months <- c(
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
)
ui <- navbarPage(
"Sample app",
tabPanel("Pick a month",
selectInput("month", "What's your favourite month?", choices = months)
),
tabPanel("Feedback", monthFeedbackUI("tab1")),
tabPanel("Birthstone", birthstoneUI("tab2"))
)
server <- function(input, output, session) {
monthFeedbackServer("tab1", reactive(input$month))
birthstoneServer("tab2", reactive(input$month))
}
shinyApp(ui, server)
```
Just pulling the modules out into separate files is useful because it helps me understand the big picture of the app.
If instead I want to dive into the details, I can look at the modules files.
### A package
Now let's make this into a package.
First I run `usethis::use_description()`, which creates a `DESCRIPTION` file.
Next, I move `app.R` to `R/app.R` and wrap `shinyApp()` into a function:
```{r}
library(shiny)
monthApp <- function(...) {
stones <- vroom::vroom("birthstones.csv")
months <- c(
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
)
ui <- navbarPage(
"Sample app",
tabPanel("Pick a month",
selectInput("month", "What's your favourite month?", choices = months)
),
tabPanel("Feedback", monthFeedbackUI("tab1")),
tabPanel("Birthstone", birthstoneUI("tab2"))
)
server <- function(input, output, session) {
monthFeedbackServer("tab1", reactive(input$month))
birthstoneServer("tab2", reactive(input$month))
}
shinyApp(ui, server, ...)
}
```
As an optional extra, I converted `birthstones.csv` to a package dataset by running `usethis::use_data(stones)`.
This creates `data/stones.rda`, which will be loaded automatically when I load the package.
I can now delete `birthstones.csv` and remove the line that reads it in: `stones <- vroom::vroom("birthstones.csv")`.
You can see the final product at <https://github.com/hadley/monthApp>.
## Benefits {#package-workflow}
Why bother doing all this work?
The most important benefit is a new workflow that makes it easier accurately re-load all app code and relaunch the app.
But it also makes it easier to share code between apps and share your app with others.
### Workflow
Putting your app code into the package structure unlocks a new workflow:
- Re-load all code in the app with `Cmd/Ctrl + Shift + L`.
This calls `devtools::load_all()` which automatically saves all open files, `source()`s every file in `R/`, loads all datasets in `data/` then puts your cursor in the console.
- Re-run the app with `myApp()`.
As your app grows bigger, it's also worth knowing about the two most important code navigation keyboard shortcuts:
- `Ctrl/Cmd + .` will open the "fuzzy file and function finder" --- type a few letters at the start of the file or function that you want to navigate to, select it with the arrow keys and then press enter.
This allows you to quickly jump around your app without taking your hands off the keyboard.
- When your cursor is on the name of function, `F2` will jump to the function definition.
If you do a lot of package development, you might want to automatically load usethis, so you can type (e.g.) `use_description()` instead of `usethis::use_description()`.
You can do so by adding the following lines to your `.Rprofile`.
This file contains R code that's run whenever you start R, so it's a great way to customise your interactive development environment.
```{r, eval = FALSE}
if (interactive()) {
require(usethis, quietly = TRUE)
}
```
The easiest way to find and open your `.Rprofile` is to run `usethis::edit_r_profile()`.
### Sharing
Since your app is now wrapped up in a function, it's easy to include multiple apps in the same package.
And because you have multiple apps in the same place, it's now much easier to share code and data across apps.
That's a huge benefit if you have a bunch of apps for related tasks.
Packages are also a great way to share apps.
[shinyapps.io](https://www.shinyapps.io) and [RStudio Connect](https://rstudio.com/products/connect/) are great way to share apps with folks who aren't familiar with R.
But sometimes you want to share apps with your colleagues who do use R --- maybe instead of allowing the user to upload a dataset, you want to provide them with a function that they call with a data.frame.
For example, the following very simple app allows the R user to supply their own dataframe for interactive summaries:
```{r}
dataSummaryApp <- function(df) {
ui <- fluidPage(
selectInput("var", "Variable", choices = names(df)),
verbatimTextOutput("summary")
)
server <- function(input, output, session) {
output$summary <- renderPrint({
summary(df[[input$var]])
})
}
shinyApp(ui, server)
}
```
[RStudio Gadgets](https://shiny.rstudio.com/articles/gadgets.html) build on this idea: they are Shiny apps that let you add new user interface to the RStudio IDE. It's even possible to write gadgets that generate code, so you can perform some task that's easy to do interactively, and the gadget generates the corresponding code and saves back into the open file.
## Extra steps
There are two common extra steps you might take beyond the basics: making it easy to deploy your app-package, and turning it into a "real" package.
### Deploying your app-package
If you want to deploy your app to RStudio Connect or Shiny[^scaling-packages-1] you'll need two extra steps:
[^scaling-packages-1]: I'd expect most other ways of deploying Shiny apps would also work since `app.R` is the most common way of structuring apps.
- You'll need an `app.R` that tells the deployment server how to run your app.
The easiest way is to load the code with pkgload:
```{r, eval = FALSE}
pkgload::load_all(".")
myApp()
```
This file calling `load_all()` **may not** be placed under the package's `R` directory - this would result in an
infinite loop when loading! The root directory of the package is the right place.
(You can see other techniques at <https://engineering-shiny.org/deploy.html>).
- Normally when you deploy an app, the rsconnect package automatically figures out all of the packages your code uses.
But now that you have a `DESCRIPTION` file, it requires you to explicitly specify them.
The easiest way to do this is to call `usethis::use_package()`.
You'll need to start with shiny and pkgload:
```{r, eval = FALSE}
usethis::use_package("shiny")
usethis::use_package("pkgload")
```
This is a little a more work, but the payoff is a having an explicit list of every package that your app needs in one place.
Now you can run `rsconnect::deployApp()` whenever you're ready to share an updated version of your app with your users.
### `R CMD check`
A minimal package contains an `R/` directory, a `DESCRIPTION` file, and a function to run your app.
As you've seen, this is already useful because it unlocks some useful workflows to speed up app development.
But what makes a "real" app?
To me, it's making a serious effort to get `R CMD check` passing.
`R CMD check` is R's automated system that checks your package for common problems.
In RStudio, you can run R CMD check by pressing `Cmd/Ctrl + Shift + E`.
I don't recommend that you do this the first time, the second time, or even the third time you try out the package structure.
Instead, I recommend that you get familiar with the basic structure and workflow before you take the next step to make a fully compliant package.
It's also something I'd generally reserve for important apps, particularly any app that will be deployed elsewhere.
It can be a lot of work to get `R CMD check` passing, and there's little pay off in the short term.
But in the long-term this will protect you against a number of potential problems, and because it ensures your app adheres to standards that R developers are familiar with, it makes it easier for others to contribute to your app.
Before you make your first full app-package, you should read "[The whole game](https://r-pkgs.org/whole-game.html)" chapter of *R packages*: it will give you a fuller sense of the package structure, and introduce you to other useful workflows.
Then use the following hints to get `R CMD check` passing cleanly:
- Remove any calls to `library()` or `require()` and instead replace them with a declaration in your `DESCRIPTION`.
`usethis::use_package("name")` to add the required package to the `DESCRIPTION`[^scaling-packages-2].
You'll then need to decide whether you want to refer to each function explicitly with `::`, or use `@importFrom packageName functionName` to declare the import in one place.
At a minimum, you'll need `usethis::use_package("shiny")`, and for Shiny apps, I recommend using `@import shiny` to make all the functions in the Shiny package easily available.
(Using `@import` is not generally considered best practice, but it makes sense here).
- Pick a license and then use the appropriate `use_license_` function to put it in the right place.
For proprietary code you can use `usethis::use_proprietary_license()`.
See <https://r-pkgs.org/license.html> for more details.
- Add `app.R` to `.Rbuildignore` with `usethis::use_build_ignore("app.R")` or similar.
- If your app contains small reference datasets, put them in `data` or `inst/extdata`.
We discussed `usethis::use_data()` above; alternatively, you can put raw data in `inst/ext` and load it with `read.csv(system.file("exdata", "mydata.csv", package = "myApp"))` or similar.
- You can also change your `app.R` to use the package.
This requires that your package is available somewhere that your deployment machine can install from.
For public work this means a CRAN or GitHub package; for private work this may mean using a tool like [RStudio Package Manager](https://rstudio.com/products/package-manager/) or [drat](https://github.com/eddelbuettel/drat).
```{r, eval = FALSE}
myApp::myApp()
```
[^scaling-packages-2]: The distinction between Imports and Suggests is not generally important for app packages.
If you do want to make a distinction, the most useful is to use Imports for packages that need to be present on the deployment machine (in order for the app to work) and Suggests for packages that need to be present on the development machine (in order to develop the app).
## Summary
In this chapter, you've dipped your toes into the water of package development.
This might seem overwhelming if you think of packages like ggplot2 and shiny, but packages can be very very simple.
In fact, all a project needs to be a package is a directory of R files and a `DESCRIPTION` file.
A package is just a lightweight set of conventions that unlock useful tools and workflows.
In this chapter, you learned how to turn an app into a package and some of the reasons you might want to.
In the next chapter, you'll learn about the most important reason to turn your app into a package: to make it easier to test.