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.
Related Posts
To Do Rest API Using Asp.net Core (Part 1)
To Do Rest API Using Asp.net Core – Adding Swagger (Part 2)
To Do Rest API Using Asp.net Core – Blazor Client (Part 3)
To Do Rest API Using Asp.net Core – Angular Client (Part 4) To Do Rest API Using Asp.net Core – Angular Client (Part 4)
To Do Rest API Using Asp.net Core – Flutter Client (Part 5) (This One)
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.
i love this very good post