Skip to main content
Free tutorial

Developer Environment Setup: Build Your First Offline Tool

Read the explanation, study the example, and practice at your own pace.

What you will learn: Install or verify the Dart development tools, organize a small project, write a useful offline text inspector, run it, and check that its results are correct.

What you will build: A command-line utility that opens a text file on your computer and reports its size, lines, non-empty lines, and word count. It needs no account, paid API, server, or third-party Dart package.

Before you begin: You need a Windows, macOS, or Linux computer, a text editor, and the free Dart SDK. The project is designed for beginners; no Flutter installation is required.

Step 1 — Understand the Idea

Imagine a small repair workshop. The editor is your workbench, the Dart SDK is your toolkit, your project folders are the labeled drawers, and the verification test is the final quality check. First organize the workshop. Then build one useful tool and verify that it works.

Our tool is called the Offline File Inspector. It reads a local UTF-8 text file and counts its bytes, lines, and words. UTF-8 is a common way to store text characters; think of it as the agreed alphabet that software uses to read a document correctly. Our program will not upload or change the file.

A realistic dependency goal: “No third-party packages” does not mean “no development tools.” The Dart SDK and your operating system are required. We are avoiding optional external Dart libraries and paid cloud services, not pretending that software runs without a platform.

Step 2 — Create the Exact Project Structure

Install Dart following the official Dart SDK guide. If you already use Flutter, its SDK includes Dart. Open PowerShell or Terminal and check the installation:

dart --version

The terminal should print the Dart SDK version. If it says that dart is not recognized, install the SDK, close the terminal, and reopen it before continuing.

Create a folder called offline_file_inspector. Inside it, create the following folders and files with the exact names. A file ending in .dart must not accidentally be saved as .dart.txt.

offline_file_inspector/
├── pubspec.yaml
├── bin/
│   └── main.dart
├── lib/
│   └── text_report.dart
├── test/
│   └── verify.dart
└── data/
    └── sample.txt

The root folder contains the project settings. bin holds the program that users run; lib holds reusable calculations; test holds checks; and data holds a sample file. All terminal commands below run from the offline_file_inspector root folder.

Step 3 — Write the Complete Working Code

Copy each code example into the matching file. These are complete files, not fragments; do not add or remove lines.

pubspec.yaml

This short configuration tells Dart the project name and supported SDK versions. Save it at the project root:

name: offline_file_inspector
description: A small offline text-file inspector using only the Dart SDK.
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'

lib/text_report.dart

This file calculates counts from text without opening files or printing messages. Save it in lib:

class TextReport {
  final int byteCount;
  final int lineCount;
  final int nonEmptyLineCount;
  final int wordCount;

  const TextReport({
    required this.byteCount,
    required this.lineCount,
    required this.nonEmptyLineCount,
    required this.wordCount,
  });
}

TextReport inspectText(String content, int byteCount) {
  if (content.isEmpty) {
    return TextReport(
      byteCount: byteCount,
      lineCount: 0,
      nonEmptyLineCount: 0,
      wordCount: 0,
    );
  }

  final lines = content.split(RegExp(r'\r\n|\n|\r'));
  if (lines.isNotEmpty && lines.last.isEmpty) {
    lines.removeLast();
  }

  final nonEmptyLines = lines.where((line) => line.trim().isNotEmpty).length;
  final words = RegExp(r'\S+').allMatches(content).length;

  return TextReport(
    byteCount: byteCount,
    lineCount: lines.length,
    nonEmptyLineCount: nonEmptyLines,
    wordCount: words,
  );
}

bin/main.dart

This is the main program. It checks the requested file, enforces a beginner-sized safety limit, reads UTF-8 text, and prints the report. Save it in bin:

import 'dart:convert';
import 'dart:io';

import '../lib/text_report.dart';

Future<int> runInspector(List<String> arguments) async {
  if (arguments.length != 1) {
    stderr.writeln('Usage: dart run bin/main.dart data/sample.txt');
    return 2;
  }

  final file = File(arguments.first);
  if (!await file.exists()) {
    stderr.writeln('File not found: ${arguments.first}');
    return 2;
  }

  try {
    final bytes = await file.length();
    const maxBytes = 1024 * 1024;
    if (bytes > maxBytes) {
      stderr.writeln('File is larger than the 1 MiB beginner safety limit.');
      return 2;
    }

    final content = await file.readAsString(encoding: utf8);
    final report = inspectText(content, bytes);

    stdout.writeln('OFFLINE FILE INSPECTOR');
    stdout.writeln('File: ${arguments.first}');
    stdout.writeln('Bytes: ${report.byteCount}');
    stdout.writeln('Lines: ${report.lineCount}');
    stdout.writeln('Non-empty lines: ${report.nonEmptyLineCount}');
    stdout.writeln('Words: ${report.wordCount}');
    return 0;
  } on FormatException {
    stderr.writeln('This file is not valid UTF-8 text.');
    return 2;
  } on FileSystemException catch (error) {
    stderr.writeln('Unable to read the file: ${error.message}');
    return 2;
  }
}

Future<void> main(List<String> arguments) async {
  exitCode = await runInspector(arguments);
}

test/verify.dart

This file runs eight checks against known inputs, using only the Dart SDK. Save it in test:

import 'dart:io';

import '../lib/text_report.dart';

bool check(String label, int actual, int expected) {
  if (actual == expected) {
    stdout.writeln('PASS $label: $actual');
    return true;
  }
  stderr.writeln('FAIL $label: expected $expected, received $actual');
  return false;
}

void main() {
  final empty = inspectText('', 0);
  final sample = inspectText('one two\nthree\n', 14);
  final blankLine = inspectText('one\n\nthree', 10);

  final results = <bool>[
    check('empty lines', empty.lineCount, 0),
    check('empty words', empty.wordCount, 0),
    check('two lines', sample.lineCount, 2),
    check('three words', sample.wordCount, 3),
    check('sample bytes', sample.byteCount, 14),
    check('blank line count', blankLine.lineCount, 3),
    check('non-empty lines', blankLine.nonEmptyLineCount, 2),
    check('blank-line words', blankLine.wordCount, 2),
  ];

  if (results.every((passed) => passed)) {
    stdout.writeln('ALL CHECKS PASSED');
    exitCode = 0;
  } else {
    stderr.writeln('CHECKS FAILED');
    exitCode = 1;
  }
}

data/sample.txt

Save these exact three lines in data/sample.txt using UTF-8. Keep the final newline if your editor adds one:

Local tools can solve real problems.
They can work without paid APIs.
Test first. Then share evidence.

Step 4 — Understand Why the Code Works

The project deliberately separates file handling from text counting. This lets the same counting function be reused in a desktop app, a command-line tool, or a future mobile interface. The following explanations follow the code from top to bottom.

pubspec.yaml: Project Settings

  • name gives the Dart project a unique local identifier.
  • description explains what the project does.
  • version identifies this edition of the utility.
  • environment and sdk describe compatible Dart versions. The quotation marks protect the version expression from YAML interpretation.
  • There is no dependencies: section because this example imports only standard Dart libraries.

lib/text_report.dart: Counting Text

  • class TextReport defines the four numbers we want to report. Each final int field stores one whole-number result that cannot be reassigned after construction.
  • const TextReport({ is the constructor, the named entry point used to create a report. Each required this.byteCount field forces the caller to provide all four counts.
  • inspectText(String content, int byteCount) accepts the document text and its original byte count. It returns a TextReport instead of printing or changing anything.
  • if (content.isEmpty) handles an empty file explicitly; the following return TextReport( supplies zeros for all counts.
  • content.split(RegExp(r'\r\n|\n|\r')) separates Windows, Unix, and older Macintosh line endings. The initial r makes the expression a raw string.
  • if (lines.isNotEmpty && lines.last.isEmpty) checks whether a final newline created an empty last item; lines.removeLast() avoids counting that item as an extra line.
  • lines.where((line) => line.trim().isNotEmpty).length counts lines containing visible characters. trim() removes surrounding whitespace for the check.
  • RegExp(r'\S+').allMatches(content).length counts runs of non-whitespace characters. This is a useful beginner definition of “word,” though it is not a linguistic word tokenizer.
  • The final return TextReport( collects the four results. The closing parentheses and braces end the constructor, conditions, function, and class; they do not add extra behavior.

bin/main.dart: Handling a Real File

  • import 'dart:convert' makes UTF-8 decoding available. import 'dart:io' provides local file access, standard output, and error output. The relative import brings in our own counting function.
  • Future<int> runInspector(List<String> arguments) async defines an asynchronous operation that eventually returns an exit-status number. async allows file operations to use await.
  • if (arguments.length != 1) requires exactly one input path; the next two lines print the correct usage and return error status 2.
  • final file = File(arguments.first) refers to the requested file. if (!await file.exists()) checks existence before reading and returns a clear error if it is missing.
  • try starts the operations for which we want predictable error handling. await file.length() reads the file length in bytes.
  • const maxBytes = 1024 * 1024 sets a 1 MiB (1,048,576-byte) beginner safety limit. if (bytes > maxBytes) rejects bigger files before attempting a whole-file read. This is a teaching limit, not a complete defense against files changing during the read.
  • await file.readAsString(encoding: utf8) opens and decodes the local text. inspectText(content, bytes) sends it to the pure counting function.
  • Each stdout.writeln( prints one report field. String interpolation such as ${report.wordCount} inserts a calculated value into the displayed message.
  • return 0 signals success. on FormatException handles invalid UTF-8, while on FileSystemException catch (error) handles file-access failures without crashing with an unexplained stack trace.
  • main( is the entry point. exitCode = await runInspector(arguments) makes the program return the appropriate success or failure status to the terminal.

test/verify.dart: Proving the Counts

  • import 'dart:io' allows success or failure messages. The second import reuses inspectText rather than copying its logic.
  • check(String label, int actual, int expected) compares one result with a known expected value. If they match, it prints PASS and returns true; otherwise it prints FAIL and returns false.
  • The three inspectText calls make examples with empty text, normal two-line text, and text containing a blank line.
  • final results = <bool>[ collects eight independently checked expectations. For example, an empty file should have zero words, and the two-line sample should have three.
  • results.every((passed) => passed) succeeds only if all eight checks are true. The program prints a final result and sets exitCode to 0 on success or 1 on failure.
  • The final braces close the conditional and function. No online test service or extra testing package is required.

data/sample.txt: Known Test Data

The three sample lines are normal UTF-8 text. Together they contain 103 bytes, 3 lines, and 17 whitespace-separated words when saved exactly as shown with Unix-style line breaks and a final newline. Windows editors that change line endings to CRLF may produce a larger byte count; the line and word counts will remain the same.

Step 5 — Run the Practical Verification Tests

Open a terminal in the project root and run these three commands, one at a time:

dart run bin/main.dart data/sample.txt
dart run test/verify.dart
dart analyze

Expected result from the first command when the sample has the exact supplied UTF-8/LF bytes:

OFFLINE FILE INSPECTOR
File: data/sample.txt
Bytes: 103
Lines: 3
Non-empty lines: 3
Words: 17

The second command must print eight PASS results followed by ALL CHECKS PASSED. The analyzer should report No issues found! for the supplied code.

Test an expected failure. Run the tool against a filename that does not exist:

dart run bin/main.dart data/does-not-exist.txt

Expected: File not found: data/does-not-exist.txt. The process must finish with an error status, not a success report.

Fix Common Problems

  • “dart” is not recognized: complete the official SDK installation, reopen the terminal, and run dart --version.
  • File not found: run the commands from the folder containing pubspec.yaml, and check the exact relative path.
  • Unexpected byte count: compare the file contents and encoding. Windows-style CRLF line breaks add bytes; the line and word counts should still match.
  • Invalid UTF-8: save the text file as UTF-8, or inspect a different valid UTF-8 text file.
  • Static analysis fails: verify each file’s full contents and spelling. A missing bracket or a file saved as .dart.txt will prevent correct analysis.
  • File larger than 1 MiB: this example intentionally refuses to load it. Later lessons will teach streaming, which processes data in smaller pieces.

Your Practical Achievement

You have made a small but functioning offline utility that processes a local file and checks its answers. You can add the source code, command output, and test results to a technical portfolio. Explain the 1 MiB limit openly. A simple project like this demonstrates good habits; it is not a guarantee of employment or client work.

Completion checklist: Dart is available; all five files match the examples; the sample report shows 3 lines and 17 words; the eight checks pass; static analysis succeeds; and the missing-file test fails safely.

Key takeaway: Independent software development begins with small reproducible projects, standard tools, clear boundaries, and honest verification. The next lesson explains how to identify and reduce unnecessary dependencies.

Further Reading

Enjoyed this? Share it