Working with Models

    Summary

    In this lesson we'll cover:

    • Using Models to represent data and functionality
    • Making our screen dynamic
    • Generics, map(), Anonymous Functions and Cascades in Dart

    The Code for This Lesson

    Check out the tourismandco repo's step/step05 branch for the code we'll cover in this lesson.

    The Significance of an App's Data Model

    • Any app will work with various "types" of data.
    • Data will have different relationships to each other defined as our "Data Model".
    • Effectively an in-memory database, where screens can fetch data as needed.

    Our Data Model

    • Models: "Location" and "Location Fact"
    • Relationships:
      • a "one to many" relationship between Location and Location Fact

    How We'll Use Our Models

    1. App loads
    2. Our Location Detail screen loads
    3. Using a hardcoded Location ID (for now), we load the name of our Location and a list of TextSection widgets for each "Location Facts" that it relates to.

    Adding Our Models

    • Note that it's important to comment your models and note what they repesent. I recommend reading through the "Effective Dart: Documentation" article on this topic.
    • The benefit of this is to make sure you can clearly define the model yourself but also make the models much more understandable by other readers.
    • We're using ordered parameters for our constructors here since there aren't that many.

    location.dart

    // models/location.dart
    
    import './location_fact.dart';
    
    /// Represents a tourism location a user can visit.
    class Location {
      final String name;
      final String imagePath;
      final List<LocationFact> facts;
    
      Location(this.name, this.imagePath, this.facts);
    
      // NOTE: implementing functionality here in the next step!
    }
    

    Using Generics in Dart

    • As you've probably noticed above and in previous lessons, we have this List<LocationFact> type.
    • "Generics" is prevalent in many programming languages. All it means is that we're specifying a data type, in this case LocationFact, for a given higher level data type, in this case, List.
    • The benefit to this is that if we try to define something other than a LocationFact in our list, a compiler error will be thrown.

    location_fact.dart

    // models/location_fact.dart
    
    /// Represents some descriptive information about a [Location].
    class LocationFact {
      final String title;
      final String text;
    
      LocationFact(this.title, this.text);
    }
    

    Adding Business Logic to Models

    • Business logic is simply defined by the functionality in an app and how data can change based on the rules or "logic" of the business.

    • When we want to associate business logic or core functionality associated with our models, we can define this in the model itself.

    • This is a common approach for simple to medium sized apps.

    • We define a static function here allowing us to simulate fetching a list of Location objects from an API. Later, we'll use a real endpoint.

    • Note: it's important to keep things consistent with single vs double quotes. Use single quotes by default if you can. See this section of the Flutter style guide for examples.

    // models/location.dart
    
    // ...
    
    class Location {
    
      // ...
    
      static List<Location> fetchAll() {
        return [
          Location('Arashiyama Bamboo Grove', 'assets/images/kiyomizu-dera.jpg', [
            LocationFact('Summary',
                'While we could go on about the ethereal glow and seemingly endless heights of this bamboo grove on the outskirts of Kyoto, the sight\'s pleasures extend beyond the visual realm.'),
            LocationFact('How to Get There',
                'Kyoto airport, with several terminals, is located 16 kilometres south of the city and is also known as Kyoto. Kyoto can also be reached by transport links from other regional airports.'),
          ]),
        ];
      }
    }
    

    Making Our Code Dynamic

    • Let's now use our new implementation of Location to fetch the first location we have in our list and inject that data into our screen.
    • Later, we'll simply load this screen based on the Location the user taps (in some "listing" type screen we have yet to implement).

    Updating location_detail.dart

    // screens/location_detail/location_detail.dart
    
    import 'package:flutter/material.dart';
    import '../../models/location.dart';
    import 'image_banner.dart';
    import 'text_section.dart';
    
    class LocationDetail extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // simply fetch the first location we have
        // NOTE: we'll be moving this to a scoped_model later
        final locations = Location.fetchAll();
        final location = locations.first;
    
        return Scaffold(
          appBar: AppBar(
            title: Text(location.name),
          ),
          body: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                ImageBanner(location.imagePath),
              ]..addAll(textSections(location))),
        );
      }
    
      List<Widget> textSections(Location location) {
        return location.facts
            .map((fact) => TextSection(fact.title, fact.text))
            .toList();
      }
    }
    
    • Update our imports above.
    • Fetch the first location available to us.
    • Update the parameters passed to our widgets.

    Using Cascades in Dart

    • The '..' above is a shortcut which allows us to invoke another method on an existing variable.
    • In this case, the [] list, before it's assigned to the children parameter, has addAll() called on it.
    • The addAll() method simply appends a list onto an existing list.
    • Note: in an upcoming version of Dart, the "spread operator" (...) will be available, allowiing us to forego the addAll() method.

    map() and Anonymous Functions

    • map() (read up on it here) allows us to take an existing list, iterate over that list and change every item using a function.
    • The function you see here is called an Anonymous Function.
    • Anonymous functions allow us to take a single statement and execute that statement without needing curly braces.
      • The (fact) here is a parameter, => denotes the one-liner functionality we can use.
    • You've seen this example before, in our main.dart as void main() => runApp(App());
    • Since map() doesn't return a List, we need to called toList().

    NOTE: you don't need to master these features of Dart early on. They're used quite often enough so you should naturally get used to them as you write more Dart code.

    Summary

    In this lesson:

    • Our app's screen now dynamically adapts whatever data we decide to define.
    • We've learned really fundamental data modeling concepts, how to organize our code and how to cleanly separate functionality from rendering logic (our widgets).
    • We also leaned some new Dart concepts that made our code more concise and easier to read.