Loading...
Loading...
Learn how to structure a Flutter project to reuse models and business logic across iOS, Android, Web, desktop platforms, and a REST API deployable to Google Cloud Run, enabling a single codebase for both client and server.
npx skill4agent add rodydavis/skills host-your-flutter-project-as-a-rest-apiOne Codebase for Client and Sever.
import 'package:flutter/material.dart';
import 'plugins/desktop/desktop.dart';
import 'ui/home/screen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: HomeScreen(),
);
}
}import 'package:flutter/material.dart';
import '../counter/screen.dart';
import '../todo/screen.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: <Widget>[
CounterScreen(),
TodosScreen(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (val) {
if (mounted)
setState(() {
_currentIndex = val;
});
},
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.add),
title: Text('Counter'),
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
title: Text('Todos'),
),
],
),
);
}
}import 'package:flutter/material.dart';
import 'package:shared_dart/src/models/counter.dart';
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
CounterModel _counterModel = CounterModel();
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counterModel.add();
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyCounterPage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text('Counter Screen'),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${_counterModel.count}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}CounterModelclass CounterModel {
CounterModel();
int _count = 0;
int get count => _count;
void add() => _count++;
void subtract() => _count--;
void set(int val) => _count = val;
}import 'package:flutter/material.dart';
import '../../src/classes/todo.dart';
import '../../src/models/todos.dart';
class TodosScreen extends StatefulWidget {
@override
_TodosScreenState createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
final _model = TodosModel();
List<ToDo> _todos;
@override
void initState() {
_model.getList().then((val) {
if (mounted)
setState(() {
_todos = val;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todos Screen'),
),
body: Builder(
builder: (_) {
if (_todos != null) {
return ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, index) {
final _item = _todos[index];
return ListTile(
title: Text(_item.title),
subtitle: Text(_item.completed ? 'Completed' : 'Pending'),
);
},
);
}
return Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}// To parse this JSON data, do
//
// final toDo = toDoFromJson(jsonString);
import 'dart:convert';
List<ToDo> toDoFromJson(String str) => List<ToDo>.from(json.decode(str).map((x) => ToDo.fromJson(x)));
String toDoToJson(List<ToDo> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class ToDo {
int userId;
int id;
String title;
bool completed;
ToDo({
this.userId,
this.id,
this.title,
this.completed,
});
factory ToDo.fromJson(Map<String, dynamic> json) => ToDo(
userId: json["userId"],
id: json["id"],
title: json["title"],
completed: json["completed"],
);
Map<String, dynamic> toJson() => {
"userId": userId,
"id": id,
"title": title,
"completed": completed,
};
}import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_dart/src/classes/todo.dart' as t;
class TodosModel {
final kTodosUrl = '[https://jsonplaceholder.typicode.com/todos'](https://jsonplaceholder.typicode.com/todos');
Future<List<t.ToDo>> getList() async {
final _response = await http.get(kTodosUrl);
if (_response != null) {
final _todos = t.toDoFromJson(_response.body);
if (_todos != null) {
return _todos;
}
}
return [];
}
Future<t.ToDo> getItem(int id) async {
final _response = await http.get('$kTodosUrl/$id');
if (_response != null) {
final _todo = t.ToDo.fromJson(json.decode(_response.body));
if (_todo != null) {
return _todo;
}
}
return null;
}
}Now time for the magic..
# Use Google's official Dart image.
# [https://hub.docker.com/r/google/dart-runtime/](https://hub.docker.com/r/google/dart-runtime/)
FROM google/dart-runtimeapiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: PROJECT_NAME
namespace: default
spec:
template:
spec:
containers:
- image: docker.io/YOUR_DOCKER_NAME/PROJECT_NAME
env:
- name: TARGET
value: "PROJECT_NAME v1"name: shared_dart
description: A new Flutter project.
publish_to: none
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
shelf: ^0.7.3
cupertino_icons: ^0.1.2
http: ^0.12.0+2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: trueimport 'dart:io';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'src/routing.dart';
void main() {
final handler = const shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addHandler(RouteUtils.handler);
final port = int.tryParse(Platform.environment['PORT'] ?? '8080');
final address = InternetAddress.anyIPv4;
io.serve(handler, address, port).then((server) {
server.autoCompress = true;
print('Serving at [http://${server.address.host}:${server.port}'](http://${server.address.host}:${server.port}'));
});
}import 'dart:async';
import 'package:shelf/shelf.dart' as shelf;
import 'controllers/index.dart';
import 'result.dart';
class RouteUtils {
static FutureOr<shelf.Response> handler(shelf.Request request) {
var component = request.url.pathSegments.first;
var handler = _handlers(request)[component];
if (handler == null) return shelf.Response.notFound(null);
return handler;
}
static Map<String, FutureOr<shelf.Response>> _handlers(
shelf.Request request) {
return {
'info': ServerResponse('Info', body: {
"version": 'v1.0.0',
"status": "ok",
}).ok(),
'counter': CounterController().result(request),
'todos': TodoController().result(request),
};
}
}'counter': CounterController().result(request),
'todos': TodoController().result(request),This is also the first time I found out about FutureOr. It allows you to return a sync or async function.
import 'dart:convert';
import 'package:shelf/shelf.dart' as shelf;
class ServerResponse {
final String message;
final dynamic body;
final StatusType type;
ServerResponse(
this.message, {
this.type = StatusType.success,
this.body,
});
Map<String, dynamic> toJson() {
return {
"status": type.toString().replaceAll('StatusType.', ''),
"message": message,
"body": body ?? '',
};
}
String toJsonString() {
return json.encode(toJson());
}
shelf.Response ok() {
return shelf.Response.ok(
toJsonString(),
headers: {
'Content-Type': 'application/json',
},
);
}
}
enum StatusType { success, error }
abstract class ResponseImpl {
Future<shelf.Response> result(shelf.Request request);
}import 'package:shared_dart/src/models/counter.dart';
import 'package:shelf/shelf.dart' as shelf;
import '../result.dart';
class CounterController implements ResponseImpl {
const CounterController();
@override
Future<shelf.Response> result(shelf.Request request) async {
final _model = CounterModel();
final _params = request.url.queryParameters;
if (_params != null) {
final _val = int.tryParse(_params['count'] ?? '0');
_model.set(_val);
} else {
_model.add();
}
return ServerResponse('Info', body: {
"counter": _model.count,
}).ok();
}
}import 'package:shared_dart/src/models/todos.dart';
import 'package:shelf/src/request.dart';
import 'package:shelf/src/response.dart';
import '../result.dart';
class TodoController implements ResponseImpl {
@override
Future<Response> result(Request request) async {
final _model = TodosModel();
if (request.url.pathSegments.length > 1) {
final _id = int.tryParse(request.url.pathSegments[1] ?? '1');
final _todo = await _model.getItem(_id);
return ServerResponse('Todo Item', body: _todo).ok();
}
final _todos = await _model.getList();
return ServerResponse(
'List Todos',
body: _todos.map((t) => t.toJson()).toList(),
).ok();
}
}export 'counter.dart';
export 'todo.dart';{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: [https://go.microsoft.com/fwlink/?linkid=830387](https://go.microsoft.com/fwlink/?linkid=830387)
"version": "0.2.0",
"configurations": [
{
"name": "Client",
"request": "launch",
"type": "dart",
"program": "lib/main.dart"
},
{
"name": "Server",
"request": "launch",
"type": "dart",
"program": "bin/server.dart"
}
]
}Keep in mind this is running your Dart code from you lib folder in your Flutter project without needing the Flutter widgets!