Module 17: Packages and pub.dev -- Standing on the Shoulders of Giants

Production Polish

A developer browsing a vast library of packages on shelves, picking the right ones

🎯

Teach: How to find, evaluate, install, and use third-party packages from pub.dev in a Flutter project. See: Five popular packages in action with real code examples. Feel: Confident navigating pub.dev and adding packages without fear of breaking things.

🎙️

Imagine you need to format a date as "March 13, 2026." You could write your own date formatting function. Handle month names, day suffixes, leap years, locales... or you could type flutter pub add intl and call DateFormat.yMMMd().format(date). Done. Twelve seconds instead of twelve hours. That is the power of packages. Thousands of developers have already solved the problems you are about to face, tested their solutions, and published them for free. Your job is to find the right package, plug it in, and get back to building your app.

What Is pub.dev?

🎯

Teach: What pub.dev is and how to evaluate packages by score, recency, null safety, and popularity. See: The anatomy of a pub.dev package page and the key metrics that distinguish good packages from risky ones. Feel: Comfortable browsing pub.dev and making informed decisions about which packages to trust.

pub.dev is Dart and Flutter's official package repository. Think of it as an app store for code libraries. Every package has:

  • A readme with usage instructions and examples
  • A score based on likes, pub points (code quality metrics), and popularity
  • A changelog showing version history
  • An API reference with every class and method documented
  • Platform compatibility tags (Android, iOS, web, macOS, Linux, Windows)

When evaluating a package, look for:

  1. High pub points (120+ out of 140 is great) -- this means the code follows best practices
  2. Recent updates -- a package last updated 3 years ago might not work with current Flutter
  3. Null safety -- all modern packages should support null safety
  4. Popularity -- high popularity means more eyes on bugs and more Stack Overflow answers
🎙️

Treat pub.dev the same way you'd treat any library you're about to trust with your app. A high-scoring package backed by thousands of users and updated last month is a safe bet. A package with two likes and no updates since 2022 is a red flag, even if it does exactly what you want. I've seen teams adopt abandonware packages and regret it when Flutter upgrades broke their build and there was no maintainer to fix it. Check the score, check the "last updated" date, and check the GitHub repo to see if issues are being addressed. Five minutes of due diligence saves weeks of pain down the line.

pubspec.yaml -- Your Dependency Manifest

🎯

Teach: How pubspec.yaml manages dependencies with version constraints, and how to add, upgrade, and remove packages using the CLI. See: The pubspec.yaml dependency section with caret, exact, and range version syntax explained side by side. Feel: Confident editing pubspec.yaml and understanding what each version constraint means.

Every Flutter project has a pubspec.yaml file at the root. This is where your dependencies live.

dependencies:
  flutter:
    sdk: flutter
  # Caret syntax: compatible with 6.1.0 up to (but not including) 7.0.0
  url_launcher: ^6.1.0
  # Exact version
  google_fonts: 6.1.0
  # Range
  intl: '>=0.18.0 <1.0.0'

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

dependencies are packages your app needs to run. dev_dependencies are packages only needed during development (testing, linting, code generation).

Version Constraint Syntax

This is one of those things that seems boring until you get bitten by a version conflict at 5 PM on a Friday.

Syntax Meaning Example
^1.2.3 >= 1.2.3 and < 2.0.0 Caret -- the most common
1.2.3 Exactly this version Exact -- use sparingly
>=1.0.0 <2.0.0 Explicit range Range -- when caret is not precise enough
any Any version Dangerous -- avoid in real apps
🎙️

The caret syntax ^1.2.3 is your best friend. It says "I want version 1.2.3 or anything compatible with it." In practice, that means any version from 1.2.3 up to (but not including) 2.0.0. Why not 2.0.0? Because in semantic versioning, a major version bump (1.x to 2.x) means breaking changes. The caret syntax keeps you safe from breaking changes while still getting bug fixes and minor features.

Adding and Managing Packages

# Add a package (updates pubspec.yaml automatically)
flutter pub add url_launcher

# Get all dependencies (usually runs automatically)
flutter pub get

# Upgrade dependencies to latest compatible versions
flutter pub upgrade

# Check for outdated packages
flutter pub outdated

# Remove a package
flutter pub remove url_launcher
💡

Always use flutter pub add instead of manually editing pubspec.yaml. It picks the latest compatible version and formats the file correctly.

The Five Packages Every Flutter Developer Should Know

🎯

Teach: Practical usage of five high-value packages that solve everyday app development problems. See: Complete code examples for each package, ready to copy into a project. Feel: Like you just unlocked a toolbox full of professional-grade tools.

google_fonts -- Beautiful Typography in One Line

Instead of downloading font files, bundling them in your assets, and registering them in pubspec.yaml, just call GoogleFonts.lobster():

import 'package:google_fonts/google_fonts.dart';

// On a single widget
Text(
  'Hello World',
  style: GoogleFonts.lobster(fontSize: 30),
)

// As the app's default text theme
MaterialApp(
  theme: ThemeData(
    textTheme: GoogleFonts.latoTextTheme(),
  ),
)

The package downloads fonts on first use and caches them. You get access to the entire Google Fonts library -- over 1,000 font families -- with zero configuration.

flutter_slidable -- Swipe Actions on List Items

🎙️

You know that satisfying swipe-to-delete gesture in your email app? Where you slide left and a red "Delete" button reveals itself? That takes about ten lines of code with flutter_slidable. Without it, you would be implementing custom gesture detection, animations, and hit testing from scratch. Trust me, use the package.

import 'package:flutter_slidable/flutter_slidable.dart';

Slidable(
  endActionPane: ActionPane(
    motion: const ScrollMotion(),
    children: [
      SlidableAction(
        onPressed: (context) => _deleteItem(index),
        backgroundColor: Colors.red,
        foregroundColor: Colors.white,
        icon: Icons.delete,
        label: 'Delete',
      ),
    ],
  ),
  startActionPane: ActionPane(
    motion: const ScrollMotion(),
    children: [
      SlidableAction(
        onPressed: (context) => _emailContact(index),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        icon: Icons.email,
        label: 'Email',
      ),
    ],
  ),
  child: ListTile(
    title: Text('Contact ${index + 1}'),
    subtitle: Text('Slide me left or right'),
  ),
)

endActionPane reveals actions when swiping from right to left. startActionPane reveals actions when swiping left to right. The motion parameter controls the slide animation style -- ScrollMotion, DrawerMotion, StretchMotion, and BehindMotion each have a different feel.

cached_network_image -- Smart Image Loading

Loading images from the internet is slow. Loading the same image from the internet every time the user scrolls past it is wasteful. cached_network_image downloads once, caches to disk, and loads from cache on subsequent views.

import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: 'https://example.com/photo.jpg',
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
)

Three lines. Automatic disk caching, a loading spinner while the image downloads, and an error fallback if the network fails. Compare that to manually managing Image.network with error handling, caching logic, and loading states.

intl -- Dates, Numbers, and Internationalization

The intl package handles date formatting, number formatting, and message pluralization:

import 'package:intl/intl.dart';

// Date formatting
final now = DateTime.now();
print(DateFormat.yMMMd().format(now));         // "Mar 13, 2026"
print(DateFormat('EEEE, MMMM d').format(now)); // "Friday, March 13"
print(DateFormat.jm().format(now));             // "2:30 PM"

// Number formatting
print(NumberFormat.currency(symbol: '\$').format(1234.56)); // "\$1,234.56"
print(NumberFormat.compact().format(1500000));                // "1.5M"
print(NumberFormat.percentPattern().format(0.85));            // "85%"

DateFormat uses pattern strings based on the Unicode CLDR standard. Common patterns:

Pattern Example Output
yMMMd() Mar 13, 2026
yMMMMd() March 13, 2026
EEEE Friday
jm() 2:30 PM
yMd() 3/13/2026

url_launcher -- Opening URLs, Email, Phone

When a user taps a phone number, you want to open the dialer. When they tap an email address, you want to open their email app. When they tap a website link, you want to open the browser. url_launcher handles all of these:

import 'package:url_launcher/url_launcher.dart';

Future<void> openUrl(String url) async {
  final uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri, mode: LaunchMode.externalApplication);
  } else {
    throw 'Could not launch $url';
  }
}

// Usage
openUrl('https://flutter.dev');           // Opens browser
openUrl('mailto:alice@example.com');      // Opens email app
openUrl('tel:+15551234567');              // Opens phone dialer
openUrl('sms:+15551234567');              // Opens SMS app
🎙️

Notice the canLaunchUrl check before launchUrl. This is not optional politeness -- it is essential. On some platforms, certain URL schemes are not available. A web app cannot open a phone dialer. An iOS simulator might not have an email client configured. Always check first, then launch.

🎯

Teach: The url_launcher pattern of check-then-launch for different URL schemes. See: All four URL schemes (https, mailto, tel, sms) in action. Feel: Ready to make any piece of contact information tappable and actionable.

Combining Packages in Practice

🎯

Teach: How to wire multiple packages together in a single widget without the code becoming tangled. See: A contact list item that uses flutter_slidable, cached_network_image, intl, and url_launcher in one cohesive component. Feel: That orchestrating several packages is manageable when each one handles its own domain cleanly.

The real power of packages shows up when you combine several of them in a single screen. Here is a contact list item that uses three packages at once:

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';

class ContactListItem extends StatelessWidget {
  final Contact contact;
  final VoidCallback onDelete;

  const ContactListItem({
    super.key,
    required this.contact,
    required this.onDelete,
  });

  Future<void> _launch(String url) async {
    final uri = Uri.parse(url);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Slidable(
      endActionPane: ActionPane(
        motion: const ScrollMotion(),
        children: [
          SlidableAction(
            onPressed: (_) => _launch('tel:${contact.phone}'),
            backgroundColor: Colors.green,
            icon: Icons.phone,
            label: 'Call',
          ),
          SlidableAction(
            onPressed: (_) => onDelete(),
            backgroundColor: Colors.red,
            icon: Icons.delete,
            label: 'Delete',
          ),
        ],
      ),
      startActionPane: ActionPane(
        motion: const ScrollMotion(),
        children: [
          SlidableAction(
            onPressed: (_) => _launch('mailto:${contact.email}'),
            backgroundColor: Colors.blue,
            icon: Icons.email,
            label: 'Email',
          ),
        ],
      ),
      child: ListTile(
        leading: CircleAvatar(
          backgroundImage: CachedNetworkImageProvider(contact.avatarUrl),
        ),
        title: Text(contact.name),
        subtitle: Text(
          'Added ${DateFormat.yMMMd().format(contact.addedDate)}',
        ),
      ),
    );
  }
}

Four packages working together in a single widget: flutter_slidable for swipe actions, cached_network_image for the avatar, intl for the date, and url_launcher for the phone and email actions. Each package handles its domain. Your code stays focused on composing them together.

🎙️

This is the real skill with packages. It is not just knowing that url_launcher exists -- it is knowing how to wire five packages together into a cohesive feature without the code turning into spaghetti. The key is keeping each package's usage isolated. The Slidable handles gestures. CachedNetworkImage handles the avatar. DateFormat handles the date. url_launcher handles the launch. Your widget is the orchestra conductor, and each package plays its instrument.

There Are No Dumb Questions

🎯

Teach: Answers to common package management questions about dependency count, abandoned packages, version conflicts, and dev_dependencies. See: Practical guidance for the real-world decisions developers face when managing third-party code. Feel: Prepared to handle package issues confidently instead of being intimidated by dependency management.

Q: How many packages is too many?

A: There is no magic number, but every package adds to your app's size and build time. The real question is: does this package save me significant development time and is it well-maintained? If yes, add it. If you are adding a package for something you could write in 10 lines, maybe write those 10 lines instead.

Q: What happens if a package is abandoned?

A: Check the package's pub.dev page for the last update date. If it has not been updated in over a year, consider alternatives. You can also fork a package and maintain your own version if necessary. The Dart team also provides a "discontinued" flag that package authors can set.

Q: Do I need to worry about package conflicts?

A: Dart's package manager (pub) resolves version constraints automatically. If two packages require incompatible versions of a shared dependency, flutter pub get will fail with an error explaining the conflict. The fix is usually upgrading one or both packages to newer versions with compatible constraints.

Q: What is the difference between dependencies and dev_dependencies?

A: dependencies are bundled into your final app. dev_dependencies are only available during development -- things like testing frameworks, linters, and code generators. When your app is compiled for release, dev_dependencies are not included. Put testing packages in dev_dependencies to keep your app size smaller.

🎙️

The "how many is too many" question has no hard number, but here's a useful heuristic. Every package is a small ongoing cost — build time, app size, update work — that you pay in exchange for someone else's engineering. If the package does something genuinely hard, that trade is obviously worth it. If the package wraps ten lines of standard library code, you're probably better off writing it yourself. A well-regarded app I've worked on had around forty packages; a poorly-maintained one had over a hundred. The latter's build times were measured in minutes and every Flutter upgrade turned into a week-long fire drill. Be deliberate about what you add.

🔄

Where this fits: Packages extend everything you have learned so far. google_fonts enhances theming (Module 12). flutter_slidable enhances list interactions (Module 9). cached_network_image improves image loading (Module 7). intl and url_launcher are utilities you will use in almost every real app.

📝 Module Quiz

Test what you learned. 45 seconds per question. Passing score is set per module. Your best attempt is saved in your browser so you can track progress -- nothing is sent to a server.

Sharpen Your Pencil

🎯

Teach: How to integrate all five packages into a complete contact directory app from scratch. See: A step-by-step exercise that builds a multi-screen app using google_fonts, flutter_slidable, cached_network_image, intl, and url_launcher. Feel: Ready to add professional-grade package functionality to any Flutter project independently.

Exercise: Contact Directory App

Build a complete contact directory app using all five packages. This exercise ties everything together.

Step 1: pubspec.yaml

Add google_fonts, flutter_slidable, cached_network_image, intl, and url_launcher with caret version syntax. Run flutter pub get.

Step 2: Contact Model

Create a Contact class with name, email, phone, website, avatarUrl, and addedDate fields. Include a static sampleContacts() method returning at least 6 contacts with real placeholder avatar URLs (e.g., https://i.pravatar.cc/150?img=1).

Step 3: Contact List Screen

  • Apply a Google Font to the AppBar title
  • Render contacts using Slidable with swipe actions: Call (green, right-swipe), Email (blue, left-swipe), Delete (red, right-swipe)
  • Use CachedNetworkImage inside CircleAvatar for avatars with a loading spinner and error fallback
  • Format addedDate with DateFormat as "Added MMM d, yyyy"
  • Call slide action uses url_launcher with tel: scheme
  • Email slide action uses url_launcher with mailto: scheme
  • Delete action removes the contact and updates the UI

Step 4: Contact Detail Screen

  • Large avatar (radius 60) with CachedNetworkImage
  • Name displayed in a different Google Font than the list screen
  • Tappable rows for email (mailto:), phone (tel:), and website (https:) using url_launcher
  • "Member since" date formatted with DateFormat.yMMMMd()

Step 5: main.dart

  • Apply a GoogleFonts text theme to the entire app
  • Navigate from list to detail on contact tap
🎙️

Take a moment to appreciate how much this exercise would have taken without packages. Custom swipe-to-act gestures are hundreds of lines of gesture detection. Network image caching is its own library. Launching phone and email URLs requires platform channels. Date formatting for a dozen locales is a small project by itself. Here, each package reduces a hard problem to a one-liner, and your job is to wire them together. That's the real skill with packages — not knowing every API, but knowing which tool fits which job. Build this app, and the pattern of "find a package, read the example, adapt it to my screen" becomes second nature.

💡

The best developers are not the ones who write everything from scratch. They are the ones who know which packages to use and how to combine them effectively.

1 / 1