Giter Site home page Giter Site logo

workshop_vaxxometer's Introduction

Vaxxometer Workshop, 08/01/2021

Let's check your flutter environment first:

flutter doctor -v

Create new project

flutter create vaxxometer

Create a git repository in the new project

cd vaxxometer

Open editor of your choice & run the app

Screenshot 2021-01-07 at 17 44 42

Modify contents of the counter to create simple Vaxxometer

Acceptance Criteria:

  • Vaccination Data is fetched from RKI servers: https://rki-vaccination-data.vercel.app/api
  • If data is loaded display it as a list
  • If data is being fetched display loading spinner
  • If data failed to load, display error message
  • Add button allowing toggling between alphabetical order and order based on vaccination progress
  • By default sort list in alphabetical order
  • Tapping on an item should display a tapped item in detail view (see screenshot)

"Design spec":

Screenshot 2021-01-07 at 23 14 07Screenshot 2021-01-07 at 23 14 11

Change Title of The App Bar

Hot reload and behold.

Fetch Data from internet

https://flutter.dev/docs/cookbook/networking/fetch-data

Visit pub.dev, and copy-paste latest version of http package into your pubspec file.

Don't forget about necessary permissions for the platform you're developing for:

https://stackoverflow.com/questions/61196860/how-to-enable-flutter-internet-permission-for-macos-desktop-app

You might need to cold restart the app after chaning platform stuff.

Fetch string data from our endpoint

Copy code snippet from this web page (including import)

https://flutter.dev/docs/cookbook/networking/fetch-data

Modify it to use with the RKI endpoint:

Future<http.Response> fetchData() {
  return http.get('https://rki-vaccination-data.vercel.app/api');
}

void main() {
  fetchData().then((value) => print(value.body));

  runApp(MyApp());
}

HOT RESTART the app and check debug console if the request response is logged:

Screenshot 2021-01-07 at 18 09 52

Write a class for the vaccination data

First inspect the response in json editor to see what you will need:

Screenshot 2021-01-07 at 18 13 46

we need a class, e.g. VaccineStatus that has following fields

  • total, integer, number of people
  • rs, unsure, let's ignore this one
  • vaccinated, integer, number of people
  • difference_to_the_previous_day, integer, number of people
  • quote, float, looks like vaccinations per 100 people

This is what this class looks like in dart:

class VaccineStatus {
  const VaccineStatus(
      {this.total,
      this.vaccinated,
      this.difference_to_the_previous_day,
      this.quote});
  final int total;
  final int vaccinated;
  final int difference_to_the_previous_day;
  final double quote;
}

We need to add a factory method to parse the following json:

{"total": 3644826, "rs": "11", "vaccinated": 24159, "difference_to_the_previous_day": 2204, "vaccinations_per_1000_inhabitants": 6.583746901136969, "quote": 0.66}

Let's quickly add that factory and then a test method:

  factory VaccineStatus.fromJson(Map<String, dynamic> json) {
    return VaccineStatus(
      total: json['total'],
      vaccinated: json['vaccinated'],
      difference_to_the_previous_day: json['difference_to_the_previous_day'],
      quote: json['quote'],
    );
  }
test('json parsing Berlin', () {
    final mock_data_berlin_decoded = jsonDecode(mock_data_berlin);
    final berlinStatus = VaccineStatus.fromJson(mock_data_berlin_decoded);
    expect(berlinStatus.total, 3644826);
    expect(berlinStatus.vaccinated, 24159);
    expect(berlinStatus.quote, 0.66);
  });

Parse the rest of the json

The VaccineStatus doesn't contain name of the state, instead the name of the state is used as a key in the json map. This is rather inconvienent but nothing that we cannot handle.

Let's create a class for that:

class StateEntry {
  StateEntry({this.status, this.name});
  final VaccineStatus status;
  final String name;
}

List<StateEntry> parseResponse(String jsonStr) {
  throw UnimplementedError();
}

Write another test for previous method:

  test('make a list from json', () {
    final states = parseResponse(full_json);
    expect(states.isNotEmpty, true);
  });

Provide parseResponse implementation

List<StateEntry> parseResponse(String jsonStr) {
  final json = jsonDecode(jsonStr);
  final statesMap = json["states"] as Map<String, dynamic>;
  return statesMap.keys.map((key) {
    final vaccineStatusJson = statesMap[key];
    return StateEntry(
      status: VaccineStatus.fromJson(vaccineStatusJson),
      name: key,
    );
  }).toList();
}

Improve our fetchData method

Future<List<StateEntry>> fetchData() async {
  final response =
      await http.get('https://rki-vaccination-data.vercel.app/api');

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return parseResponse(response.body);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load vaccination data');
  }
}

Wrap our existing body with a FutureBuilder widget

We want to display a loading state for our list, the list or error. We'll use FutureBuilder widget for this (explainer in video below):

      body: FutureBuilder<List<StateEntry>>(
        future: fetchData(),
        builder: (context, snapshot) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          );
        }
      ),

...and remove other fetch from main

Handle respective states

        FutureBuilder<List<StateEntry>>(
          future: fetchData(),
          builder: (context, snapshot) {

            // an error occured
            if (snapshot.hasError) {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      'An error occured ${snapshot.error}',
                    ),
                  ],
                ),
              );
            }

            // there's no data yet
            if (!snapshot.hasData) {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    CircularProgressIndicator(),
                    Text(
                      'Loading',
                    ),
                  ],
                ),
              );
            }

            // we have data
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Text(
                    'We have data ${snapshot.data}',
                  ),
                ],
              ),
            );
          })

Add ListView widget

We have a list of elements, it would be cool if we displayed them as a scrollable list.

            // we have data
            final items = snapshot.data;
            return ListView.builder(
              itemBuilder: (context, index) => Text(items[index].name),
              itemCount: items.length,
            );

Create Dedicated Widget for the list view item

class StateEntryWidget extends StatelessWidget {
  final StateEntry entry;

  const StateEntryWidget({Key key, this.entry}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text(entry.name);
  }
}

Add some more information to the list cell

class StateEntryWidget extends StatelessWidget {
  final StateEntry entry;

  const StateEntryWidget({Key key, this.entry}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Column(
          children: [
            Text(entry.name),
            Text(
                "${entry.status.vaccinated} out of ${entry.status.total} vaccinted"),
          ],
        ),
        Text("${entry.status.quote}")
      ],
    );
  }
}

Make it look "pretty"

class StateEntryWidget extends StatelessWidget {
  final StateEntry entry;

  const StateEntryWidget({Key key, this.entry}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  entry.name,
                  style: Theme.of(context).textTheme.headline5,
                ),
                Text(
                    "${entry.status.vaccinated} out of ${entry.status.total} vaccinted"),
              ],
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(
            "${entry.status.quote}%",
            style: Theme.of(context).textTheme.headline4,
          ),
        )
      ],
    );
  }
}

Add sorting

We want to be able to sort:

  • byName
  • byQuota
  • byVaccinatedCount
List<StateEntry> sortByQuotaDesc(List<StateEntry> input) {
  final output = List<StateEntry>.from(input);
  output.sort((a, b) => b.status.quote.compareTo(a.status.quote));
  return output;
}

List<StateEntry> sortByVaccinatedDesc(List<StateEntry> input) {
  final output = List<StateEntry>.from(input);
  output.sort((a, b) => b.status.vaccinated.compareTo(a.status.vaccinated));
  return output;
}

List<StateEntry> sortByNameAsc(List<StateEntry> input) {
  final output = List<StateEntry>.from(input);
  output.sort((a, b) => a.name.compareTo(b.name));
  return output;
}

Make sorting use extension

https://dart.dev/guides/language/extension-methods

extension StateEntrySortingExtensions on List<StateEntry> {
  List<StateEntry> sortedByQuotaDesc() {
    final output = List<StateEntry>.from(this);
    output.sort((a, b) => b.status.quote.compareTo(a.status.quote));
    return output;
  }

  List<StateEntry> sortedByVaccinatedDesc() {
    final output = List<StateEntry>.from(this);
    output.sort((a, b) => b.status.vaccinated.compareTo(a.status.vaccinated));
    return output;
  }

  List<StateEntry> sortedByNameAsc() {
    final output = List<StateEntry>.from(this);
    output.sort((a, b) => a.name.compareTo(b.name));
    return output;
  }
}

Toggle sorting mode using floating action button

We need different icons

Material Icon Set

We'll use:

  • sort_by_alpha for for name sorting
  • accessibility/family_restroom for vaccinated count sorting
  • trending_up for quota sorting

Check with hot reload if the icons are there!

Define enum class for possible sorting types

enum SortingType { byQuota, byVaccinated, byName }

Replace counter value with sortingType, add extension method accepting enum:

  List<StateEntry> sortedBy(SortingType sortingType) {
    switch (sortingType) {
      case SortingType.byQuota:
        return sortedByQuotaDesc();
      case SortingType.byVaccinated:
        return sortedByVaccinatedDesc();
      case SortingType.byName:
        return sortedByNameAsc();
    }
  }

Add extension method on enum to display proper tooltip and icon for the floating action button

extension SortingTypeExt on SortingType {
  IconData get iconData {
    switch (this) {
      case SortingType.byQuota:
        return Icons.trending_up;
      case SortingType.byVaccinated:
        return Icons.family_restroom;
      case SortingType.byName:
        return Icons.sort_by_alpha;
    }
  }

  String get tooltip {
    switch (this) {
      case SortingType.byQuota:
        return "Sort by Percentage";
      case SortingType.byVaccinated:
        return "Sort by Vaccinated Count";
      case SortingType.byName:
        return "Sort by Name";
    }
  }
}

Finally make the floating action button callback update widget's state:

void _switchSortingType() {
    setState(() {
      final nextIndex = SortingType.values.indexOf(sortingType) + 1;
      sortingType = SortingType.values[nextIndex % SortingType.values.length];
    });
  }

floatingActionButton: FloatingActionButton(
        onPressed: _switchSortingType,
        tooltip: sortingType.tooltip,
        child: Icon(sortingType.iconData),
      )

Make items on the list clickable

Wrap item with InkWell and display message saying e.g. Hello from Berlin! using SnackBar:

class StateEntryWidget extends StatelessWidget {
  final StateEntry entry;

  const StateEntryWidget({Key key, this.entry}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        Scaffold.of(context)
            .showSnackBar(SnackBar(content: Text("Hello from ${entry.name}!")));
      },
      child: Row(
      /// ...

Navigate To Detail View

Create a screen and navigate to it instead of displaying snack bar: https://flutter.dev/docs/cookbook/navigation/navigation-basics

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}
onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  );
}

Send data to a new screen

Pass argument to your new route

onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => SecondRoute(
              entry: entry,
            ),
          ),
        );
      },

Display place name in the app bar:

class SecondRoute extends StatelessWidget {
  const SecondRoute({Key key, this.entry}) : super(key: key);
  final StateEntry entry;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(entry.name),
      ),
      /// put any kind of widget you wish here
      body: Placeholder(),
    );
  }
}

Publish your work to codemagic.io as a static page

https://docs.codemagic.io/publishing/publishing-to-codemagic-static-pages/

Optional

  • Use json_serializable
  • Add widget test & mock API call with a repository and get_it
  • Use BLoC pattern
  • Add Theme switching

workshop_vaxxometer's People

Contributors

vishna avatar

Stargazers

 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.