Dart 3.10 introduces "build hooks" and "code assets," a new feature enabling developers to seamlessly integrate non-Dart code dependencies (like native libraries) into Flutter applications during the build process.
Mind Map
Click to expand
Click to explore the full interactive mind map • Zoom, pan, and navigate
[Music] Welcome to another episode of the Flutter Build Show. I'm your host, Daco Harkes,
a software engineer at Google in the Dart compiler and virtual machine team. In this episode,
we're going to learn about build hooks and code assets, a new feature available in Dart 3.10.
Some problems in computer science are either so annoying to solve or so performance-sensitive that
the whole world agrees to use a single blessed solution almost always written in C. SQLite is
just such a system and while there are multiple SQLite packages on pubdev all of them use the
same engine written in C. This means that flutter developers using SQLite don't have to reimplement
the engine which is great. However, we do have to bundle a correctly compiled version of the SQLite
engine into our apps in a place where our Dart code knows to look. Today, SQLite plug-in authors
solve that problem for us. But that hasn't been a fun experience. And that's where build hooks come
in. They offer you a way to write simple Dart scripts that declare which non-Dart assets your
app requires at runtime. and Flutter will move those assets in the right place every time you
execute Flutter Run or Flutter build. Let's take a look to ensure everyone is on the same page. Hooks
are a software pattern by which systems allows developers to inject their own logic into its
overall flow. In Flutter, we're adding build hooks for app developers to inject custom logic into
Flutter's compilation steps to include nondart code dependencies called code assets. These new
build hooks are conceptually simple. We'll write a few soon, but let's look at a highle overview
first. Build hooks are a dart script which lives at hook/bu dart and which flutter calls during
compilation. Build hooks run in parallel to the rest of your compilation and receive a few
necessary inputs like relevant directories and the target platform. In the end, the build hook
produces code assets which Flutter includes in the app bundle for use at runtime by your app. Build
hooks are proper Dart code, not YAML. So, they can produce assets in a number of ways. For example,
you can include system libraries, download a pre-ompiled dynamic library, or even compile a
code asset directly from source. And if compiling stuff directly from source sounds scary, fear not,
for the Flutter 2 is ready to help you with common scenarios. By default, Flutter caches the results
of your build hooks. So, it's not if these files will be redownloaded or recompiled every time you
execute Flutter Run. But what about when over the course of developing your app, you do need Flutter
to rerun your build hooks? That's where the second build output comes in. Build hook should list all
the files across your codebase that Flutter should include in its caching check sum. This means
that whenever the contents of any of those files change, Flutter will no longer get a cache hit and
will be forced to rerun your build hook. Note that Flutter always adds the build hook itself and any
code it imports as dependencies, meaning you don't need to add them yourself. Packages from Publodev
can also include build hooks. So over time, your Flutter app may depend and execute many build
hooks while compiling your app. That's the highle view of the actual hooks. But what about those
assets? You might be wondering whether they're similar or related to the assets we declare in
our pubspec.l files. They absolutely are. Assets in your pubspec. file are data assets while the
files you produce in your build hook are called code assets. However, both get baked into your
flutter app bundle by the Flutter tool so that the Dart code knows where to find them. Just like data
assets, code assets are discoverable by a unique ID. This is similar to how you also use a data
assets path to, for example, display an image. We'll see where the code assets unique ID comes
from later. Code assets have other attributes, but we'll see them in action as we work through
three different examples that illustrate common use cases. First, we're going to link a system
library, which is the simplest possible build hook you can write. Then, we'll get more complicated
by downloading a pre-ompiled asset from the internet. And finally, we'll compile our own
library and use all of the bells and whistles of Flutter's new build hook system. As we proceed,
note that these full examples are available in the package code assets and linked in
the show notes. Build hooks themselves are fully explained at dartledev/tools/hooks.
So, let's write our first build hook. This hook will add the ability to call the get hostname
function, which exists in some form or another on every operating system. To start adding a build
hook to your Flutter app or Dart package, add a few dependencies. Then create a new file add
hook/build to dart and give it a main method. Next, call a special build function which comes
from the hooks package. Build itself takes two arguments. First, it wants all the arguments that
Flutter passed to this build hook. And second, it takes an asynchronous callback which itself
accepts two parameters, input and output. Right now, build hooks only support adding code assets,
but that won't be true forever. So, start by adding a check for which type of asset Flutter
currently wants to build. Next, let's add a stop code asset to the output parameter. This
will house all the information Flutter needs to correctly embed this resource into our app. The
code assets constructor has three required parameters: package, name, and link mode.
For package, supply the name of your Flutter app or Dart package. To avoid naming collisions,
this must be the name in your pubspect. DML. For name, supply a Dart library URI minus the leading
package part. Together, the package and name define the asset ID. The formula to derive the
asset ID is the word package then a colon then the value supplied to the package parameter slash and
then the value supplied to the name parameter. You can supply anything you want for this name
parameter. Meaning the final asset ID does not actually need to be a valid Dart import. However,
creating a valid import is less confusing later on. Since we're just calling a system API here,
the function we need is already available. We just have to find it. For most of the possible
platforms, the link mode we want is look up in process. Which means that at runtime when we
want to use the functions in this code asset, the Dart runtime will look up those functions in the
running process. On Windows, we need to access the Windows system library instead of the Unix
one. So now it's time to make use of that input variable to make sure we're setting up everything
correctly for the target platform. The info we need is available at input.config.code.target
OS and we can switch on that to do something else for Windows. For Windows, FFI gen will generate
a different set of bindings. Additionally, the native function is lazily loaded from the system
library which comes standard on every Windows machine. This means we just have to include a
reference to it. And lastly, throw an exception for any unexpected values. Our first build hook
is complete. To review, this hook produces a simple code asset and doesn't have to declare any
dependencies for the caching check sum. So, it was pretty simple. This is all good and fun and all,
but the whole point of this is that we can call the function from Dart, right? So,
let's do that. At this point, the code asset is baked into your Flutter app and is ready to use
via Dart FFI. However, writing Dart Fi bindings by hand is tedious and errorprone. So, we're going to
generate the bindings with package FFI genen from the system library headers instead. This video
isn't an FFI gen tutorial, so I'm going to breeze past the next part. Remember, all of this code
is pulled directly from the examples in package code assets, and the full source is linked in the
video description below along with FFI genen's own documentation. In a file at tool FFI gender dart,
add this boiler plate. Then establish your package route and create a generator. First,
we need to specify what the generator should run on the C headers. To create this Unix.h file,
I found the headers online. Then I made a new file at source/unix.h outside of the lip directory and
then I added the headers include statement that I found on the man page. After setting up the
header configuration, we need to specify which C functions we're interested in, only get host name.
All the other functions are excluded by default to avoid generating a huge Dart file with thousands
of bindings. Lastly, we need to specify the output where our Dart binding should be written. Now,
we're ready to call the generators generate method. Next, our FFI gen script also requires
special handling for Windows. I'll copy paste the FFI generator and update the header file and
output file to Windows. Now when you execute Dart tool/ FFI general Dart, Dart will spit out a bunch
of really funl looking code which exposes your fancy code assets to the rest of the application.
We want to expose a more idiomatic Dart wrapper. So let's create a new file, import the generated
bindings, and declare a function. Since we're calling native code, we have to worry about native
memory which includes freeing that memory to avoid memory leaks. To automate releasing memory,
call the using function which takes a callback that is passed an arena object. That arena object
can allocate memory and when the using function completes, arena automatically releases any memory
it allocated. Now we can allocate memory, call the correct platforms generated bindings and return
our value as a Dart string. Note that we want to give a nice Dart API here. So we have to convert
a native string to a Dart string and convert any native errors to Dart exceptions. Let's write a
test to see if our code works. We don't know, of course, what our host name is, but at least
it should not be empty or null. Let's run the test. It works. We just bound to native code,
expose the functionality in a nice Dart API, and at runtime, the Dart code can access the system
libraries via the code assets that we output in the build hook. Pretty neat. In the next example,
things will get spicier by downloading a pre-built SQLite binary to include in our app. I'll create
a new empty Dart package and add my dependencies, dev dependencies, and the hook/build to Dart file.
In that build file, I will add the same familiar starting code. The real code sample available in
package code assets covers more edge cases than we have time to cover here. So, for the purpose
of the video, I'll focus on the highlights. First, I'll add a code asset to the output
variable. Its package and name combine to produce an asset ID. Like before, notice that link mode
is simpler this time because all platforms will use the same type of resource, a downloaded file.
Next, we'll need to actually download that file. So, I'll create a helper function. What follows is
a bit of pseudo code, but remember the full source is linked below. Because I'm currently on a Mac,
I'll just handle that platform. On Mac OS, Homebrew is the easiest way to install SQLite,
but that requires pseudo. So, I'll just see whether it's installed and warn the user if it's
not. Finally, I'll return the file as a URI. For the full source code, check the links in
the video description. The final step to actually call this code from Dart is to add FFI genen. So,
switch over to tools/ FFI gender dart and add a declaration that looks pretty familiar. Note the
SQLite header files value for the entry points. Just like with the headers for get host name,
it is our job to provide the headers. You can either automate this by downloading them in your
build hook or you can just pop over to the website and grab them manually and commit them in your git
repo. We've gone ahead and downloaded the headers from sqlite.org/d downloaded html and place them
in a third party directory. Note that it's always a good idea to place any code in your package that
is not under your own copyright in a third party directory. For now, we're only interested in lib
version just to see if we can use SQLite and we'll pop the generated bindings into lip source third
party. Note that the generated binding should generally have the same copyright as the source
of the bindings. Hence, we place it in a third party directory and you can pop the copyright
in the output preamble. Now, we run dart tool/fi general dart and we are able to import and call
the generated bindings. Pretty neat. Okay, let's write a test to verify that everything works as
expected. We don't know exactly what SQLite version we have, but we should definitely be
3 something, otherwise we won't be able to work with it. Let's run the test. Sweet. What happened
here in the background is that Flutter took the downloaded dynamic library and bundled it with
the Dart code when it was run. Now that we know we can call native functions in the database engine,
we could continue implementing calling functions to execute queries, but we'll leave that as an
exercise for you or just head over to pup.dev and use one of the existing SQLite packages. We're
on the home stretch now. All that remains is to compile SQLite from source. Interestingly enough,
this scenario is actually the easiest of the three thanks to the native tool chain C package.
To start, you need the actual source. So I've placed SQLite.c from the same download page next
to SQLite.h in my third party directory. Now in hook/build.dart I can use the C builder library
constructor from that native tool chain C package to compile the library. Note the map passed to the
defines which ensures native functions are visible in the Windows build. Once the builder object is
ready, we call and await its run method. Notice how it takes the input and output parameters.
It handles instantiating the code object for us. The C builder also includes SQLite C as a
dependency like so, so we don't have to. But if we weren't using a helper library, this is what
it would look like to explicitly mark a dependency in our build hook. Ultimately, this means that if
we ever update SQLite.c to a newer version, the build hook will be rerun. Inquisitive viewers
may be wondering whether our previous FFI gen specification for the downloaded SQLite package
will change for the self-compiled version. And the answer is it does not. The exact same FFI gender
dart creates the exact same bindings which we can invoke in the exact same Dart code. Also, we can
run the same test. But let's tweak it a bit. Since we have checked in the source code of SQLite,
we know the exact version number. And the test passes. So that's build hooks and code assets.
Other asset types are planned, but for now, this system enhances Flutter developers ability to
interface with industry standard native libraries. This feature pairs well with Flutter's thread
merge, which is what allows you to synchronously call system libraries like the one linked in the
first example. For more info on that, check out the latest episode of Decoding Flutter titled
The Great Threat Merge. And for everything else, head on to flutter.dev. Happy hacking. [Music]
Click on any text or timestamp to jump to that moment in the video
Share:
Most transcripts ready in under 5 seconds
One-Click Copy125+ LanguagesSearch ContentJump to Timestamps
Paste YouTube URL
Enter any YouTube video link to get the full transcript
Transcript Extraction Form
Most transcripts ready in under 5 seconds
Get Our Chrome Extension
Get transcripts instantly without leaving YouTube. Install our Chrome extension for one-click access to any video's transcript directly on the watch page.