Scharf Beobachtet

Tech Know How

Go Rest Service und Flutter App im Zusammenspiel

2021-11-28 17:26:52
RGB Control App
RGB Control App

In diesem Artikel möchte ich einmal zeigen, wie einfach man einen Restful Service selber erstellen und dann mit einer kleinen App steuern kann.

Zunächst der Code für den Restful Service, mit Go und Go-Gin erstellt, 48 Zeilen lang.

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/gin-gonic/gin"
)

type rgb struct {
	ID      string `json:"id"`
	Current string `json:"current"`
	R       string `json:"r"`
	G       string `json:"g"`
	B       string `json:"b"`
}

var rgbModel = rgb{ID: "1", Current: "color", R: "255", G: "0", B: "0"}

func main() {
	router := gin.Default()
	router.GET("/rgb", getRgb)
	router.POST("/rgb", postRgb)
	router.Run()
}

func getRgb(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, rgbModel)
}

func postRgb(c *gin.Context) {
	var newRgb rgb

	body, _ := ioutil.ReadAll(c.Request.Body)
	println(string(body))
	println("end string body")

	err := json.Unmarshal(body, &newRgb)
	if err != nil {

		fmt.Println(err)
	}

	rgbModel = newRgb
	c.IndentedJSON(http.StatusCreated, newRgb)
}

Der Service stellt auf Port 8080 einen Restful Service zur Verfügung, mit dem ein RGB Objekt geladen oder überschrieben werden kann.

D.h. es können Id, Bezeichnung und RGB Werte mittels POST Request geändert werden. Per GET wird dieses dann im JSON Format zurückgegeben.

Ausgabe des GET Requests aus dem Rest Service
Ausgabe des GET Requests aus dem Rest Service

Das ganze einmal für die Kommandozeile / Terminal als GET, um den Stand zu holen, der zweite Befehl ändert das JSON Objekt dann.

curl http://192.168.178.62:8080/rgb

curl http://192.168.178.62:8080/rgb \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": 1,"current": "anim_theater","r": 255,"g": 0, "b": 0}'

Sendet man ein falsch strukturiertes Objekt, erhält man einen Response Code 400. Durch das Objekt-Mapping in Go haben wir hier für den Aufbau schon mal ein wenig Absicherung.

Nun zum Flutter / Dart Part – eine App, laufend im selben Netzwerk. Diese soll mittels einfacher Buttons und eines kleinen Konfigurations-Formulars die RGB Werte ändern können.

So soll das ganze aussehen:

Screenshot aus der Test Flutter App

Und dazu der Code. Aufgeteilt in Main, Form und eine extra Klasse für HTTP Requests.

main.dart

import 'package:flutter/material.dart';
import 'package:ledrgb_controls_app/form_screen.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RGB Form',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: FormScreen()
    );
  }
}

FormScreen Class

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

class FormScreen extends StatefulWidget {
  const FormScreen({Key? key}) : super(key: key);

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

class _FormScreenState extends State<FormScreen> {
  final double _maxRGBValue = 255.0;
  double _sliderValueRed = 90.0;
  double _sliderValueGreen = 90.0;
  double _sliderValueBlue = 90.0;

  HttpService httpS = HttpService();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("RGB Form"),
      ),
      body: Center(
        child: Container(
          margin: EdgeInsets.all(10),
          child: Column(
            children: [
              Row(
                children: [
                  Text("Rot"),
                  Slider(
                    activeColor: Colors.red,
                    value: _sliderValueRed,
                    min: 0,
                    max: _maxRGBValue,
                    divisions: _maxRGBValue.toInt(),
                    label: _sliderValueRed.round().toString(),
                    onChanged: (double value) {
                      setState(() {
                        _sliderValueRed = value;
                      });
                    },
                  ),
                ],
              ),
              Row(
                children: [
                  Text("Grün"),
                  Slider(
                    activeColor: Colors.green,
                    value: _sliderValueGreen,
                    min: 0,
                    max: _maxRGBValue,
                    divisions: _maxRGBValue.toInt(),
                    label: _sliderValueGreen.round().toString(),
                    onChanged: (double value) {
                      setState(() {
                        _sliderValueGreen = value;
                      });
                    },
                  ),
                ],
              ),
              Row(
                children: [
                  Text("Blau"),
                  Slider(
                    activeColor: Colors.blue,
                    value: _sliderValueBlue,
                    min: 0,
                    max: _maxRGBValue,
                    divisions: _maxRGBValue.toInt(),
                    label: _sliderValueBlue.round().toString(),
                    onChanged: (double value) {
                      setState(() {
                        _sliderValueBlue = value;
                      });
                    },
                  ),
                ],
              ),
              TextButton(
                onPressed: () {
                  // httpS.makeGetRequest();
                  httpS.makeRGBPostRequest(
                      _sliderValueRed, _sliderValueGreen, _sliderValueBlue);
                },
                child: Text("Senden"),
              ),
              Divider(
                height: 10,
              ),
              TextButton(
                onPressed: () {
                  httpS.makeAnimPostRequest("christmas_tree");
                },
                child: Text("Weihnachtsbaum"),
              ),
              TextButton(
                onPressed: () {
                  httpS.makeAnimPostRequest("christmas_candles");
                },
                child: Text("Weihnachtskerzen"),
              ),
              TextButton(
                onPressed: () {
                  httpS.makeAnimPostRequest("rainbow");
                },
                child: Text("Regenbogen"),
              ),
              TextButton(
                onPressed: () {
                  httpS.makeAnimPostRequest("stars");
                },
                child: Text("Sterne"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Und hier noch der Service für die HTTP Requests (Im Kommentar steht noch der Medium Artikel, auf dem die Request Methoden basieren):

import 'package:fluttertoast/fluttertoast.dart';
import 'package:http/http.dart';

// https://suragch.medium.com/how-to-make-http-requests-in-flutter-d12e98ee1cef

class HttpService {
  static const urlPrefix = 'https://jsonplaceholder.typicode.com';
  static const urlPrefixRGB = 'http://192.168.178.62:8080/rgb';

  Future<void> makeGetRequest() async {
    final url = Uri.parse('$urlPrefix/posts');
    Response response = await get(url);
  }

  Future<void> makeGetRequest2() async {
    final url = Uri.parse('$urlPrefixRGB');
    Response response = await get(url);
  }

  Future<void> makeRGBPostRequest(double r, double g, double b) async {
    final url = Uri.parse('$urlPrefixRGB');
    final headers = {"Content-Type": "application/json"};
    final json = '{"id": "1", "current": "color", "r": "${r.toInt()}", "g": "${g.toInt()}", "b": "${b.toInt()}"} ';
    final response = await post(url, headers: headers, body: json);
    Fluttertoast.showToast(msg: "Response? " +response.statusCode.toString());
  }

  Future<void> makeAnimPostRequest(String anim) async {
    final url = Uri.parse('$urlPrefixRGB');
    final headers = {"Content-Type": "application/json"};
    final json = '{"id": "1", "current": "$anim", "r": "255", "g": "255", "b": "255"} ';
    final response = await post(url, headers: headers, body: json);
    Fluttertoast.showToast(msg: "Response? " +response.statusCode.toString());
  }

  Future<void> makePostRequest() async {
    final url = Uri.parse('$urlPrefix/posts');
    final headers = {"Content-type": "application/json"};
    final json = '{"title": "Hello", "body": "body text", "userId": 1}';
    final response = await post(url, headers: headers, body: json);
    print('_jv_ Status code: ${response.statusCode}');
    print('_jv_ Body: ${response.body}');
  }

  Future<void> makePutRequest() async {
    final url = Uri.parse('$urlPrefix/posts/1');
    final headers = {"Content-type": "application/json"};
    final json = '{"title": "Hello", "body": "body text", "userId": 1}';
    final response = await put(url, headers: headers, body: json);
    print('_jv_ Status code: ${response.statusCode}');
    print('_jv_ Body: ${response.body}');
  }

  Future<void> makePatchRequest() async {
    final url = Uri.parse('$urlPrefix/posts/1');
    final headers = {"Content-type": "application/json"};
    final json = '{"title": "Hello"}';
    final response = await patch(url, headers: headers, body: json);
    print('_jv_ Status code: ${response.statusCode}');
    print('_jv_ Body: ${response.body}');
  }

  Future<void> makeDeleteRequest() async {
    final url = Uri.parse('$urlPrefix/posts/1');
    final response = await delete(url);
    print('_jv_ Status code: ${response.statusCode}');
    print('_jv_ Body: ${response.body}');
  }
}

Ein paar Hinweise zur Flutter App:

Bilder für App Icon oder sowas kann man super von
https://pixabay.com/de/ holen. Die meisten Bilder sind dort für völlig freie Verwendung nutzbar.

Zudem aus der pubspec.yml

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  fluttertoast: ^8.0.8
  http: ^0.13.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_launcher_icons: ^0.9.0


#flutter pub run flutter_launcher_icons:main
flutter_icons:
  android: true
  ios: true
  image_path: "assets/launcher/intersection-g24ee61f8d_1280.png"
  adaptive_icon_background: "#000000"
  adaptive_icon_foreground: "assets/launcher/intersection-g24ee61f8d_1280.png"      

Den Befehl zur Icon Erstellung habe ich da als Kommentar mit reingegeben.

Wenn man, so wie ich in diesem Beispiel, alles nur über http – ohne SSL – macht – muss man Flutter für den Debug Modus eben dieses noch erlauben. Dazu einmal die debug/manifest.xml im Android Verzeichnis entsprechend anpassen:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.ledrgb_controls_app">
    <!-- Flutter needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <application android:usesCleartextTraffic="true" />
</manifest>

So. Nun haben wir alles zusammen und können mit der App den Service steuern.

Die App dann einmal in Bewegung:

Zurück