Welcome, Guest User :: Click here to login

Logo 67443

Lab 11: RepoBrowser (Flutter)

Due Date: December 03

Objectives

  • Learn the basics of Flutter Programing from Scratch
  • Experience working with Widgets
  • Practice building hybrid apps for iOS, Android, and Web (Chrome)

README.md

Lab 11: Flutter RepoBrowser

Flutter Install

  1. If you have not done so already install Flutter Follow the installation instructions. This may take 20-30 minutes, so please be prepared. If you haven't already, you will want to install one of the preferred IDE's like VS Code and associated Flutter & Dart plugins. (Aside: To build for Chrome you will need to switch to Flutter's beta channel.)

  2. Run flutter doctor to make sure everything is installed correctly. Ideally you should get the following output:

Flutter doctor results

  1. Follow any instructions for fixing errors. Some issues students have run into are:
  • Doctor does not see your Android Studio Flutter/Dart plugins ~> install the plugins in Android Studio, then see this bugfix
  • There are no Connected Devices ~> start the iOS simulator with flutter emulators --launch ios

  1. Test your setup by building the generic Flutter project. Use VS Code to create a new Flutter project called flutter_test. In VS Code, this can be done easily with View > Command Palette > Flutter: New Project. After the code auto-generates, look at the lower right hand bar and see which device is selected; in these labs I will be choosing an iOS simulator because that's what I have and what I teach in other classes. Then run this generic project by pushing F5 or using VS Code option of Run > Run without debugging. Assuming everything is set up, you should see something like this:

Flutter test app running


RepoBrowser -- Part 1: Basic app structure

In this first part, we are just going to set up a few things to the project that are good practices when we build our app for the course project. As discussed in class, we will separate out our code into smaller, more manageable and understandable, components.

  1. First thing is to create a new Flutter project as you have previously. To make life a little easier, we are going to just clear out all the contents of main.dart after the class MyApp; that is, get rid of the stateful widget and its associated state. This will get rid of the demo code and give us a cleaner slate to work with.

  2. We begin by creating some folders in the lib directory to contain these components. Create folders for models, views, and utils.

  3. The utils folder is where we will keep utility-type files that can be used throughout the app to help with certain tasks. In this case we will create two such files. The first is a strings.dart file with the following content: const appTitle = 'Repo Browser';. That's it for this file, but if we had other strings in the project that were appearing in multiple places, this is where they would go. The nice thing would be that any modifications we wanted to make to strings here would apply throughout the app, keeping the interface consistent.

    The second file we need is styles.dart and here we want to define two styles -- one larger and bolder for the repo titles and another smaller for the descriptions. Add in the following:

    import 'package:flutter/material.dart';
    
    final repoNameFont = const TextStyle(
        fontSize: 20.0, fontWeight: FontWeight.bold);
    final descriptionFont =
        const TextStyle(fontSize: 14.0, fontWeight: FontWeight.normal);
    
  4. Next up is models. Here we will add a file called repo.dart and add the following struct that will define the essential elements of a repository for us:

    class Repo {
      const Repo(this.name, this.htmlURL, this.description);
      final String name;
      final String htmlURL;
      final String description;
    }
    
  5. Let's go back to main.dart to make a few quick changes. At the top of the file after the initial import, add the following: import 'package:repo_browser/utils/strings.dart' as strings;. Now use those strings by changing the title in MyApp to title: strings.appTitle,. While we are at it, get rid of the debug banner and change the color to something other than blue. Finally, reset the home to: home: new ListRepos(), and you're all set.

    Oh dear, we have an error. We haven't actually created the ListRepos class yet, so let's do that in Part 2.

Part 2: Creating the Repo List View

  1. Go to the views directory and add in the file list_repos.dart. In that new file, let's add the following skeleton code:

    import 'package:flutter/material.dart';
    import 'package:repo_browser/utils/strings.dart' as strings;
    
    
    // The basic widget
    class ListRepos extends StatefulWidget {
      const ListRepos({Key? key}) : super(key: key);
    
      @override
      _ListReposState createState() => _ListReposState();
    }
    
    // Actual state for the widget
    class _ListReposState extends State<ListRepos> {
    
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text(strings.appTitle),
          ),
        );
      }
    }
    

    Notice again we are using our constant string for appTitle here after we import the file. Now having this skeleton in place, we should be able to go back to main.dart and import this file to get the class and remove the error message. Run this app and see that all the basics are in place and it runs.

  2. Back to the list_repos.dart file, we want to go to the _ListReposState and add in a variable called _repos which is an empty array (set the type to dynamic with <dynamic>[])

  3. Now is the tricky part. We want to create a Future which we said in class is an object representing a delayed computation and that Future will make a call to the GitHub API to retrieve popular repos and use that data to populate the _repos we just created. To start that process, we first add the following code to our list of import statements:

    import 'dart:convert';
    import 'package:http/http.dart' as http;
    

    When we do this, we get an error message because the http package hasn't been added to our dependencies list. To do that, go to pubspec.yaml and to the list of dependencies, add http: ^0.13.3 (do this after the flutter dependency). It should look like this:

    dependencies:
      flutter:
        sdk: flutter
      http: ^0.13.3
    

    If you are using VS Code and it is configured with the proper extensions, then saving this file will automatically run flutter pub get, but if not you can run it yourself in the terminal.

  4. Next, we have to use this http library to actually GET to get the data from GitHub with the following:

    Future<void> _loadData() async {
      const dataUrl =
        'https://api.github.com/search/repositories?q=language%3Apython&sort=stars&order=desc';
      final response = await http.get(Uri.parse(dataUrl));
    
    }
    

    As we said in class, this will make an asynchronous call to GitHub to retrieve some data. Look at this data right now by putting https://api.github.com/search/repositories?q=language%3Apython&sort=stars&order=desc in your web browser. (Aside: if you don't already have the JSON Formatter extension for Chrome installed, this might be a good time.) We have this data, but now we need to do something with it.

  5. Inside the _loadData() method, we want to include setState() and it is here that we will decode and parse the JSON data into something we can use. Let's start with:

    setState(() {
      final Map<String, dynamic> repoMap = json.decode(response.body);
      
    
    });
    

    Now we don't want everything, just the data to make a list of repos. Add to this List<dynamic> data and set it equal to the element of repoMap that contains the actual data of individual repos. (This will be clear if you did the previous step and looked at the raw data in the browser.)

  6. Having this data list, we now want to loop through it (for loop works well here) and for each case, we want to get the key data and add it to a Repo object and then add that object to our own _repos list. The following can work as part of your for loop:

        final name = item['name'] as String? ?? '';
        final url = item['html_url'] as String? ?? '';
        final description = item['description'] as String? ?? 'N/A';
        final repo = Repo(name, url, description);
        _repos.add(repo);
    

    You are getting an error here because you haven't imported the model, so be sure to do that. ;-)

    Do you remember why we are using String? ?? '' or String? ?? 'N/A' in this code? If so, post your answer in the #labs channel of our Slack workspace. If someone has posted the fully correct answer, just give it a thumbs up and you will be given the same bonus credit. However, if their answer is not as complete (or just wrong), please add your own answer in to get the bonus credit. (Note: to be clear, you must respond on Slack to get full credit for the lab, but everyone who writes/selects the correct answer gets a bonus.)

  7. Now that we have this code, we actually need to call it. In our state object, we will use the initState() method that is called on initialization to run our method. Do this with the following:

    @override
    void initState() {
      super.initState();
      _loadData();
    }
    
  8. Time to eat cookies.

  9. Now that we have data, we need to display that in a ListView widget, but first we need a custom widget that will describe what will be in each list item. We can do that with the following:

    Widget _buildRow(int i) {
      return Stack(children: <Widget>[
    
        // RepoTitle CONTAINER
        Container(
            padding: EdgeInsets.only(left: 10.0, right: 37.0, top: 5.0),
            child: Text('${_repos[i].name}', style: styles.repoNameFont),
            
        ),
    
        // RepoDescription CONTAINER
        Container(
          padding:
              EdgeInsets.only(left: 19.0, right: 37.0, top: 51.0, bottom: 5.0),
          child: Text('${_repos[i].description}', style: styles.descriptionFont),
        ),
    
      ]);
    }
    

    Oops, this is giving us an error because we haven't imported styles.dart as styles (similar to what we did with strings). Fix that now.

  10. With that custom widget in place, go to the state's build() method and after the appBar add in a body which creates the list view and uses our custom widget:

      body: ListView.separated(
          itemCount: _repos.length,
          itemBuilder: (BuildContext context, int position) {
            return _buildRow(position);
          },
          separatorBuilder: (context, index) {
            return const Divider();
          }),
    

    If you run the project, you should have a working list of popular Python repos. ::fistbump::

Part 3: Creating the Web View

  1. While this is nice, it'd be even nicer to click on the repo title and go to a page showing the repo on GitHub. First step is to convert the repo title container from Text to a TextButton. This can be done by replacing that container with the following:

      Container(
          padding: EdgeInsets.only(left: 10.0, right: 37.0, top: 5.0),
          child: TextButton(
            child: Text('${_repos[i].name}', style: styles.repoNameFont),
            onPressed: () {
              String htmlURL = _repos[i].htmlURL;
              Navigator.push(
                  context,
                  new MaterialPageRoute(
                      builder: (BuildContext context) =>
                          new ShowRepo(htmlURL)));
            },
          )
      ),
    
  2. Of course, once more, we have an error because there is no ShowRepo() class. Let's add a new file to the views called show_repo.dart and add the following base code:

    import 'package:flutter/material.dart';
    
    
    class ShowRepo extends StatelessWidget {
      final String htmlURL;
      ShowRepo(this.htmlURL);
      @override
      Widget build(BuildContext context) {
        print("Show Repo Details");
    
        return new Scaffold(
          appBar: new AppBar(
            title: new Text("Repository Details"),
          ),
    
    
            );
      }
    }
    

    Now go back to list_repos.dart and import this new file and the error should go away. Reload the project and clicking on the title should take us to a mostly empty page. One more thing, let's make the repo titles stand out more and signal to the user they are clickable by changing their color to the same color as the app header. (Now where would I go to change the repo title style so that anywhere the style was used, it would be changed? Hmmm...)

  3. Back to our ShowRepo() class to add in a web view in the body. Before we can do this, we need to add in the web view plugin from Flutter. Go back to pubspec.yaml and add to the dependencies after 'http', webview_flutter: ^2.0.12 and save (or rerun flutter pub get) to install.

  4. Gotcha with Macs: to use this web view plugin, you need to install the Cocoapods gem (many Flutter plugins use cocoapods, so good to have installed). Do that with gem install cocoapods. After that comes the trick. You need to go to the ios/Runner directory, open Info.plist and near the end (after UIViewControllerBasedStatusBarAppearance is set to false, but before </dict>) and the following:

    <key>io.flutter.embedded_view_preview</key>
    <string>YES</string>
    

    Sadly, this is poorly documented, but necessary for this to work on a Mac.

  5. With this plugin installed, add it to show_repo.dart with import 'package:webview_flutter/webview_flutter.dart';

  6. Now we can add to the Scaffold after the appTitle the following to create the web view:

      body: Container(
          width: double.infinity,
          // the most important part of this example
          child: WebView(
            initialUrl: htmlURL,
            // Enable Javascript on WebView
            javascriptMode: JavascriptMode.unrestricted,
          )),
    

    Test it out and see that it is working. Qapla'