Mozart Al Khateeb

Full Stack

Mobile

.Net Developer

ERP Developer

Mozart Al Khateeb

Full Stack

Mobile

.Net Developer

ERP Developer

Blog Post

To Do Rest API Using Asp.net Core – Flutter Client (Part 5)

To Do Rest API Using Asp.net Core – Flutter Client (Part 5)

This post is a continuation for previous posts, to follow along you have to at least complete part 1 and part 2 where we built a restful web service using Asp.net Core or you can grab the source code from GitHub.

Flutter is a mobile app framework developed by Google allowing you to create fast and good looking applications.

Having worked with other cross platform and hybrid app frameworks such as Ionic and Xamarin I found flutter to be faster.

Flutter uses dart which is an Object Oriented Programming language similar to C#, Java and other languages out there but of course it has some dart specific language syntax.

In flutter everything is a widget and the flutter SDK contains tons of widgets that we can use in addition, flutter has platform specific widgets (Cupertino for IOS and Material for Android). The way flutter is built allows developers to modify every aspect of the UI that includes creating custom widgets if you wanted to.

To start using Flutter there are a couple of steps you need to complete, and since the Flutter team has a well documented guide I won’t be writing this in here and you know docs can change, so please go to the flutter website complete the steps before you continue.

Getting Started

Creating your Flutter App

Open you command prompt or terminal, navigate to the folder where you want to place your project then type the following and hit enter:

flutter create --org com.yourdomain fluttertodo

now you can run the newly created project navigate by executing the following commands:

cd fluttertodo
flutter run

If everything is setup correctly an emulator will launch showing the default flutter app.

Adding Dependencies

Since we are gonna retrieve data from a rest API we have to add the HTTP dependency to the pubspec.yaml file, to do so you have to open the project in an IDE such as VS Code or Android Studio then go to pubspec.yaml search for cupertino_icons and add this line below it:

http: ^0.12.0+2

Please note that .yaml files are indentation sensitive so dependencies should be aligned or you will get an error.

Since Flutter is all widgets, it’s really hard to post little chunks of code but i’ll try to clarify thing as much as possible.

Todo model

Inside your lib folder, create a new folder called models and inside it create the file todo.dart. Add the following code to the newly created file and make sure to read the code comments to understand what is going on.

import 'dart:convert';
// A flutter class that resemble our rest API's request response data
class ToDo {
  final int id;
  String description;
  String status;

// Flutter way of creating a constructor
  ToDo({this.id = 0, this.description = '', this.status = 'Pending'});

// factory for mapping JSON to current instance of the Todo class
  factory ToDo.fromJson(Map<String, dynamic> json) {
    return ToDo(
      id: json['id'],
      description: json['description'],
      status: json['status'],
    );
  }

// Instance method for converting a todo item to a map
  Map<String, dynamic> toMap() {
    return {"id": id, "description": description, "status": status};
  }

}

//  A helper method that converts a json array into List<ToDo>
List<ToDo> fromJson(String jsonData) {

  // Decode json to extract a map
  final data = json.decode(jsonData);

  // Map each todo JSON to a Todo object and return the result as a List<ToDo>
  return List<ToDo>.from(data.map((item) => ToDo.fromJson(item)));
}

// A helper method to convert the todo object to JSON String
String toJson(ToDo data) {
  // First we convert the object to a map
  final jsonData = data.toMap();

  // Then we encode the map as a JSON string
  return json.encode(jsonData);
}
API Service

The API service is the class which will take care of all network requests so we do not mix it up with UI code. For that create a new folder called services inside the lib folder and create the file api_service.dart inside it. Then paste the following code and as mentioned before read the inline comments.

import 'dart:convert';

// Import the client from the Http Packages
import 'package:http/http.dart' show Client;

//Import the Todo Model
import 'package:todo_flutter/models/todo.dart';

class ApiService {
  // Replace this with your computer's IP Address
  final String baseUrl = "http://192.168.0.105:8091/api";
  Client client = Client();

// Get All Todos
  Future<List<ToDo>> getToDos() async {
    final response = await client.get("$baseUrl/ToDos");
    if (response.statusCode == 200) {
      return fromJson(response.body);
    } else {
      return null;
    }
  }

// Update an existing Todo
  Future<bool> updateToDo(ToDo data) async {
    final response = await client.put(
      "$baseUrl/ToDos/${data.id}",
      headers: {"content-type": "application/json"},
      body: toJson(data),
    );
    if (response.statusCode == 204) {
      return true;
    } else {
      return false;
    }
  }

// Create a new Todo
  Future<bool> addToDo(ToDo data) async {
    final response = await client.post(
      "$baseUrl/ToDos",
      headers: {"content-type": "application/json"},
      body: toJson(data),
    );
    if (response.statusCode == 201) {
      return true;
    } else {
      return false;
    }
  }

// Delete a Todo
  Future<bool> deleteTodo(int todoId) async {
    final response = await client.delete(
      "$baseUrl/ToDos/$todoId",
    );
    if (response.statusCode == 204) {
      return true;
    } else {
      return false;
    }
  }

// Get list of all Todo Statuses
  Future<List<String>> getStatuses() async {
    final response = await client.get("$baseUrl/Config");
    if (response.statusCode == 200) {
      var data = (jsonDecode(response.body) as List<dynamic>).cast<String>();
      return data;
    } else {
      return null;
    }
  }
}

Now the we have our service and model classes we can focus on designing the look of our app.

GUI

main.dart

The main.dart file is the entry point of the application, here you can setup the default look and feel of the app by using themes for example. The MaterialApp widget is top most widget in the widget tree. So delete all existing code and paste the following code instead.

import 'package:flutter/material.dart';

// Import Our home screen
import './screens/home_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter ToDo',
      home: HomeScreen(), // The Home screen widget as app home page
    );
  }
}
home_screen.dart

Inside the lib folder create a new folder called screens and add home_screen.dart inside of it then paste the following code there.

import 'package:flutter/material.dart';
import '../widgets/manage_todo_widget.dart';
import '../models/todo.dart';
import '../services/api_service.dart';

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  ApiService _apiService;

  @override
  void initState() {
    super.initState();
    _apiService = ApiService();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Flutter ToDo"),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () => _openManageTodoSheet(null, context),
          ),
        ],
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: FloatingActionButton(
        child: Icon(
          Icons.add,
        ),
        onPressed: () => _openManageTodoSheet(null, context),
      ),
      body: SafeArea(
        child: FutureBuilder(
          future: _apiService.getToDos(), // Get todos which returns a future
          builder: (BuildContext context, AsyncSnapshot<List<ToDo>> snapshot) {
            if (snapshot.hasError) {
              return Center(
                child: Text(
                    "Something wrong with message: ${snapshot.error.toString()}"),
              );
            } else if (snapshot.connectionState == ConnectionState.done) { // Called when the future is resolved (i.e: when the result is returned from the server)
              List<ToDo> todos = snapshot.data;
              return _buildListView(todos);
            } else {
              return Center(
                child: CircularProgressIndicator(), // Loading with the request is being processed 
              );
            }
          },
        ),
      ),
    );
  }

// Build todos list 
  Widget _buildListView(List<ToDo> toDos) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
      child: ListView.builder(
        itemCount: toDos.length,
        itemBuilder: (context, index) {
          ToDo toDo = toDos[index];
          return Padding(
            padding: const EdgeInsets.only(top: 8.0),
            child: Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      toDo.description,
                      style: Theme.of(context).textTheme.title,
                    ),
                    Text(toDo.status),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: <Widget>[
                        FlatButton(
                          onPressed: () {
                            _apiService.deleteTodo(toDo.id).then((_) {
                              setState(() {
                                // Here we call set state in order to rebuild the widget and get todos
                              });
                            });
                          },
                          child: Icon(
                            Icons.delete,
                            color: Colors.red,
                          ),
                        ),
                        FlatButton(
                          onPressed: () {
                            _openManageTodoSheet(toDo, context);
                          },
                          child: Icon(
                            Icons.edit,
                            color: Colors.blue,
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }

// This method opens the modal bottom sheet which hosts the ManageTodoWidget which is responsible for editing or adding new Todos
  void _openManageTodoSheet(ToDo toDo, BuildContext context) {
    toDo = toDo ?? new ToDo();
    showModalBottomSheet(
      context: context,
      builder: (BuildContext context) {
        return GestureDetector(
          onTap: () {},
          child: ManageTodoWidget(
            todo: toDo,
            saveChanges: _saveChanges, // We pass a reference tho the _saveChanges so we can call it from the child widget
          ),
          behavior: HitTestBehavior.opaque,
        );
      },
    );
  }

  void _saveChanges(ToDo todo) {
    if (todo.id == 0) { // New Todo with id zero
      _apiService.addToDo(todo).then((_) {
        Navigator.of(context).pop(); // Close Modal Bottom sheet
        setState(() {}); // Calling set state to rebuild the UI and get fresh todo list
      });
    } else {
      _apiService.updateToDo(todo).then((_) {
        Navigator.of(context).pop(); // Close Modal Bottom sheet
        setState(() {}); // Calling set state to rebuild the UI and get fresh todo list
      });
    }
  }
}

The code above will render the home screen which contain a list with all Todos and logic for deleting Todos in addition to the delete and add logic that is invoked from the modal bottom sheet.

manage_todo_widget.dart

Create a new folder inside the lib folder call it widgets then add the manage_todo_widget.dart inside it and paste the following code.

import 'package:flutter/material.dart';

import '../services/api_service.dart';
import '../models/todo.dart';

class ManageTodoWidget extends StatefulWidget {
  final ToDo todo; // a new or existing todo 
  final Function saveChanges; // Function passed by the parent widget to save changes

  const ManageTodoWidget({Key key, this.todo, this.saveChanges})
      : super(key: key);

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

class _ManageTodoWidgetState extends State<ManageTodoWidget> {
  ApiService _apiService;
  Future<List<String>> _statuses;

// Define the form key that preserve the state of the form
  final _form = GlobalKey<FormState>();

  @override
  void initState() {
    super.initState();
    _apiService = ApiService();
    _statuses = _apiService.getStatuses();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        child: Form(
          key: _form,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: <Widget>[
              TextFormField(
                initialValue: widget.todo.description,
                onSaved: (value) {
                  widget.todo.description = value; // on saved we persist the form state
                },
              ),
              FutureBuilder(
                future: _statuses, // Future like the on inside the home screen
                builder: (_, AsyncSnapshot<List<String>> snapshot) {
                  if (snapshot.hasError) {
                    return Center(
                      child: Text(
                          "Something wrong with message: ${snapshot.error.toString()}"),
                    );
                  } else if (snapshot.connectionState == ConnectionState.done) {
                    var statuses = snapshot.data;
                    return DropdownButtonFormField( // Building a drop down list with all statuses
                      hint: Text('Select Todo'),
                      value: widget.todo.status,
                      onChanged: (status) {
                        setState(() {
                          widget.todo.status = status;
                        });
                      },
                      items: statuses.map(
                        (status) {
                          return DropdownMenuItem(
                            child: Text(status),
                            value: status,
                          );
                        },
                      ).toList(),
                      onSaved: (value) => widget.todo.status = value, // on saved we persist the form state
                    );
                  } else {
                    return Center(
                      child: CircularProgressIndicator(),
                    );
                  }
                },
              ),
              FlatButton(
                child: Text(
                  'Save Changes',
                  style: TextStyle(color: Colors.blue),
                ),
                onPressed: () {
                  _form.currentState.save(); // we call the save method in order to invoke the onsaved method on form fields
                  this.widget.saveChanges(widget.todo); // call the save changes method that was passed by the parent widget
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

The code above defines the look of the modal bottom sheet which is its child widget.

Well this was a simple Todo App, I did not walk you through Installing android studio or creating a virtual machine to run the app, but as specified at the beginning of this post you can find there information on the flutter website.

Source Code

Taggs:
1 Comment
  • Edmond Gatley 8:00 am December 8, 2020 Reply

    i love this very good post

Write a comment