Flutter

Rapidly develop native cross-platform iOS and Android Apps with Flutter's calling ServiceStack's HTTP and gRPC end-to-end typed .NET API integrations.

Getting Started

Two mix templates to help you build services with Flutter clients are the flutter and flutter-grpc mix templates.

These templates create a new Flutter application using your locally installed Flutter SDK that comes wired up to the ServiceStack project template it is mixed into.

These mix templates can be added to ServiceStack templated projects using the ServiceStack dotnet x tool, which can be installed using the command:

$ dotnet tool install --global x 

Pre-requisites

  • Flutter SDK
  • Dart SDK
  • ServiceStack dotnet X tool

New or Existing Project

The Flutter integration works with most ServiceStack templates. It does make the assumption that when working locally, the initial Flutter application will connect to the ServiceStack host via localhost:5001 or if the client is running on an Android Emulator, 10.0.0.2:5001.

For example, you could start with a new web ServiceStack project to host your web services, and add a working Flutter client using the mix template.

$ x new web MyApp

Then from your new solution directory MyApp, mix in the Flutter application using:

$ x mix flutter

Flutter Setup with gRPC

Alternatively, if you want to use gRPC for communication, you can use the grpc project template.

$ x new grpc MyApp

With the flutter-grpc mix template to get a gRPC equivalent setup just as quickly.

$ x mix flutter-grpc

Project Structure

The x mix flutter template uses your locally installed Flutter SDK to create the initial Flutter application via the flutter create command. It then overrides some source files, and adds some required Dart dependencies to facilitate the integration with your web services.

Running

To develop on your Flutter client, it is best to take advantage of the hot reload functionality of both Flutter and dotnet watch.

ServiceStack App

From your web project directory, navigate into your MyApp AppHost directory and run:

$ dotnet watch

Keep your services running, and open your myapp_flutter directory using Android Studio.

Flutter App

When building application in flutter, you can use various IDEs or editors, but in this example we will be using Android Studio. Flutter applications are written in the Dart Language, and since both are developed by Google, there are great resources for learning both the Dart Language, and the Flutter Framework, included dedicated resources for those familiar with Xamarin.Forms.

When developing on Windows, Android Studio will automatically support web and desktop targets, as well as making it easier to set up and manage Android Emulated devices.

Development

From Android Studio, you can target multiple platforms including Windows, Web and Android. If you are on Windows, targeting a Windows Desktop application can provide a rapid development cycle as Flutter Hot Reload works well and performs quickly. And since ServiceStack can generate client Data Transfer Objects or DTOs, we can have a typed end-to-end service integration when developing Flutter client applications.

Flutter main.dart

The x mix flutter command provides a customized main.dart with a built-in integration of the Hello API service of your local ServiceStack application.

This uses the servicestack Dart package which contains a supported Service Client. To support both web and non-web client targets, a ClientFactory is used with a conditional import of the platform specific service client from servicestack/web_client.dart for web, and servicestack/client.dart for all native platforms.

var baseUrl = "https://localhost:5001";
var clientOptions = ClientOptions(baseUrl: baseUrl);

client = ClientFactory.createWith(clientOptions);
runApp(const MyApp());

The use of Android Emulators for local development is catered for by checking if the application is not in release mode, is not a web platform and if it is running on the Android platform.

if (!kReleaseMode && !kIsWeb) {
  if (Platform.isAndroid) {
    clientOptions.baseUrl = "https://10.0.2.2:5001";
    clientOptions.ignoreCertificatesFor.add(clientOptions.baseUrl);
  }
}

This is done since Android Emulators have limited network access, and can only access the ServiceStack application running on the host. Android can only communicate with the host via the aliased 10.0.2.2 IP address from the running emulator.

Flutter Template Overview

Flutter applications use Widgets to build everything you see in a Flutter application.

Widgets describe what their view should look like given their current configuration and state. When a widget’s state changes, the widget rebuilds its description, which the framework diffs against the previous description in order to determine the minimal changes needed in the underlying render tree to transition from one state to the next.

The MyApp widget is the entry point of our application, and the MaterialApp to default your Flutter application to use Material Design as a style or theme.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HelloFlutter(title: 'Flutter Demo Home Page'), // Your app starts here
    );
  }
}

The HelloFlutter widget is a StatefulWidget which references the HelloFlutterState class where the UI is declared and updates are made to the Flutter UI.

The example application listens for changes from the TextField using a TextEditingController, binding a callService method which fires whenever a change is made.

class HelloFlutterState extends State<HelloFlutter> {
  //State for this widget
  String result = "";
  var myController = TextEditingController();
  ...

  @override
  void initState() {
    super.initState();
    // Listen for changes from the TextField and call our API
    myController.addListener(callService);
  }
  
  void callService() async {
    var text = myController.text.isEmpty ? "World" : myController.text;
    var response = await client.get(Hello(name: text));
    setState(() {
      result = response.result!;
    });
  }
  
@override
  Widget build(BuildContext context) {
    return Scaffold(//...
      body: Center(//..
        TextField(//..
          controller: myController,
      )

Calling the Hello API

The callService method is an async method where Dart has a familiar syntax to dotnet. The generated Request and Response DTOs located in the dtos.dart file represent the same messages on our ServiceStack server, so we also get similar syntax and types when compared to calling from a dotnet client.

  void callService() async {
    var text = myController.text.isEmpty ? "World" : myController.text;
    var response = await client.get(Hello(name: text)); // response of type `HelloResponse`.
    setState(() {
      result = response.result!;
    });
  }

Updating your client DTOs

During development of your web services, when changes are made to your Request DTO classes, your client sometimes needs to be aware of these changes. Your client DTOs in the lib/dtos.dart file can be regenerated while your ServiceStack application is running by using the Servicestack dotnet x tool:

$ x dart

If you use Android Studio, there is also a ServiceStack IDEA Plugin for Jetbrains IDEs that can make it easy to quickly update your client DTOs.

INFO

To install the Plugin goto Settings->Plugins->Search Marketplace for ServiceStack.

The dtos.dart file that comes with the Flutter mix template only contains DTOs for the default Hello service, but if other web services are running on your host application, updating your dtos.dart file using one of the above methods will then sync your client and server Request DTOs.

For example, if you use one of our Jamstack templates, there is also a Todo service.

Running x dart in project terminal will update our DTOs, and we can query for Todo items using the following Dart syntax.

Future<QueryResponse<Todo>> queryTodos() async {
    return await client.get(QueryTodos());
}

The Todo is a shared model type, so we can use them in our application passing around the same data structure.

Future<Todo> updateTodo(Todo item) async {
    return await client.put(UpdateTodo(
        id: item.id,
        isFinished: (item.isFinished ?? false) ? true : false,
        text: item.text));
}

We've made an example codebase called BookingsFlutter to show a more featured cross-platform application integrating with different types of services.

gRPC Examples

Create a new Flutter project with Android Studio:

Add protoc generated TodoWorld DTOs and gRPC GrpcServiceClient to lib/ folder:

x proto-dart https://todoworld.servicestack.net -out lib

Add required dependencies to pubspec.yaml:

dependencies:
  fixnum: ^0.10.11
  async: ^2.2.0
  protobuf: ^1.0.1
  grpc: ^2.1.3

Install dependencies by running pub get or clicking on Get Dependencies link in the IDE:

Flutter protoc gRPC insecure Example

Use protoc generated DTOs and GrpcServiceClient to call TodoWorld gRPC Service in _MyHomePageState class in bin/main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_grpc/services.pbgrpc.dart';
import 'package:grpc/grpc.dart';

//...

class _MyHomePageState extends State<MyHomePage> {
  String result = '';
  GrpcServicesClient client = GrpcServicesClient(
      ClientChannel('todoworld.servicestack.net', port:50054,
          options:ChannelOptions(credentials: ChannelCredentials.insecure())));

  void _callGrpcService() async {
    var response = await client.getHello(Hello()..name="Flutter gRPC");
    setState(() {
      result = response.result;
    });
  }

  //...
}

Capture the result gRPC API request in the result String then change the Widget build() to display that instead of _counter then update the floatingActionButton to call your _callGrpcService method instead:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'gRPC Service Example:',
            ),
            Text(
              '$result',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _callGrpcService,
        tooltip: 'gRPC Service Example',
        child: Icon(Icons.play_arrow),
      ),
    );
  }

With Flutter's live-reload capability you should be able to see your changes instantly in the Android emulator where clicking the icon should display the result of your plain-text gRPC Service Request:

Flutter protoc gRPC SSL Example

To use gRPC SSL we'll need a copy of our gRPC's Service SSL Certificate which we can make available to our Flutter App by saving it to our App's assets directory:

mkdir assets
x get https://todoworld.servicestack.net/grpc.crt -out assets

As loading assets is an asynchronous operation we'll need to preload it either by loading it in main() and passing it as an attribute down to all our components or we can load it in our State widget's initState() method:

class _MyHomePageState extends State<MyHomePage> {
  String result = '';
  GrpcServicesClient client;

  @override
  void initState() {
    super.initState();
    DefaultAssetBundle.of(context).load("assets/grpc.crt").then((bytes) => setState(() {
      client = GrpcServicesClient(
          ClientChannel('todoworld.servicestack.net', port:50051,
              options:ChannelOptions(credentials: ChannelCredentials.secure(
                  certificates: bytes.buffer.asUint8List(),
                  authority: 'todoworld.servicestack.net'
              ))));
    }));
  }

  void _callGrpcService() async {
    var response = await client.getHello(Hello()..name="gRPC SSL");
    setState(() {
      result = response.result;
    });
  }

  //...
}

You'll also need to update the port to refer to the gRPC SSL endpoint, update your Hello request so we can verify the result is from the new gRPC SSL request. Now after live-reload has completed, clicking on the icon will show the response of a gRPC SSL Request:

Refer to /src/mobile/flutter/flutter_grpc for a complete example project.

Flutter gRPC Mix Template

The Flutter integration works with ServiceStack grpc template that has been configured to support gRPC services. It also makes the assumption that when working locally, the initial Flutter application will connect to the ServiceStack gRPC services via localhost:5054 or if the client is running on an Android Emulator, 10.0.0.2:5054. 5054 is the insecure HTTP port that is used by the grpc project template, but if you want to add grpc support to your application using x mix grpc, be sure to set up the use of the same port for local development, or modify your Flutter client to match your server setup.

For example, you could start with a new grpc ServiceStack project to host your services via gRPC, and add a working Flutter client which connects via gRPC using the mix template.

$ x new grpc MyApp

Then from your new solution directory MyApp, mix in the Flutter application using:

$ x mix flutter-grpc

Project Structure

The x mix flutter-grpc template uses your locally installed Flutter SDK to create the initial Flutter application via the flutter create command. It then overrides some source files, and adds some required Dart dependencies to facilitate the integration with your gRPC services.

Running

To develop on your Flutter client, it is best to take advantage of the hot reload functionality of both Flutter and dotnet watch.

ServiceStack App

From your grpc project directory, navigate into your MyApp AppHost directory and run:

$ dotnet watch

The grpc template is pre-configured listening on 3 ports in the appsettings.json.

  • https://*:5001 - Http1 - Normal Web Service Access
  • https://*:5051 - Http2 - Grpc Secure
  • http://*:5054 - Http2 - Grpc Insecure

When developing locally, the Flutter client application accesses gRPC services using the GrpcInsecure endpoints using port 5054.

Keep your services running, and open your myapp_flutter directory using Android Studio.

ServiceStack generates this .proto file used by the client and then provides tooling through the dotnet x tool to generate gRPC client code in Dart, or multiple other languages.

The Flutter client application itself uses Dart libraries like protobuf from Google, the grpc from the Dart team and leverages the optimized servicestack package to improve the development experience.

Since your ServiceStack application host generates the .proto file, you can use any other standard gRPC tooling with this file if you prefer.

var host = "localhost";
var channel = ClientChannel(host, port:5054,
  options:const ChannelOptions(credentials: ChannelCredentials.insecure(
)));
var client = GrpcServicesClient(channel);

runApp(const MyApp());

Calling the Hello API

The callService method is an async method where Dart has a familiar syntax to dotnet. The generated gRPC client in the 4 services.*.dart files is baked into the fluter-grpc mix template and contain the initially generated gRPC client. Calling gRPC services using generated client is similar to using other ServiceStack service clients, but since it is gRPC, we have dedicated methods on our client for our services. Eg, getHello rather than just the verb get.

void callService() async {
  var text = myController.text.isEmpty ? "World" : myController.text;
  var response = await client.getHello(Hello(name: text));
  setState(() {
    result = response.result;
  });
}

Updating your client DTOs

During development of your web services, when changes are made to your Request DTO classes, your client sometimes needs to be aware of these changes. We can update both the services.proto file and generated Dart client using the ServiceStack dotnet x tool and the command:

$ x proto-dart https://localhost:5001

This command updates the services.proto file first, and then uses our hosted gRPC client generator to generate a working gRPC client in the language of your choice.

The initial services.*.dart files contain a basic gRPC client for working with the default Hello service, but if other web services are running on your host application, using x proto-dart will then sync your client and generated client as you make changes.

For example, if you use one of our Jamstack templates, and configure gRPC using x mix grpc, there is also a Todo service.

Make your service support gRPC

For a ServiceStack service to support gRPC, it needs to use the [DataContract] and [DataMember(Order = x)] attributes on the types exposed by your generated clients. Once these attributes are added

[Tag("todos")]
[Route("/todos", "GET")]
[DataContract]
public class QueryTodos : QueryData<Todo>
{
    [DataMember(Order = 1)]
    public int? Id { get; set; }
    [DataMember(Order = 2)]
    public List<long>? Ids { get; set; }
    [DataMember(Order = 3)]
    public string? TextContains { get; set; }
}

[DataContract]
public class Todo : IHasId<long>
{
    [DataMember(Order = 1)]
    public long Id { get; set; }
    [DataMember(Order = 2)]
    public string Text { get; set; }
    [DataMember(Order = 3)]
    public bool IsFinished { get; set; }
}

Once updated, re-running your ServiceStack application, we can update our generated gRPC client using x proto-dart from your project directory while your ServiceStack host is running locally.

This will enable you to integrate with the Todo service using the updated client using the following syntax.

Future<QueryResponse_Todo> queryTodos() async {
  return await client.getQueryTodos(QueryTodos());
}

gRPC's use of Protocol Buffers does have a number of restrictions in the Types in supports that are worth keeping in mind.

Questions?

Head over to our Customer Forums or GitHub Discussions if you are having issues or have any questions!