1

I am new to Flutter and we are adopting it in our company for a new app. I am making a configuration form for it. We have a lot of large models defined with OpenAPI so manually writing models is something we'd rather avoid. So I found this OpenAPI generator. My colleague found this package for generating GetX project.

So I have a project generated with the get_cli (actually there's a bug so I had to generate the project with flutter cli and then call get init). I also have the generated OpenAPI client in the project root with this command: java -jar C:\Users\venca\Documents\Gatema\openapi-generator-cli-6.0.0.jar generate -i .\openapi.yaml -g dart-dio -o ./api_generated. The generator is using built_value and I'm trying to make it work with GetX features like observables in the configuration form so I don't have to deal with setState and calling .rebuild() on the models.

I created a new project for this question and a fake specification but the concept is the same. I used this mock server for this example project with these data:

{
  "document": {
    "basePath": "/home/user",
    "configFile": "file.json"
  }
}

The spec is this:

openapi: 3.0.3
info:
  title: foo
  version: 0.0.0
servers:
  - url: http://localhost:3000

paths:
  /document:
    get:
      summary: Get file
      operationId: getDocument
      responses:
        '200':
          description: Content of file
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/document'
    put:
      summary: Replace content
      operationId: replaceDocument
      requestBody:
        description: New content
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/document'
      responses:
        '205':
          description: File updated
        '400':
          description: Bad Request

components:
  schemas:
    document:
      description: Model of config file
      type: object
      properties:
        basePath:
          type: string
          minLength: 1
        configFile:
          type: string
          minLength: 1

home_view.dart looks like this - I kept comments with the .rebuild():

import 'package:flutter/material.dart';

import 'package:get/get.dart';
import 'package:openapi/openapi.dart';

import '../controllers/home_controller.dart';

class HomeView extends GetView<HomeController> {
  const HomeView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('HomeView'),
        centerTitle: true,
      ),
      body: Center(
        child: TestForm(),
      ),
    );
  }
}

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

  @override
  State<TestForm> createState() => _TestFormState();
}

class _TestFormState extends State<TestForm> {
  final _formKey = GlobalKey<FormState>();
  final HomeController homeController = Get.find();
  late Rx<Document> config;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder(
        future: homeController.initDocument(),
        builder: (context, AsyncSnapshot snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.done:
              if (snapshot.data != null) {
                config = snapshot.data! as Rx<Document>;
              } else {
                return const Text("There was an error while fetching settings - empty response");
              }
              return Form(
                key: _formKey,
                child: Obx(
                  () => ListView(
                    padding: const EdgeInsets.all(8),
                    children: [
                      TextFormField(
                        initialValue: config.value.basePath,
                        decoration: const InputDecoration(labelText: 'Base path'),
                        validator: (value) {
                          if ((value ?? "").isEmpty) {
                            return 'Field must not be empty';
                          }
                        },
                        // onSaved: (val) => setState(() => config = config.rebuild((p0) => p0..basePath = val)),
                      ),
                      TextFormField(
                        initialValue: config.value.configFile,
                        decoration: const InputDecoration(labelText: 'Name of config file'),
                        validator: (value) {
                          if ((value ?? "").isEmpty) {
                            return 'Field must not be empty';
                          }
                        },
                        // onSaved: (val) => setState(() => config = config.rebuild((p0) => p0..configFile = val)),
                      ),
                      Container(
                        padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
                        child: ElevatedButton(
                          onPressed: () async {
                            final form = _formKey.currentState;
                            if (form!.validate()) {
                              try {
                                form.save();
                                await homeController.saveDocument(config);
                              } catch (e) {
                                print("Exception when calling saveDocument: $e\n");
                              }
                            }
                          },
                          child: const Text('Save'),
                        ),
                      ),
                    ],
                  ),
                ),
              );
            case ConnectionState.waiting:
              print("waiting....");
              return const CircularProgressIndicator();
            default:
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else {
                return Text('Result: ${snapshot.data}');
              }
          }
        },
      ),
    );
  }
}

And home_controller.dart is this:

import 'package:get/get.dart';
import 'package:openapi/openapi.dart';

class HomeController extends GetxController {
  final _api = Openapi().getDefaultApi();

  initDocument() async {
    final DocumentBuilder _documentBuilder = DocumentBuilder();
    _documentBuilder.basePath = "/usr";
    _documentBuilder.configFile = "config.json";
    var document = _documentBuilder.build();

    Rx<Document> data = document.obs;

    try {
      final response = await _api.getDocument();
      data = response.data!.obs;
      print(data.value);
      return data;
    } catch (e) {
      print("Exception when calling getDocument: $e\n");
      return Future.error(e.toString());
    }
  }

  saveDocument(newDocument) async {
    try {
      var doc = newDocument.value;
      print(doc);
      final response = await _api.replaceDocument(document: doc);
    } catch (e) {
      print("Exception when calling replaceDocument: $e\n");
      throw Exception(e);
    }
  }
}

At first I had only the setState with .rebuild() which was working. But with the real project I was quickly fed up with it when I encountered arrays and other more complicated stuff than just TextFormField. So I wanted to make it easier with observables. Then I wrote it like this but it's not working - no errors but also no updates - the doc variable stays the same as original data from server, nothing changes.

Is it even possible to combine it like this? Or am I stuck with rebuilds? Or am I just completely missing a point of something (like I said - I'm new into Flutter)? I found only one similar question but it's not about Form (How to combine the usage of GetX and build_value?) Also I'd rather avoid the equivalent of first four lines of the initDocument() method because in real project there are tens of different properties... Thank you

Liniik
  • 50
  • 1
  • 9

0 Answers0