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