Chapter 5. Understanding libraries and privacy

This chapter covers

  • Organizing your code into reusable packages
  • Importing libraries into your application
  • Hiding functionality with library privacy
  • Using multiple source files
  • Using scripts as runnable libraries

Most programming languages have a library structure that allows code to be split over multiple files and published to other developers, which promotes modularity and code reuse and improves the ability of developers to work on different parts of the same codebase by hiding the internal implementation of a library. Dart is no exception; but in Dart, the library mechanism also has a big impact on privacy, especially class and function privacy. In languages such as Java and C#, privacy is centered around the class; but in Dart, this privacy exists at the library level rather than the class level. That’s why we’re discussing libraries and privacy this early in this book.

In this chapter, you’ll learn how to create and use libraries of code in Dart and how these libraries relate to Dart’s privacy model, which you can use to hide the internal workings of a library. The library is the smallest unit of deployable code and can be as small as a single class or function or as large as an entire application. In the real world, all but the most trivial application should have its code split into multiple libraries, because this design promotes a good, loosely coupled architecture, reusability, and testability. By building a simple logger framework that you can import into your own code, you’ll explore these features as you go through this chapter.

When you’re building a package of code for reuse, there are often internal workings that you don’t want third-party users to be able to access except via a published and agreed-on interface—for example, the internal state of some class data. In Dart, you can publish a library of code to your team members or web users with only the parts that you want to make visible available to those end users. This setup allows the internals of the library to change without affecting end users. It’s different from that in Java and C#, which have a different, class-centric privacy model. In these languages, the class internals can change without affecting end users.

 

Why doesn’t Dart have a class-centric privacy model?

This is one of the areas of Dart that’s particularly influenced by JavaScript and web development. In JavaScript, there is no notion of privacy, except by following certain conventions such as returning closures from other functions. For this reason, the Dart privacy model should be thought of as an improvement to JavaScript, as opposed to comparing it to more traditional class-based languages such as Java and C#.

 

Dart’s optional typing allows you to provide documentary type information in the code at the point where users interact with your library, such as on function parameters and return types, or class properties, while letting you use only as much type information in the library as you feel is necessary. As noted earlier in the book, type information doesn’t change how your application runs, but it does provide documentation to tools and other developers.

In chapter 3, you were already importing the built-in dart:html library using the import keyword, and it turns out that it’s just as easy to import your own library.

5.1. Defining and importing libraries in your code

In this section, you’ll create a logger library called loglib. It will provide a simple debug/warn/info logger that allows you to output log messages to the web browser’s debug console. A logger library with varying levels of log output is available for most languages: for example, nLog for .NET and log4j for Java. The simplest way to log in Dart is to use the print() function; the loglib example will wrap this function.

In order to properly experiment with libraries, you need some third-party code to call your library, such as the PackList application from chapter 3, a simple Dart app containing only a main() function. Because the PackList app already has some basic functionality that can be logged, it’s an ideal third-party app to use your new loglib library. It has the following functionality, which provides useful items to log:

  • Build UI (debug level)
  • User adds an item (info level)
  • User adds an item with no title (warn level)
  • User marks item as packed or no longer packed (info level)

This use case gives you a set of functions that your library should make available, shown in figure 5.1.

Figure 5.1. loglib functions and classes made available for external code

loglib will contain three top-level functions—info(), warn(), debug()—and a single class, Logger. A top-level function is a function that exists outside a class (just like the main() function). Libraries can be built of functions as well as classes, and a function doesn’t need to be wrapped in a class (as it does in C# or Java). Thus it’s perfectly valid for a library to consist entirely of functions. (It’s equally valid to have a library with no top-level functions that consists only of classes.)

You structure your code into libraries in order to let third-party code use packaged, abstracted functionality. When you use existing libraries, such as the dart:html library in chapter 3, you don’t need to know how it creates HTML elements and triggers browser events—only that it does. By building code into sensible libraries, you’re able to package and version them for others to use, providing a published interface to your library’s functionality.

5.1.1. Defining a library with the library keyword

A Dart library is a .dart file that has a library library_name; declaration at the start of the file. The library declaration announces to Dart that this file represents the root file of a library and must be the first statement (before other code such as class declarations and functions), as shown in figure 5.2.

Figure 5.2. library is the first statement in a library.

The library name’s purpose is to identify the reusable block of code. The library name must be in lowercase, with multiple words separated by underscores. The library name doesn’t need to be the same as the filename, although by convention Dart files are also all lowercase, with multiple words separated by underscores. Unlike in Java, there’s no relationship between filename and folder structure in Dart. Figure 5.3 shows some of the values you can and can’t use as library names.

Figure 5.3. Valid and invalid library names

In addition to being able to call a library by any string, it’s also possible to create a library in any folder, with no restriction placed on the number of libraries in a folder. To define the loglib library, you need a single file, loglib.dart, containing this single line:

The Dart Editor helpfully identifies library .dart files (as opposed to nonlibrary .dart files) by making them bold in the Editor’s Files view, as shown in figure 5.4.

Figure 5.4. The Dart Editor indicates library files in bold.

Now that you have a library file defined, you can start to populate it with code. Your top-level logger functions at present will call the built-in print("") function to output the log message to the browser console and use the Dart shorthand function syntax discussed in chapter 4. The Logger class will have a function that can do the same, as shown in the following listing.

Listing 5.1. loglib.dart functions and classes

The loglib library at present doesn’t add much value over the built-in print() function; you’ll expand on it as you progress through the chapter.

 

Tip

The built-in print() function sends its output either to stdout when it’s running as a server-side script or to the browser’s debug console (accessible through Tools > JavaScript console in Chrome/Dartium or Web Developer > Web Console in Firefox).

 

Now that you have a library with some usable functions and classes, it’s time to use them. You can let third-party code use your library.

5.1.2. Importing libraries with import

Import the loglib library using the import "loglib.dart"; statement if the loglib.dart file is in the same folder. This import statement is deceptively powerful: it allows you to reference a library filename by URI, either directly from the filesystem or via HTTP over the web. The following import statements are all valid but offer different degrees of flexibility:

 

Warning

Using an absolute file path to a specific folder on a specific drive doesn’t promote great reuse; it means that another developer who wants to use your app’s code needs to have the same filesystem structure (and the same drive letters if they’re running Windows). It’s better to use relative paths or package management.

 

The PackList app from chapter 3 can import the loglib library. In the following examples, you’ll use the directory structure shown in figure 5.5, which will allow for a relative import statement.

Figure 5.5. The folder structure for the loglib and PackList example

By using the relative path from the packlist.dart file to the loglib.dart file, PackList can import the loglib library with the following statement:

import "../loglib/loglib.dart";

Listing 5.2 shows the import statement that packlist.dart will use, in addition to the existing dart:html library import from chapter 3. The order of the imports isn’t important, because all the imports are loaded before code starts executing, but they must appear before any code statements.

Listing 5.2. packlist.dart importing the loglib.dart library

Libraries Importing Other Libraries

A library can import many other libraries, too. Your loglib library could import a number of other libraries (if it needed to), but the import statement must appear after the library declaration and before any code statements, as shown in figure 5.6.

Figure 5.6. The import statement must appear before code statements.

If loglib were to import the dart:html library, then the dart:html library would become available only to the loglib library. If the rest of your application also wanted to use the dart:html library, then you’d also need to specify another import statement elsewhere in your application file. Imports are local to the library that declares the import statement.

Circular references between library imports are also allowed. For example, Library A can import Library B, and Library B can also import Library A, because the Dart libraries are fully loaded before the application starts running, so the Dart tools can understand this type of dependency.

Using the Top-Level Imported Functions

The first use case for using loglib in your PackList app was to output a debug message when you started building the UI. The ideal place to put this code is at the beginning of the main() function where you have the todo marked, as shown in figure 5.7. Running the PackList app will now put a message onto the browser console. Figure 5.7 also shows the relationship between the PackList debug() method call and the loglib library.

Figure 5.7. PackList calls the top-level function in the loglib library.

Dart is designed to be converted to JavaScript so it can run in browsers that don’t natively support Dart. JavaScript doesn’t natively support libraries: a multilibrary Dart application becomes a single JavaScript file on conversion, with each imported library in its own annotated section, commented with the library name, as in listing 5.3. Note in the JavaScript output that only the debug(msg) function exists in the loglib section—this is because you aren’t yet using the other functions or classes, so it knows not to convert them.

Listing 5.3. Generated JavaScript extract of the PackList app and loglib library

You can now flesh out the logging use cases in your PackList app. The remaining ones are as follows:

  • User adds an item (info level)
  • User adds an item with no title (warn level)
  • User marks item as packed or no-longer packed (info level)

You can achieve this by adding the following lines to your existing PackItem class. First, check the itemText length in the constructor and output either an info or a warning message; second, add another event handler to listen to the UI item being clicked by the user, which adds an info message.

Listing 5.4. Adding logging method calls to the PackList application

Using Top-Level Imported Classes

Your PackList app is now using the publicly exposed functions. You also have a Logger class in loglib, which you’ve ignored so far. You can add another log message when you’ve finished building the UI, but this time (for the sake of example) use the Logger class. Create an instance of the class using the new keyword just as if you’d declared it in the same PackList file, as shown next.

Listing 5.5. packlist.dart using the Logger class imported from loglib

You’ve now wired in the loglib library with the PackList app. The PackList app now logs a variety of messages as it starts up and the user interacts with it. Figure 5.8 shows some of the logging that appears in the developer console.

Figure 5.8. The logging messages output by the Pack-List app

Using Library Prefixes to Prevent Name Clashes

Now that you have a working library, you can let other developers use it—they have a well-defined interface into your library. Unfortunately, there’s nothing to stop another developer from also using a different library that also contains a Logger class and a top-level info() method. This is where import prefixes are used.

An import prefix allows you to import multiple libraries that may contain top-level functions and classes with the same names. Imagine a scenario in which a developer wants to use another (hypothetical) Logger library called WebLogger, which is able to send log messages to a server somewhere and also contains the info() function.

Dart wouldn’t know which of the info() functions you intended to call, as demonstrated in figure 5.9.

Figure 5.9. Multiple imported libraries can sometimes contain the same function names, so you need a mechanism to deal with these clashes.

Fortunately, Dart provides an as statement, which forms part of the import declaration. This feature allows you to deal with namespace clashes by giving all the code in a specific library a name that you can reference. The as statement is followed by a prefix name that becomes your reference when you refer to any code in that specific library elsewhere in code, as shown in figure 5.10.

Figure 5.10. You can declare a library prefix for use when referring to the code in an imported library.

Once you use the prefix defined by the as statement, you must always refer to any class or function in that library with the prefix. Although it’s possible to always use a prefix with every import declaration, doing so could cause your code to become cluttered because every reference to every class and method in the imported library would need to use the prefix. The pragmatic approach is best: add library prefixes only when doing so aids readability and/or prevents naming clashes, rather than using library prefixes everywhere.

Currently, your loglib logging library has all its functions and classes available to users of the library. Nothing is hidden from any app that imports the library—all your functionality is publicly available. In the next section, you’ll make some items private so they aren’t accessible from outside the library.

 

Remember

  • The library library_name; statement must be the first statement in a library.
  • Libraries can use the import "uri/to/lib.dart"; statement to import other libraries.
  • library and import statements must appear before other code.
  • You can use library prefixes to avoid naming clashes between different libraries.

 

5.2. Hiding functionality with library privacy

When you’re building a library of functionality, it’s likely that there will be some internal implementation details that you won’t want to expose to end users of that library. The Logger library currently contains a basic function of outputting data to the browser console. Suppose you want to add a feature to your Logger library that sends the log messages to a server somewhere. You don’t want external users of your library to call this server logging code directly; it needs to be called internally by your entry-point functions. If you just declare classes and functions in your library, they will be accessible to end users; but fortunately Dart lets you declare items as private by prefixing their names with an underscore (_).

 

Note

Privacy in the context of this chapter refers to the object-orientated notion of privacy, which through language structures hides the internals of one system from another system. Making your library private so your code can’t be read by prying eyes is a different concept and may be addressed by code obfuscation and minification, both of which are under development by the Dart team. In addition, privacy isn’t related to security—all imported library code runs as part of a single codebase. In chapter 15, you’ll see how you can create separate isolates, which provide a degree of security.

 

As shown in figure 5.11, privacy exists at a library level rather than a class level. Anything declared to be private is accessible from within its own library but can’t be referenced by name from outside that library.

Figure 5.11. Privacy is achieved by prefixing the class, function, property, or method name with an underscore, which allows access only from other code in the same library.

Privacy in a library can be applied to top-level functions and classes and in classes to their properties and methods by adding the underscore prefix to the name. Calling code also needs to include the underscore prefix when using these private elements, as in

 

Building a language feature around a naming convention?

The underscore prefix is a common (but not necessarily universal) naming convention to indicate privacy, especially in languages that don’t have privacy built in (such as JavaScript and Python). Dart takes this naming convention further by making it a language feature.

This feature has been the subject of some debate in the Dart community—it’s perhaps one of the biggest points of contention. On the one hand, you get privacy with little developer overhead; and at the call site you can see where something being called is private, which is useful when you’re exploring the internals of new libraries because you don’t need to seek out the declaration. On the other hand, it does affect readability, and it’s possible to have code that looks like this:

var someValue = new _MyClass()._getValue()._doAction(_withProperty);

Another argument against using the underscore prefix is that if you need to change something from public to private (or vice versa), it must be renamed everywhere that it’s used. The other side to this argument is that if you’re renaming from private to public, then the renaming will happen only in your library (if it’s currently private in your library, then no external user will be using it). If you’re changing from public to private, then there are more fundamental issues (such as breaking your library users’ code by removing a function) than just renaming.

 

The two rules to remember are as follows:

  • Code in a library can access any other code in the same library.
  • Code outside a library can access only nonprivate code in that library.

These rules are particularly important for classes, which have a mixture of public and private properties and methods.

5.2.1. Using privacy in classes

Privacy in classes is different than in C# and Java. The first rule about Dart privacy means that two different classes both have access to each other’s private methods and properties (similar to Java’s package-private feature).

In the loglib library, you currently have a Logger class. Perhaps you want to determine whether the logger is enabled or disabled by storing an internal _isEnabled property: its internal state. Other classes using the Logger class that are in the same library can access the internal state directly, but users of your library can’t access that internal state. Other parts of the app should have no knowledge about the workings of Logger class, only that it does work. Figure 5.12 illustrates this relationship.

Figure 5.12. WebServerLogger can access private properties of the Logger class because they’re in the same library.

By using the underscore prefix, you can build rich functionality into your library and ensure that only the functionality that users of your library need is exposed through a well-defined and consistent interface of classes, methods, and top-level functions.

Accessing Private Fields with Getters and Setters

As discussed in chapter 3, getters and setters provide a way to access fields. They too can be made public or private through the use of the underscore. If you want to allow external users read-only access to the _isEnabled property, you can add a public getter to your class. Likewise, when you add a public setter, the value becomes writable. It’s interesting to note that it’s perfectly valid to have read-only or write-only values by providing only a getter or a setter. Figure 5.13 shows how your library can show and hide a class’s properties through getters and setters.

Figure 5.13. Using getters and setters to provide varying levels of field access to external users of a class

Using Private Methods

In addition to private fields wrapped with getters and setters, private methods in classes can also be defined, again, by prefixing the method name with an underscore. A useful effect of this is that it makes refactoring of long methods possible, because the extracted methods, when marked private, can’t be called from outside the library.

Figure 5.14 takes a typically long method, extracts a block of code, and refactors it into a private method called _sendToServer(). The _sendToServer() method can’t be called from outside the library, but the original log() function still works exactly the same way, with external users of the library being unaware of the change.

Figure 5.14. To keep your code readable and maintainable, you can extract a block of code into a private method of the same class that isn’t visible from outside the library.

A Puzzler with Private Classes

In the same way that you can have private methods and private properties in a class, it’s also possible to create private classes in a library by prefixing the class name with an underscore, as shown with the _ServerLogger class in listing 5.6. Private classes can be useful in that they can be created only in your library. You can’t use the new keyword to create a new instance of a private class from outside your library.

Private classes can have public methods and properties. The _ServerLogger class has a private property called _serverIp and a public property called serverName.

An interesting puzzler is why a private class (which is accessible only in a library) can have public methods and properties. When you’re in the same library, it makes no difference whether a property is public or private; and if the class is private, how can it be referenced from outside the library? The following listing shows how this can happen, in the getServerLogger() function, which returns a new instance of the private _ServerLogger class.

Listing 5.6. mixed_loglib.dart: library containing both public and private classes

Although you can directly access private classes outside of a library, a public method or function in that library may return an instance of a private class. This pattern should be avoided, but Dart still handles it through optional typing.

 

Tip

It’s valid to return a public class from a library, but such a class is generally referred to by a public, implicit interface, rather than its implementation class name. I discuss this idea in the next chapter.

 

The calling code in a separate library can see (and instantiate) the Logger class, but it has no knowledge of the _ServerLogger private class. It can call the getServerLogger() function and use the private _ServerLogger returned, as long as it doesn’t use the class name directly—storing the _ServerLogger instance in an untyped variable, as in the following snippet, which stores the returned _ServerLogger in the variable named privateInstance:

Even though you can’t refer to the _ServerLogger class by name, once you have an instance of it you can access its public properties on that private instance with no complaint from the tools. You won’t get autocomplete help, though, because you’re unable to provide the type information to the tools. If you tried to access the privateInstance._serverIp property, you’d get a noSuchMethod error, because you’re trying to access a private property from outside the library. Accessing privateInstance.serverName, though, will work fine, because that isn’t marked private. Writing libraries with the intention of their being used this way should be considered bad practice unless used in conjunction with public interfaces, because there’s no way for end users of your library to find out how your private classes should be used (other than looking at the source).

5.2.2. Using private functions in libraries

Functions in the top level of a library can also be public and private in the same way as classes. Prefixing a private function with an underscore makes it private to a library, meaning it can be accessed from anywhere in the library. This can be useful when you want to provide private utility functions in a library but there’s no associated data, so they don’t warrant becoming methods in a class.

You can see this by adding a private function to the loglib library, as shown in the following listing, which is called by the public-facing info(), warn(), and debug() functions and your Logger class.

Listing 5.7. loglib.dart with a private function

The private _logMsg() top-level function is accessible only from within the loglib library—external users of the library can’t access this function. In the example, an instance of the private class _ServerLogger is created for each call for the purposes of example only—a print() function would suffice.

 

Remember

  • The private _ prefix can be applied to functions, classes, methods, and properties.
  • Code marked as private can be accessed only from within the same library.
  • Code not marked as private can be accessed by external users of that library.

 

Building reusable libraries with hidden internal functionality is standard practice in most applications, and Dart enables this feature by taking the underscore convention and baking it into the language.

Although you can now split your application into reusable libraries, a library can still consist of many thousands of lines of code. Keeping track of all that code in a single library file can be awkward. Fortunately, Dart provides a way to divide libraries even further: into collections of source files.

5.3. Organizing library source code

The loglib library now contains a mix of public and private classes and functions. If you were to add even more functionality, it wouldn’t be long before the library file would become hard to navigate, even with the tools. Even more of an issue when developing in teams is that any major refactoring to the file can easily cause issues for other developers on the team if you’re working on the same library simultaneously.

Fortunately, Dart allows you to split a library into multiple source files. External users of your library have no knowledge of this, and it makes no difference to users of your library whether it’s constructed of a single file, 100 files containing a class or function each, or any combination of classes and functions.

In this section, you’ll take the loglib.dart file, which currently contains two classes and four functions, as shown in figure 5.15, and split it into separate source files.

Figure 5.15. loglib.dart contains a growing number of classes and functions.

These functions and classes will be split into two separate source files, with the loglib.dart library file linking them together. The goal is to end up with three files in total, as demonstrated in figure 5.16.

Figure 5.16. The intended goal is to split the library into three files.

This is just one way to split the library. You could split each class and function into its own file or split all the public functions and classes into one file and all the private functions and classes into another.

 

Tip

In libraries, there are likely to be multiple units of functionality, each of which may consist of a few classes. As a best practice, it’s these units of functionality that you should wrap into a single source file.

 

5.3.1. Using the part and part of keywords

Dart provides the part keyword to allow you to split code into separate files in a library. It’s used in the same file as the library keyword and needs to provide a relative path to the other source files that make up the library: for example, part "functions.dart";. You can create new, empty text files for classes.dart and functions.dart and cut and paste the classes and functions into them. They need no extra keyword. The following listing shows the complete functions.dart file.

Listing 5.8. Complete functions.dart source file

We call this a part file. You can only use it in the context of a library—it achieves nothing on its own. It’s important to note that a source file is an extract of code that could have remained in the original library file but has been extracted into a separate file for developer convenience. It has no bearing on how the code is used in terms of either public and private visibility of classes and functions or conversion to JavaScript.

After extracting your functions and classes into their respective files, import them as demonstrated in figure 5.17.

Figure 5.17. Splitting a single library file into separate source files

The part "classes.dart"; keyword takes a filename as its parameter, which should be a filename that’s relative to the library file; for example, you could put all the linked source files into a subfolder from the main loglib.dart library file:

part "sourceFiles/classes.dart";

The loglib.dart file now contains only a series of library, import, and part statements. This setup is perfectly valid and is a pattern you’ll see around the built-in Dart source libraries. Remember that in section 5.1 we noted that a library can import other libraries. If your loglib library needed to import other libraries, the import statements also appear in this file:

The order of the statements is important, as shown in figure 5.18.

Figure 5.18. part statements must come before any other source code.

Any classes and functions imported from external libraries such as import "dart:html"; become available to all part files that belong to loglib. Thus the relationship between each class and function in the library remains unchanged, although they’re organized in source files.

 

Source-file restrictions

You should be aware of the following restrictions when using the part keyword:

  • Files imported into a library with the part command need to be treated as though they’re part of the original library file. That is, they can’t contain any statements of their own. If they did, they’d potentially break the strict ordering of the library, import, and part keywords.
  • Source files can belong to only a single library in an application. loglib and webloglib couldn’t both use part "classes.dart";.
  • A class or function must exist in a single file. There is no way to make a class or a function span multiple files (there are no partial classes as in C#).

If you think of part files as being part of the same logical library file, then these restrictions make sense. You couldn’t have one part of a file also be part of a different file, and you couldn’t have a library file contain another library statement halfway down. It would also be impossible to split a class or function into two separate parts in the same file.

 

It’s important to remember that having classes and functions in different part files has no effect on privacy. They’re all considered to be in the same library, and privacy exists at the library level.

 

Remember

  • A single library file can be split into many part files.
  • External users of the library have no knowledge that the library has been split.
  • Dart treats a library file split into multiple physical part files as though it were a single library file.

 

In addition to encapsulating functionality into your library and making that available for third-party code to use, it’s also possible to run a library directly, like a script.

5.4. Packaging your libraries

In Dart, a package is a standalone application or one or more libraries put together into a single unit with a version number. Packages serve two purposes: they allow your application to easily import other people’s packages, and they let you format your own file structure in such a way as to allow your code to be packaged and imported by third parties.

The pub tool, which is built into the Dart Editor and is also available from the command line, lets you import packages that your code depends on. These packages may be hosted on a web server, in GitHub (or other Git repository), and in the pub.dartlang.org repository. If you’ve used other package managers, such as Java’s Maven or Node.js’s npm, pub performs a similar function: it automatically downloads your application’s dependencies and any nested dependencies. Pub also manages versioning conflicts, including highlighting conflicts in nested dependencies.

A file called pubspec.yaml, which is found in the root of your source’s file structure, contains all the important information to enable pub to find and download dependencies. It uses a YAML file format: a human-readable markup language that uses indentation to define sections and subsections. Listing 5.9 shows an example pubspec.yaml file. The name field is mandatory, and the version and description fields are also mandatory if you want your package hosted on pub.dartlang.org. The other fields are optional, but the important one is dependencies, which tells pub about your dependencies (if you have no dependencies outside the core Dart SDK, then you can omit the dependencies field).

Listing 5.9. Example pubspec.yaml for loglib

Pub works by using convention over configuration, and it expects a specific layout for your application. The key files and folders are shown in figure 5.19; fortunately, the Dart Editor creates this structure for you when you create a new project.

Figure 5.19. A package structure is defined by convention.

To pull the various dependencies into your project, you need to use the pub install and pub update commands, which both exist in the Dart Editor menus. These commands install or pull newer versions of the dependencies into your application’s structure and create a pubspec.lock file. Packages are downloaded into a cache, normally the .pub-cache/ folder in your home directory. The pubspec.lock file contains the actual versions of the different dependencies that pub installed (useful when you’ve specified a version range). This file can be committed into source control, which ensures that other developers on your team use the same version of any dependencies.

Once the dependencies are installed, you can refer to them using the import keyword in your library code. For example, the PackList app can use the loglib package, as shown in this snippet:

5.5. Scripts are runnable libraries

The loglib library provides logging functions to external users by providing a number of classes and functions that external code can use. It’s also possible to run the library directly—remember that a Dart script is no more than a .dart file containing a top-level main() function.

An example using the loglib library is to allow it to replay a series of log messages loaded from a web server back into the developer console. You can provide a publicly accessible function such as replay(url) that calls a server and sends each of the returned log messages to the existing private _logMsg() function.

One way of running this new replay functionality is to write a separate app that imports the loglib library and then calls replay(). This seems like a lot of work in order to call a single function. Fortunately, Dart provides another alternative. A library can also contain a main() function, and a main() function is all Dart needs in order to let the library become runnable (a main() function is the entry-point function for all Dart scripts). The following listing shows main() added to the loglib library.

Listing 5.10. Library containing a main() method

You can now use this functionality from within an HTML file by including a script tag in the associated HTML, such as

<script type="application/dart" src="loglib.dart"></script>

This calls the main() function once the code is fully loaded and ready to run.

Although it’s best practice to keep main() in the library file (that is, the file containing the library statement), you can put main() in a different part file. Remember, a function or class in a part file performs exactly the same as if it were in the main library file, and the main() function is just the same.

The implication is that every Dart app that you create can also become a library by the addition of a library declaration at the top. In this way, it becomes trivial to take an existing Dart app and turn it into a library that can be embedded in some other application by making the existing Dart app also function as a library. By adding a library statement to your PackList app, you can include it in a mashup of other applications, each of which provides independent functionality, brought together by a common theme (see figure 5.20).

Figure 5.20. You can take an existing app and turn it into a library by adding a library statement.

Dart comes with modularity and flexibility built in, and regardless of whether you start by building a library or an app, it’s incredibly easy to switch between one and the other.

5.6. Summary

Dart provides a library structure for organizing and reusing your source code. With library and part files, you can structure your code in a way that makes sense for team development and third-party reuse.

The import statement allows apps and libraries to import other libraries in a simple fashion while avoiding naming clashes through the use of library prefixes. Library-based privacy allows you to share libraries of code and functions and classes in that library that are private from code that uses your library. Libraries can also become standalone applications through the addition of a main() function, which is the entry point for any Dart script.

 

Remember

  • Libraries can import other libraries.
  • A library can also be used as a runnable script.
  • A library’s source code can be split across multiple part files.
  • Any code declared as private in a library is accessible from within any other part of that library.
  • Any code not declared as private can also be used by code that uses the library.

 

Now that you know enough Dart to build a structured app consisting of multiple libraries and files and you understand how Dart’s privacy mechanism relates to libraries rather than classes, it’s time to take a deeper look at Dart’s class, interface, and inheritance mechanisms and how they fit into the dynamic world of optional typing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.77.158