Marcus Aurelius, author of Meditations. Photograph by Pierre-Selim Copyright CC BY-SA 3.0

Meditations in your Pocket

Marcus Aurelius, author of Meditations. Photograph by Pierre-Selim Copyright CC BY-SA 3.0

Meditations in your Pocket

Meditations are a series of notes that the Roman emperor Marcus Aurelius wrote for guidance and self-improvement. While it is likely that the emperor wrote them purely for his own benefit, they have been published after his death and contain elements of Stoic philosophy that many people have consulted since.

One way achieve a bit of self-improvement, coding-wise, is to pick a small project to do with a new set of tools. In particular, I have found the Flutter framework for creating mobile apps very interesting. Unfortunately I did not have the opportunity to explore Flutter much beyond running some sample apps previously. To bring a change to this I thought it would be a nice small project to create a small app for browsing the text of “Meditations” in Flutter. I aim to show what I have built in this article, organize my thoughts, and hopefully help others unfamiliar with Flutter to get a small glimpse of what it can do.

To give a preview of the implemented functionality, here is an animation of the functionality of the app:

Navigating the text of Meditations in the app running on an Android emulator.

The full code of the application presented in this article is available to explore. If one wants to follow along by running the code, or has a similar project in mind, the first step is to install Flutter. There are quite a few dependencies needed for mobile development, but thankfully there is some excellent documentation available on how to get started from the official Flutter site. For code editing I highly recommend Visual Studio Code with the Flutter plugin, but other options are also available.

If everything is installed correctly the flutter command line tool will be available. For this project I intended to create an Android app, but the framework allows for app development for iOS (Apple) phones, as well as desktop and web targets. For Android development, aside from the development toolchain, it is probably good idea to have either an emulator or a device for Android development setup and connected. This allows us to the result of the code up and running. In fact, on of the biggest draws of the Flutter framework is Hot Reload, which allows on to make changes and quickly see the results in the running app, without reinstalling everything from scratch.

To get started with a fresh project, the command flutter create will create a directory with all the necessary files, while flutter run will ensure that the project is run on the emulator or connected device for testing, debugging and seeing our work. For the “Meditations” app, we could start by creating a new project named Meditations and making changes from there on.

The default generated project will create a bunch of files and directories required for an example app, which is a very small application that has a single button and a screen that shows how many times that button has been pressed (see the test drive part of the Flutter docs). The core part are the files main.dart and widget_test.dart, in the lib and test directories respectively. The former is responsible for defining and running the app, while the later exists for testing the functionality.

In order to get the Meditations app up and running, we will have four files in total. Similarly to the default project we have a main.dart and a widget_test.dart for defining and testing our app, but with different contents. In addition, we also have a meditations.dart file where we define how to search and navigate the text of the Meditations, as well as the file meditations_test.dart to test this search and navigation functionality.

Although we do not have to go full Test Driven Development (TDD) for such a small demo application, it is a good idea and practice to start off writing some tests to define what behavior we want to show.

We first start describing our application by walking through (some of) the contents of meditations_test.dart. It also gives us an opportunity to ease us into the Dart language that the Flutter framework uses, in case if the reader is unfamiliar with it. It would be too much for this short article to fully explain the Dart language, for which there are some excellent guides, but I hope I can make the functionality clear enough, even for readers completely new to it.

import 'package:meditations/mediations.dart';
import 'package:test/test.dart';

The first portion is importing elements that we are going to test, to be defined in the mediations.dart file, and functionality for defining tests, which is in the test package.

Next up are the tests that are in the main method. As in some other languages, the main method is where the app execution starts. In the case of testing, the tests within the ‘main’ method are executed. The tests themselves contain information about the purpose of the test, the returned value (or values) of the functionality under test and the expected value of what we believe should be returned.

Below are some examples of such tests:

  test('Checking the existance of a Book that exists.', () {
    expect(Meditations.existBook(0), true);
  });

  test('Checking the existance of a Book that can not exist.', () {
    expect(Meditations.existBook(-1), false);
  });

  test('Checking the existance of a Book that does not exist.', () {
    expect(Meditations.existBook(40), false);
  });

  test('Checking the existance of a Book Section that exists.', () {
    expect(Meditations.existBookSection(0, 1), true);
  });

  test('Checking the existance of a Book Section that can not exist.', () {
    expect(Meditations.existBookSection(0, -10), false);
  });

  test('Checking the existance of a Book Section in a non-existent Book.', () {
    expect(Meditations.existBookSection(40, 0), false);
  });

  test(
      'Checking the existance of a Book Section that does not exist in an existing Book.',
      () {
    expect(Meditations.existBookSection(0, 40), false);
  });

In order to understand what these tests want to achieve, first we need to dive deeper about what functionality that is implemented in Meditations.dart which we intend to test. The purpose of it is to easily find a particular part in the text of ‘Meditations’. ‘Meditations’ itself is divided into a total of 12 books, each with multiple sections of text. Instead of showing the full text of all books on a single screen we want to make the user able to navigate between the text of each of these sections.

Part of implementing this navigation process requires having the ability to check, for a given book number, whether that book exists. As in Dart indexes start at 0, the method existBook should return true for any integer from 0 to 11, and false otherwise. Later on we will show how we will turn the book and section numbers back to 1-indexed ones in the UI of the app. Similarly to checking the existence of a book, we want to able to check if a specific section in a specific book exists, which the method existBookSection should provide.

Of course the functionality should not end here. In particular we also want to be able to get the text of the first and last sections of Meditations. In order to keep the return of these search results simple, we also return the book and section numbers (0-indexed) along with the text where possible as well, even though for the first section, we should already know the book and section numbers.

As Dart is an object oriented language, we create a specific object of the class BookSectionText to hold these elements (more on the definition of this class a bit later).

  test('Getting the first part of Meditations returns Book 1, Section 1.', () {
    BookSectionText bst = Meditations.getFirst();
    var firstText = Meditations.book1.first;
    var expectedBookNr = 0;
    var expectedSectionNr = 0;
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, firstText);
  });

  test(
      'Getting the last part of Meditations returns last Section of the last Book.',
      () {
    BookSectionText bst = Meditations.getLast();
    var lastText = Meditations.books.last.last;
    var expectedBookNr = Meditations.books.length - 1;
    var expectedSectionNr = Meditations.books.last.length - 1;
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, lastText);
  });

The final set of tests we are going over are for the methods for finding the previous section and the next one. This seems generally a straightforward thing to do, when moving to the previous or next section in the same book. However for convenience we also want to move to the next, or previous, book if the these are unavailable in the current book, but exist in another one. So the functions for nextSection and previousSection has to have this ‘roll-over’ functionality included which will make for a nicer user interface. However even with roll-over, there are cases when we ran out of sections to show (e.g.: when wanting to get the next section after the last one). In this case we use an object of the class NoSuchBookSection to indicate this.

Below are some tests for the nextSection and previousSection functionality:

  test('Getting the next Section of a Book.', () {
    BookSectionText bst = Meditations.getNextSection(11, 3);
    var expectedBookNr = 11;
    var expectedSectionNr = 4;
    var expectedText = Meditations.books[expectedBookNr][expectedSectionNr];
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, expectedText);
  });

  test('Getting the next Section of a Book when not available.', () {
    BookSectionSearchResult bst = Meditations.getNextSection(12, 30);
    expect(bst is NoSuchBookSection, true);
  });

  test('Getting the next Section in the next Book', () {
    BookSectionText bst = Meditations.getNextSection(0, 16);
    var expectedBookNr = 1;
    var expectedSectionNr = 0;
    var expectedText = Meditations.books[expectedBookNr][expectedSectionNr];
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, expectedText);
  });

  test('Getting the previous Section of a Book.', () {
    BookSectionText bst = Meditations.getPreviousSection(0, 3);
    var expectedBookNr = 0;
    var expectedSectionNr = 2;
    var expectedText = Meditations.books[expectedBookNr][expectedSectionNr];
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, expectedText);
  });

  test('Getting the previous Section of a Book when not available.', () {
    BookSectionSearchResult bst = Meditations.getPreviousSection(0, 0);
    expect(bst is NoSuchBookSection, true);
  });

  test('Getting the previous Section in the previous Book', () {
    BookSectionText bst = Meditations.getPreviousSection(1, 0);
    var expectedBookNr = 0;
    var expectedSectionNr = 16;
    var expectedText = Meditations.books[expectedBookNr][expectedSectionNr];
    expect(bst.bookNr, expectedBookNr);
    expect(bst.sectionNr, expectedSectionNr);
    expect(bst.text, expectedText);
  });
}

Now that we got the tests for the core functionality of navigating the text of Meditations done. Next we will go over the actual implementation of what we aim to test in the file meditations.dart.

First off are the class definitions for the classes that we mentioned above:

class BookSectionSearchResult {}

// This represents the result of search, which results in a text with the attached book and section number.
class BookSectionText extends BookSectionSearchResult {
  int bookNr;
  int sectionNr;
  String text;
}

// This represent the result of a search, in which case no section is found.
class NoSuchBookSection extends BookSectionSearchResult {}

If the reader is familiar with an object oriented language, the definitions should be reasonably straightforward. The search results are represented by two types of classes that we used in our tests. An instance of a class NoSuchBookSection represents that no valid text could be returned and an instance BookSectionText represents the search results that returned the text of a specific section and its book/section numbers. These are both subclasses of the class BookSectionSearchResult which allows us easily define the return type of the methods getPreviousSection and getNextSection.

Also note that the code here contains single line comments which are denoted after a // in Dart.

Next up is the definition of the Meditations class that describes the text of Meditations by Marcus Aurelius and how to access them.

It starts with the snippet:

class Meditations {
  static final book1 = [
    '''I. Of my grandfather Verus I have learned to be gentle and meek, ... 
    // End of snippet. 

Of course quoting the full text of the representation is too much for this article. Even in the code of this demo app we only represent the full text of the first and last books in detail at the moment, although the structure to implement every section exists.

However we can say we represent the text as a list of books, where each book is itself a list of sections which are represented by the text of these sections.

In Dart, we can represent a list as a set of items in between square brackets, i.e. [], such as follows:

  static final books = [
    book1,
    book2,
    book3,
    book4,
    book5,
    book6,
    book7,
    book8,
    book9,
    book10,
    book11,
    book12
  ];

The other interesting part is that we use the keywords static and final. The static keyword indicates that the variables are class-wide, as the text in these books would be same for any instance of the Meditations class. The final keyword indicates that we do not intend to change these variables once they are defined, as the text of Meditations will not be changing in our app.

The final piece of our Meditations class are methods for accessing and navigating the texts that we described previously in our tests, such as getFirst, getLast, getPreviousSection and getNextSection. Hopefully following them should be reasonably straightforward given the comments and our tests. The only additional thing to note that these methods are also marked as static. As the Meditations class does not contain any variables that would change per instance, every one of these methods could be made available at a class level (e.g. it could be called as Meditations.getNextSection(11, 3)).

See the text of these methods below:

// We assume that there is always at least one book in the list of books and at least one section in each book with a text.

  // Checks if the book with given book number exists.
  static bool existBook(int bookNr) {
    if (bookNr < 0 || bookNr > books.length - 1) {
      return false;
    } else {
      return true;
    }
  }

  // Checks if the section with given book and section numbers exists.
  static bool existBookSection(int bookNr, int sectionNr) {
    if (!existBook(bookNr)) {
      return false;
    } else {
      var selectedBook = books[bookNr];
      if (sectionNr < 0 || sectionNr > selectedBook.length - 1) {
        return false;
      } else {
        return true;
      }
    }
  }

  // Returns the length of a book, in the number of sections.
  static int bookLength(int bookNr) {
    if (!existBook(bookNr)) {
      return 0;
    } else {
      var selectedBook = books[bookNr];
      return selectedBook.length;
    }
  }

  // Returns the search result for a given book and section number.
  static BookSectionSearchResult getText(int bookNr, int sectionNr) {
    if (!existBookSection(bookNr, sectionNr)) {
      return NoSuchBookSection();
    } else {
      var bst = new BookSectionText();
      bst.bookNr = bookNr;
      bst.sectionNr = sectionNr;
      bst.text = books[bookNr][sectionNr];
      return bst;
    }
  }

  // Returns the first section.
  static BookSectionSearchResult getFirst() {
    var selectedBook = books[0];
    if (selectedBook == null) {
      return NoSuchBookSection();
    }
    var selectedSection = selectedBook[0];
    if (selectedSection == null) {
      return NoSuchBookSection();
    } else {
      var bst = new BookSectionText();
      bst.bookNr = 0;
      bst.sectionNr = 0;
      bst.text = selectedSection;
      return bst;
    }
  }

  // Returns the last section.
  static BookSectionSearchResult getLast() {
    var selectedBook = books.last;
    if (selectedBook == null) {
      return NoSuchBookSection();
    }
    var selectedSection = selectedBook.last;
    if (selectedSection == null) {
      return NoSuchBookSection();
    } else {
      var bst = new BookSectionText();
      bst.bookNr = Meditations.books.length - 1;
      bst.sectionNr = Meditations.books.last.length - 1;
      bst.text = selectedSection;
      return bst;
    }
  }

  // Given a book- and a section number, gets the next section.
  static BookSectionSearchResult getNextSection(int bookNr, int sectionNr) {
    //Get the next section in the current book
    var nextSection = getText(bookNr, sectionNr + 1);
    //If it exits return it
    if (!(nextSection is NoSuchBookSection)) {
      return nextSection;
    } else {
      //Otherwise get the first section of the next book
      return getText(bookNr + 1, 0);
    }
  }

  // Given a book- and a section number, gets the previous section.
  static BookSectionSearchResult getPreviousSection(int bookNr, int sectionNr) {
    //Get the previous section in the current book
    var previousSection = getText(bookNr, sectionNr - 1);
    //If it exits return it
    if (!(previousSection is NoSuchBookSection)) {
      return previousSection;
    } else {
      //Otherwise get the last section of the previous book
      var previousBookNr = bookNr - 1;
      return getText(previousBookNr, bookLength(previousBookNr) - 1);
    }
  }
}

We have now covered two of the four main files responsible for the functionality of our app, notably the functionality to navigate the text of Meditations and the tests for them. The purpose of the other two is to create the app using this functionality for navigating the text with the Flutter framework (main.dart) and the tests for the app (widget_test.dart).

Let us start with the test first. One of the nice features of the Flutter framework is that one can test the full app nearly as easily than any other Dart code.

First we start importing the functionality for testing, as well as our main app that we aim to test:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meditations/main.dart';

Next up is the main method with the functionality that we aim to test. This functionality is similar to the functionality that we have shown previously of our app, seen here again below:

Navigating the text of Meditations in the app running on an Android emulator.

In particular when we are navigating the text of Meditations, we tap the icons for first, previous, next and last in the bottom navigation bar.

On the screen we not only navigate to the right text, but we also show the number of the book and section of the text that we are currently reading. We use this information to create a navigation test, as once the right icon has been tapped, we should be able to predict and check the book and section numbers that should now be on screen.

The test is all tied together with the test functionality of Flutter for setting up the widgets (the elements of interaction) of the app for testing, redrawing the elements after a (simulated) interaction and checking them (more documentation on testing Flutter widgets can be found in the Flutter documentation). The code of this for the app can be seen as below.

void main() {
  testWidgets('Bottom navigation smoke test', (WidgetTester tester) async {
    // Build the app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that the title shows Book 1 and Section 1.
    expect(find.text('Meditations Book 1 Section 1'), findsOneWidget);

    // Tap the icon for next and trigger a frame.
    await tester.tap(find.byIcon(Icons.navigate_next));
    await tester.pump();

    // Verify that the title has changed correctly.
    expect(find.text('Meditations Book 1 Section 2'), findsOneWidget);
    expect(find.text('Meditations Book 1 Section 1'), findsNothing);

    // Tap the icon for last and trigger a frame.
    await tester.tap(find.byIcon(Icons.last_page));
    await tester.pump();

    // Verify that the title has changed correctly.
    expect(find.text('Meditations Book 12 Section 26'), findsOneWidget);
    expect(find.text('Meditations Book 1 Section 2'), findsNothing);

    // Tap the icon for previous and trigger a frame.
    await tester.tap(find.byIcon(Icons.navigate_before));
    await tester.pump();

    // Verify that the title has changed correctly.
    expect(find.text('Meditations Book 12 Section 25'), findsOneWidget);
    expect(find.text('Meditations Book 12 Section 26'), findsNothing);

    // Tap the icon for first and trigger a frame.
    await tester.tap(find.byIcon(Icons.first_page));
    await tester.pump();

    // Verify that the title has changed correctly.
    expect(find.text('Meditations Book 1 Section 1'), findsOneWidget);
    expect(find.text('Meditations Book 12 Section 25'), findsNothing);
  });
}

Now all that remains is to show how the actual app is put together. We will go through the code of it here, found in main.dart, step by step.

First we start with the imports of the widget library we are using as well as our implementation of accessing the text of Meditations.

import 'package:flutter/material.dart';
import 'mediations.dart';

Next is, the very short main method, that ensures the app we are building is run when requested.

void main() {
  // The main method runs the app that was described
  runApp(MyApp());
}

As mentioned in our description of the test, this app consists of a tree of widgets (elements of interaction), the root of which we identify with MyApp in the code. This MyApp widget is the one that gets run by the main method seen above.

The widgets themselves have properties that contain the elements that the widget is using. For example the title of the app, the theme of the app and the other widgets it uses such as a widget for a home page. The main job of the widget is to implement a specific build function that describes how these various elements fit together. In the case of MyApp we are creating a MaterialApp customized to our specific needs. This includes a home page for navigating the text of Mediations, as well as a theme that uses a color (hopefully) close enough to Imperial purple.

class MyApp extends StatelessWidget {
  // The app consists of a tree of widgets, of which this is the root.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Meditations',
      theme: ThemeData(
        primarySwatch: Colors.purple,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Meditations'),
    );
  }
}

The previous Widget does not carry an internal, mutable, state directly in itself (hence why it is an extension of a StatelessWidget). However, as one can deduce from our tests, we want to keep information such as the current book and section number around and display these.

In order to get this done, we use an extension of StatefulWidget named MyHomePage which carries the state. The state itself contains the elements of book number, section number and the text to be shown on page. In addition, there are methods for manipulating the state and showing it.

For setting the state, we make use of all the functionality we built in meditations.dart to search for the next (or previous, first, last) section and change the state accordingly. Each of these state setting methods are inside a call to setState which makes the Flutter framework be aware of the state changes and, if required, it can redraw the relevant widgets to show these changes.

The methods for showing the state are reasonably straightforward. we mostly use these to translate the internally 0-indexed book- and section numbers into 1 indexed ones to match the actual numbers used in Meditations (albeit not with Roman Numerals though that would be a nice future feature for the app).

The final part is making sure there are widgets for showing the state and interacting with the state. As one can see in the animation of the screen, as well is in the test, we have a section showing the current section and page number, a text area that scrolls showing the current section and a bottom navigation bar to change the current section.

The code for all of this, when put together, can be seen below:

class MyHomePage extends StatefulWidget {
  // The homepage of the app, which also creates and holds the state.
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // The variables used to describe the state.
  int _bookNr = 0;
  int _sectionNr = 0;
  String _text = (Meditations.getFirst() as BookSectionText).text;

  // Functions to show the state variables on the screen.
  String getShowText() {
    return _text;
  }

  int getShowBookNr() {
    // The screen should book and section numbers starting with 1 instead of 0.
    return _bookNr + 1;
  }

  int getShowSectionNr() {
    return _sectionNr + 1;
  }

  // Functions to navigate the text of Meditations and update the state based on UI interaction.
  void _first() {
    // The call to setState makes the Flutter framework aware of the state changes.
    // It will ensure that the build method below will be rerun.
    setState(() {
      BookSectionSearchResult result = Meditations.getFirst();
      if (result is BookSectionText) {
        _text = result.text;
        _bookNr = result.bookNr;
        _sectionNr = result.sectionNr;
      }
    });
  }

  void _previous() {
    setState(() {
      BookSectionSearchResult result =
          Meditations.getPreviousSection(_bookNr, _sectionNr);
      if (result is BookSectionText) {
        _text = result.text;
        _bookNr = result.bookNr;
        _sectionNr = result.sectionNr;
      }
    });
  }

  void _next() {
    setState(() {
      BookSectionSearchResult result =
          Meditations.getNextSection(_bookNr, _sectionNr);
      if (result is BookSectionText) {
        _text = result.text;
        _bookNr = result.bookNr;
        _sectionNr = result.sectionNr;
      }
    });
  }

  void _last() {
    setState(() {
      BookSectionSearchResult result = Meditations.getLast();
      if (result is BookSectionText) {
        _text = result.text;
        _bookNr = result.bookNr;
        _sectionNr = result.sectionNr;
      }
    });
  }

  // Handles the action of the bottom navigation bar on screen.
  void _onBottomNavTapped(int index) {
    setState(() {
      switch (index) {
        case 0:
          {
            _first();
          }
          break;

        case 1:
          {
            _previous();
          }
          break;

        case 2:
          {
            _next();
          }
          break;

        case 3:
          {
            _last();
          }
          break;

        default:
          {
            _first();
          }
          break;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called.
    // It contains a nested widget with a app bar on top, a scrollable text area in the middle, and a navigation bar at the bottom.
    return Scaffold(
      appBar: AppBar(
        title: Text(
            '${widget.title} Book ${getShowBookNr()} Section ${getShowSectionNr()}'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            new Expanded(
                flex: 1,
                child: SingleChildScrollView(
                    child: Container(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(getShowText())))),
          ],
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.first_page),
            label: 'First',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.navigate_before),
            label: 'Previous',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.navigate_next),
            label: 'Next',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.last_page),
            label: 'Last',
          ),
        ],
        onTap: _onBottomNavTapped,
      ),
    );
  }
}

This is pretty much the full description of the Meditations app. It shows how a small application can be built up and structured. My experience with the Flutter framework was very pleasant. Most things I wanted to do were pretty straightforward and there is a large amount of documentation for the framework and setup. I like the emphasis on getting tests up and running straight from the box and the large set of pre-made widgets help a lot with getting a workable UI going very quickly. My biggest pain point was the setup of the whole Android tool-chain itself. Although there were some additional features to the Dart language (such as the upcoming, at the time of this writing, Null Safety) the language is easy to pick up and become productive with.

Of course there are tons of possible extensions to the app. Aside from including the full text of Meditations, one can improve upon the navigation, add features such as bookmarking, random section selection and more. Refactoring and extending the app so it could load in other advice/text in similar format is also a possibility.

Hopefully this article and code was clear enough to follow and my thought process came over clearly. The project has definitely increased my interest in using Dart/Flutter in another project and felt like good practice. Hopefully your next project, perhaps made with Flutter, will be similarly enjoyable and enriching to do!

Avatar
Newres Al Haider
Postdoctoral Researcher and Software Engineer

Related