Shalitha Suranga Programmer | Author of Neutralino.js | Technical Writer

Why you should use a Go backend in Flutter

15 min read 4372

Why you should use a Go backend in Flutter

Flutter is a popular open source framework for creating cross-platform applications for release on Android, iOS, Linux, macOS, Windows, Fuchsia, web, and others. Flutter is growing more popular every day because of its performance and inbuilt platform-independent UI widgets, and friendly development environment.

Go is a compiled, statically typed, high-performance language with a simple syntax.

Flutter and Go became popular in the developer community after Google initiated both open source projects. Nowadays, many developers choose to use Go and Flutter together. Using a Go backend for a Flutter frontend specifically brings many advantages compared to other popular backend languages.

In this article, we’ll discuss these advantages and verify them practically by building a full-stack application.

Advantages of using a Go backend for Flutter apps

Application frontends usually communicate with the server-side modules with various network communication concepts, such as REST, WebSocket, GraphQL, SOAP, and gRPC.

The above communication concepts are tech stack-agnostic, so the backend technology doesn’t affect the frontend and vice versa. However, Go-based backends bring numerous nontechnical and hidden benefits for Flutter frontends. Further, you can avoid time-consuming business logic rewrites by directly using Go modules in the Flutter app.

Both Go and Flutter are Google open source projects. The Google open source community backs both projects by offering free community-based developer support, contributing code, and creating resources. You can discuss your Go development problems in the official Go mailing thread and Flutter-related problems in the official Flutter mailing list.

Google released Go v1 in 2012 and introduced Flutter v1 in 2018, but both technologies experienced rapid growth for cloud-based business applications in late 2019. Both projects are now growing in popularity every day and come with an excellent reputation, community support, and up-to-date, Google-engineered technology.

Go and Flutter are performance-first technologies

Nowadays, some developers tend to overlook application performance due to powerful computer hardware components. For example, many individuals use powerful computers and mobile devices, so hybrid apps don’t show performance issues despite those apps generally performing slower than native apps. Additionally, many web developers rarely need to optimize web backend performance due to strong cloud computing infrastructure. Go-based backends work well on low-end server computers. However, Flutter works well on low-end mobile devices.

Both Go and Flutter projects strive to solve the primary technical problem by carefully considering performance factors.



Flutter offers near-native performance with a rendering canvas powered by Skia and the native platform channels concept.

The Go compiler produces fast and optimized native binaries and makes Go quick and agile, similar to other modern, popular, enterprise-level programming languages like C#, Java, and JavaScript (Node.js).

A Go backend offers fast and efficient native server-side services for Flutter applications to achieve better native performance.

Similarities in development environment

Flutter uses Dart as the cross-platform application development language. Dart and Go offer features to solve different technical problems. However, Go/Dart syntax, developer tools, and third-party libraries have considerable similarities. Therefore, the same full-stack development team can work on both backend and frontend projects without any productivity issues. Flutter developers can also get started with Go backend development comfortably thanks to Go’s minimal syntax.

Moreover, Go development tools work perfectly on all Flutter development tools’ operating systems. As a result, you can configure a productive Go development environment on your Flutter development computer.

Reusing Go backend logic in Flutter

Sometimes we have to reuse backend code directly in a frontend application. If you use Node.js for the backend and React Native for the frontend, you can easily share common business logic by creating a JavaScript package.

With shared Dart packages, we can easily reuse frontend and backend code if we use Dart to implement backend web services. Conduit, Shelf, and Angel help developers build RESTful APIs with Dart, but Dart’s server-side support is still growing and is not yet comparable with the Go ecosystem. So, you should think twice before using Dart for writing the backend.

However, Dart’s server-side support is still growing and is not comparable with the Go ecosystem yet, so you need to think twice before using Dart for writing the backend.

If you use C#, Java, or Node.js for developing your backend, you may have to rewrite the same existing business logic in Dart on the Flutter frontend. The Go mobile project offers a way to call Go code from the platform-specific mobile development environments, i.e., Java and Android.

Therefore, we can connect Go mobile with Flutter and build an efficient way to reuse Go-based business logic.

Using Go for serving the Flutter web app

As you may already know, users can access the Flutter app from a web browser with Flutter web support. But how can you serve your Flutter web app from your cloud environment? You either need to use a prebuilt static server or write one with a preferred backend language.

It’s possible to write a fast and complete static file server in Go with a few lines of code. You can even serve your Flutter web app from the Go RESTful backend without creating a separate Go web server instance.

Tutorial: Building a Flutter app with a Go backend

Now that we know the benefits of using a Go backend for a Flutter application, let’s develop a Go RESTful web service and a Flutter frontend to verify the above advantages. We are going to build a full-stack product list application with a Go REST API and Flutter frontend. The Go-based RESTful web service will return a product list in JSON format and the Flutter app will display a product list by calling the web service.

We will also convert the Flutter app into a web app and serve it using the same RESTful web service. Finally, I will demonstrate how to share Go code with the Flutter app by using the Go mobile project.

Developing a Go RESTful backend

We are going to create a RESTful backend to produce a JSON-formatted product list. First, install the latest Go development tools from the official Go Downloads page or with a package manager tool (e.g., Snap) if your computer doesn’t have the Go compiler. Next, create a new Go module with the following commands to get started:

mkdir go_backend
cd go_backend
go mod init go_backend

We need an HTTP routing library for developing RESTful web services. The Gin web framework offers almost all HTTP-based backend development features, such as routing, JSON binding, and validation. Add the Gin framework package to the current project with the following command:

go get -u github.com/gin-gonic/gin

We also need to enable CORS since we are going to use Flutter web in this tutorial. Download the Gin CORS middleware package into your project with the following command:


More great articles from LogRocket:


go get github.com/gin-contrib/cors

Now, create a file named

main.go

and add the following code:

package main
import (
    "github.com/gin-gonic/gin"
    "github.com/gin-contrib/cors"
)
type Product struct {
    Id int `json:"id"`
    Name string `json:"name"`
    Price int `json:"price"`
    Description string `json:"description"`
}
func productsHandler(c *gin.Context) {
    products := []Product {
        Product {100, "BassTune Headset 2.0", 200, "A headphone with a inbuilt high-quality microphone"},
        Product {101, "Fastlane Toy Car", 100, "A toy car that comes with a free HD camera"},
        Product {101, "ATV Gear Mouse", 75, "A high-quality mouse for office work and gaming"},
    }
    c.JSON(200, gin.H{
        "products": products,
    })
}
func main() {
    r := gin.Default()
    r.Use(cors.Default())
    r.GET("/products", productsHandler)
    r.Run(":5000")
}

The above code implements the

GET /products

endpoint for returning a JSON-formatted product list. Here we construct a static product list by creating a

Product

struct slice with the

[] Product

syntax. We added some JSON struct tags in the Product struct with the

json:

notation for converting exported title case struct fields to lowercase JSON fields. We use a hardcoded product list for demonstration purposes, but you can use any preferred database connection to fetch stored product details.

Testing the Go backend

Let’s test the above Go backend. First, start the web service with the following command:

go run main.go

The above command starts the Gin RESTful server for accepting HTTP requests from the port

5000

. You can test the product list endpoint with the well-known Postman tool, as shown below.

Testing the Go RESTful backend with the postman tool

Creating the application frontend with Flutter

Let’s create a product list with Flutter and display data from the above Go backend. If you haven’t already installed Flutter, you can easily do so from the official Flutter binary releases page.

First, create a new Flutter application with the following command:

flutter create flutter_frontend

Once the project is created, run it with the

flutter run

command and test it on Chrome or your mobile device to verify that everything works as expected. We need to create Dart classes for each primary JSON object to make the codebase maintainable and readable. Add the following code to the

lib/product_model.dart

file to define the product model:

class Product {
  final int id;
  final String name;
  final String description;
  final int price;

  const Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
  });


  factory Product.fromJson(Map json) {
    return Product(
      id: json['id'],
      name: json['name'],
      description: json['description'],
      price: json['price']
    );
  }
}

Next, we can create a Dart service to communicate with the Go backend. We will use the Dio HTTP client library, so add it to your Flutter project with the following command:

flutter pub add dio

Now, create the product service implementation in the

lib/product_service.dart

file with the following Dart source:

import 'package:dio/dio.dart';
import 'package:flutter_frontend/product_model.dart';

class ProductService {
  final String productsURL = 'http://localhost:5000/products';
  final Dio dio = Dio();

  ProductService();

  Future<List<Product>> getProducts() async {
    late List<Product> products;
    try {
      final res = await dio.get(productsURL);

      products = res.data['products']
        .map<Product>(
          (item) => Product.fromJson(item),
        )
        .toList();
    }
    on DioError catch(e) {
      products = [];
    }

    return products;
  }
}

Here, we created the

getProducts

asynchronous function to get products as instances of the

Product

model by calling the Go backend via the Dio client. The above source code offers business data via a Dart service, but if you work with many RESTful endpoints you can use the repository pattern to organize the code better.

The above product service uses

localhost

in the URL, since I run the app on Chrome (Flutter web mode) for demonstration. If you need to test the app on a mobile device, use your computer’s local network IP address instead of

localhost

and use the same WiFi network for both PC and mobile.

Finally, we can create the product list application frontend by importing the above product service. Replace the existing code in the

lib/main.dart

file with the following code:

import 'package:flutter/material.dart';
import 'package:flutter_frontend/product_service.dart';
import 'package:flutter_frontend/product_model.dart';

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

class MyApp extends StatelessWidget {
  final _productService = ProductService();

  @override
  Widget build(BuildContext context) {
    const title = 'Product List';

    return MaterialApp(
      title: title,
      theme: new ThemeData(scaffoldBackgroundColor: const Color(0xffdddddd)),
      home: Scaffold(
        appBar: AppBar(
          title: const Text(title),
        ),
        body: FutureBuilder<List<Product>>(
          future: _productService.getProducts(),
          builder: (context, snapshot) {
            var products = snapshot.data ?? [];

            if(!snapshot.hasData) {
              return const Center(child: CircularProgressIndicator());
            }

            return ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                var product = products[index];
                return ListTile(
                  title: Text(products[index].name),
                  subtitle: Text('#${product.id} ${product.description}'),
                  trailing: Text('\$${product.price}')
                );
              },
            );
          },
        ),
      ),
    );
  }
}

In the above code snippet, we used the

FutureBuilder

class to display a loading animation until the frontend fetches all products from the backend. Run the application on your mobile device or Chrome by entering the

flutter run

command.

You will see the working product list application interface, as shown below.

Our product list application is running in Chrome

Serving the Flutter web app with Go

Now we are going to expose our Flutter application as a web app via the Go backend. Then, we can access the app with any modern web browser. We can easily add static file serving support to the existing web service via the Gin static middleware. Install the static middleware package from the project directory.

go get github.com/gin-contrib/static

Next, add the following package import to the main Go source file.

"github.com/gin-contrib/static"

Finally, ask the Gin framework to serve static web content with the following code line:

r.Use(static.Serve("/", static.LocalFile("./static", false)))

Make sure that the final web service source looks like this:

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-contrib/cors"
    "github.com/gin-contrib/static"
)

type Product struct {
    Id int `json:"id"`
    Name string `json:"name"`
    Price int `json:"price"`
    Description string `json:"description"`
}

func productsHandler(c *gin.Context) {
    products := []Product {
        Product {100, "BassTune Headset 2.0", 200, "A headphone with a inbuilt high-quality microphone"},
        Product {101, "Fastlane Toy Car", 100, "A toy car that comes with a free HD camera"},
        Product {101, "ATV Gear Mouse", 75, "A high-quality mouse for office work and gaming"},
    }
    c.JSON(200, gin.H{
        "products": products,
    })
}

func main() {
    r := gin.Default()
    r.Use(cors.Default())
    r.Use(static.Serve("/", static.LocalFile("./static", false)))

    r.GET("/products", productsHandler)

    r.Run(":5000")
}

We can now build the Flutter web app to get static web resources. Enter the following command and generate Flutter web app resources:

flutter build web

Create a new directory named

static

inside the Golang project and copy all generated Flutter web app resources from the

./build/web

directory to the

static

directory.

Start the Go backend server and go to the

http://localhost:5000

URL from your web browser. You will see the working Flutter web application, as shown below.

Our product list in Flutter

You can deploy the Go project into your cloud environment with Flutter web resources by using a container system like Docker. Then everyone can access your full-stack Flutter web app from web browsers.

Reusing Go code in the Flutter app

The Go mobile project offers tools to generate native Android and iOS libraries from Go source files. The Flutter project uses platform-specific host applications, aka embedders, to initialize the Flutter engine on each platform. Therefore, we can use Go modules in Flutter with the Go mobile project and Flutter platform channel APIs. For example, on the Android platform, we can call the Java code via the Flutter platform channel APIs from Dart, then we can call the Go mobile-generated library functions via Java Go mobile bindings. This approach is helpful for developers who want to reuse Go backend code within the Flutter app without rewriting Go modules in Dart.

Now, we are going to modify the well-known Flutter demo application by displaying a random number instead of incrementing the existing number with each tap on the floating action button. We will generate this random number via a Go module. In the following example, I will explain how to embed a Go module in an Android app. You can use a similar approach to embed Go modules in iOS apps, too.

Before you continue with the tutorial, make sure that your computer has the following components, which can be easily installed via Android Studio:

  • Android SDK
  • Android NDK
  • Clang compiler and Make

First, we need to install the Go mobile CLI with the following command:

go install golang.org/x/mobile/cmd/[email protected]
gomobile init

If the

gomobile

command doesn’t work after the installation process, you can solve the issue by adding the Go mobile binary to the

PATH

environment variable, as shown below.

export PATH=$PATH:~/go/bin

Let’s create a new Go module to generate a random number. First, create a new Go project in your working directory.

mkdir gomobilelib
cd gomobilelib
go mod init gomobilelib

Next, create a new file named

gomobilelib.go

and add the following source code.

package gomobilelib

import "math/rand"

type (
    GoMobileLib struct {}
)

func (p *GoMobileLib) RandomNumber() int {
    return rand.Intn(100);
}

We can generate native platform-specific libraries for each mobile operating system with Go mobile’s binding feature. Before using it, we need to install the Go mobile bind package.

go get golang.org/x/mobile/bind

Now we can generate an Android library file using the following command:

gomobile bind --target android

After running the above command, you can see the

gomobilelib.aar

file in the Go module directory. The .aar file contains the Go runtime libraries and the above

gomobilelib

module in platform-specific binary format for each mobile CPU architecture.

Let’s use the Android library by creating a new Flutter project. Create a new Flutter project with a Java-based host app.

flutter create gomobilefrontend -a java

Copy the

gomobilelib.aar

file to the

./gomobilefrontend/android/app/src/main/libs

directory. Link the newly created library with the Android host app by adding the following configuration to the

./gomobilefrontend/android/app/build.gradle

file.

repositories {
    flatDir {
         dirs 'src/main/libs'
    }
}
dependencies {
    api(name:'gomobilelib', ext:'aar')
}

Next, replace the existing code in the

MainActivity.java

file with the following code:

package com.example.gomobilefrontend;

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import gomobilelib.GoMobileLib;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "example.com/gomobilelib";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {

    GoMobileLib goMobileLib = new GoMobileLib();

    super.configureFlutterEngine(flutterEngine);
      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            if(call.method.equals("getRandomNumber")) {
              result.success(goMobileLib.randomNumber());
            }
            else {
              result.notImplemented();
            }
          }
        );
  }
}

The above code exposes the Android library’s

randomNumber

function as

getRandomNumber

to the Flutter app via the platform channels API. Now we can invoke

getRandomNumber

from the Flutter app to receive a new random number.

You can now create an asynchronous Dart function to call the exported Android library function. For example, the following Dart function updates the

_counter

variable with the random number generated by the Go module:

static const platform = MethodChannel('example.com/gomobilelib');
int _counter = 0;

Future<void> _getRandomNumber() async {
  int randomNumber;
  try {
    randomNumber = await platform.invokeMethod('getRandomNumber');
  } on PlatformException catch (e) {
    randomNumber = 0;
  }

  setState(() {
    _counter = randomNumber;
  });
}

Note that we need to use the same platform channel identifier in the Flutter app and Android host app to get everything working properly. Look at the following complete source code of the modified demo application that displays random numbers:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GoMobileFlutter',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter with Go Mobile'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('example.com/gomobilelib');
  int _counter = 0;

  Future<void> _getRandomNumber() async {
    int randomNumber;
    try {
      randomNumber = await platform.invokeMethod('getRandomNumber');
    } on PlatformException catch (e) {
      randomNumber = 0;
    }

    setState(() {
      _counter = randomNumber;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'The following number was generated by Go:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _getRandomNumber,
        tooltip: 'Get a random number',
        child: const Icon(Icons.add),
      ),
    );
  }
}

If you run the Android application with the above source code using the

flutter run

command, you can generate a new random number by clicking on the floating action button, as shown in the following preview.

Our Go-powered Flutter app is now running in Android

Similar to the above example application, you can reuse your Go modules in your Flutter applications without rewriting them in Dart. Therefore, if you select Go for writing backend web services, you can productively use the core business logic modules directly with the Flutter frontend. Learn more about platforms channels from the official Flutter documentation.

This project’s source code is available at my GitHub repository.

Go backend vs. other Flutter backend options

A particular fullstack application’s backend and frontend are two different and separate modules. An application’s frontend typically uses a technology stack-agnostic protocol over the network to communicate with the backend. Therefore, the choice of backend language doesn’t affect the frontend implementation directly.

But, as we discussed earlier, using Go as the backend language brings many non-technical and hidden technical benefits like code reusability. What if we use a backend technology other than Go for the Flutter app?

Let’s compare Go-powered backends with other popular Flutter backend options:

Comparison factor Go Node.js (JavaScript) .NET Core (C#) Dart Java
Popular RESTful frameworks/libraries Gin, Revel, Martini, Echo, Goji, and Gorilla Express, Fastify, Nest.js, Koa, and Connect Inbuilt .NET Core web API Conduit, Shelf, Angel, Jaguar, and Aqueduct Spring, Spark, Vert.x, and Jersey
Non-RESTful communication strategies (i.e., WebSocket, GraphQL) Has libraries for implementing WebSockets, GraphQL, and gRPC backends; offers an official module for WebSockets Has libraries for implementing WebSockets, GraphQL, and gRPC backends Has libraries for implementing GraphQL and gRPC backends; offers inbuilt support for WebSockets Has libraries for implementing WebSockets, GraphQL, and gRPC backends; offers inbuilt support for WebSockets too Has libraries for implementing GraphQL and gRPC backends; Java EE and community projects offer the WebSockets support
Performance Good multi-threaded performance; Go code gets compiled into optimized platform-specific Assembly with a minimal runtime Suitable for real-time less CPU-extensive tasks due to the single-threaded nature; optimizes code execution with V8’s JIT Good multi-threaded performance; ource code gets translated to optimized CIL (Common Intermediate Language) for the VM Suitable for real-time less CPU-extensive tasks due to single-threaded nature; possible to generate native binaries for better performance Good multi-threaded performance; source code gets translated to optimized bytecode for the VM
Third-party open source packages availability Good; every community package tends to follow Go’s minimal design concepts Good; able to find many packages for literally anything, so it’s somewhat hard to find the overall best package for a specific need without a detailed comparison Good, but most high-quality libraries come with proprietary licensing models Growing developer community — Dart’s server-side ecosystem has not yet matured Good; every community package tends to follow Java’s design principles and offer production-ready solutions
Developer support and popularity Rapid popularity growth since the first release in 2012; good developer support from maintainers and the community Popular mostly because of its ease of development; good developer support from maintainers and the community Popular mostly because of the usage in enterprise application development; comes with commercial components, but the community-based developer support is good Growing developer support and popularity; some packages like Aqueduct have been discontinued by maintainers Popular mostly because of the usage in enterprise application development; comes with commercial components, but the community-based developer support is good
Similarities with Flutter Similarities in the CLI, development workflows, language syntax, and internal design principles (i.e., the performance-first design); backed by the same organization: Google Similarities in the language syntax and runtime Similarities in the language syntax and design principles (solid OOP) The same programming language is used, so code reusability is undboutedly great; backed by the same organization: Google Similarities in the language syntax and design principles (solid OOP)
Reusability of the backend code in Flutter Able to auto-generate platform-specific language bindings with native libraries via Go mobile Requires a rewrite Requires a rewrite Able to share the code directly with a Dart package Able to share the code to Android with a Java package; requires a rewrite for iOS

Many React developers typically like to build their backend services with Node.js, since they can work with JavaScript to develop the entire fullstack app. Similarly, it’s possible to develop a backend with Dart too. Unfortunately, Dart’s server-side ecosystem has still not matured, so you may face issues with community-based packages and developer support if you choose Dart for your app’s backend.

Node.js is also a popular backend choice, but many experienced developers don’t recommend Node.js for CPU-intensive operations. Node.js with TypeScript or Deno has some similarities with the Dart language syntax, but Node.js development workflows and experience have many differences (i.e., compilation, CLI features, etc.) with Flutter.

Go has many similarities with Dart, and Go’s development tools have similarities with Flutter development tools. Flutter and Go strive to achieve better performance and are backed by the same organization. Also, reusing your backend’s Go code in the Flutter app is possible. Therefore, using a Go-powered backend for your Flutter app is a good decision.

But, this doesn’t mean that other backend options will negatively affect your Flutter app. The choice of backend technology doesn’t impact the frontend. But, using Go in the backend for a Flutter app brings impressive advantages over other backend options.

Using a Backend-as-a-Service (BaaS) solution like Firebase or Supabase is also a popular modern backend option for Flutter apps. However, with this option, you still may have to create cloud functions with a preferred backend language to run custom code for performing actions that don’t belong to your frontend code.

Conclusion

In this article, we discussed the advantages of using Go for Flutter applications with a sample RESTful web service. We also compared Go with other popular backend options. Many developers use the RESTful web service pattern for modern cloud-oriented applications, but the RESTful approach doesn’t solve every technical requirement. For bidirectional asynchronous communications, for example, you may have to use the WebSockets protocol; for lightweight and high-speed streaming calls, you can consider using the gRPC protocol.

The Go package ecosystem provides packages for both WebSockets and gRPC protocols, so you can use Go to implement backends for your Flutter frontends with any preferred communication protocol.

As we discussed, Go backends bring many benefits for Flutter apps. There are many similarities in Go and Flutter development environments, and both technologies have the same popularity level and reputation, developer support, and growing package ecosystem, and the performance-first design principle.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Shalitha Suranga Programmer | Author of Neutralino.js | Technical Writer

Leave a Reply