Build a kundli screen in Flutter

You will build a screen that takes a birth date, time and place, and shows the lagna, the planets and a chart image.

A secret key inside an app can be extracted in minutes, so the app talks to a small backend of yours and the backend holds the key.

How far this page is tested: our test suite runs the backend below against the API's request chain, and checks that each field the Dart code reads is in the backend's answer. We compiled the Dart code as printed with Flutter 3.47.9, with no analyzer issues, but we have not yet run it on a phone. If you hit a problem, tell us at developers@astrologics.in.

You need Node.js 18 or later, Flutter, and a test key. Test keys run the real engine and are never charged.

1. Create the backend

mkdir kundli-backend
cd kundli-backend
npm init -y
npm install https://developers.astrologics.in/downloads/sdks/astrologics-typescript-0.1.0.tgz

Save this as server.mjs. Birth details travel in the POST body, never in a URL.

import { createServer } from "node:http";
import { AstroLogics, ApiError } from "astrologics";

const client = new AstroLogics({ apiKey: process.env.ASTROLOGICS_API_KEY });

function reply(res, status, body) {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}

createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/kundli") return reply(res, 404, { error: "Not found" });
  let raw = "";
  for await (const chunk of req) raw += chunk;
  try {
    const { date, time, place, placeId } = JSON.parse(raw);
    const birth = { date, time, place: placeId ? { place_id: placeId } : { query: place } };
    const [kundli, png] = await Promise.all([
      client.getKundli({ birth }),
      client.getChartPng({ varga: "D1", birth }),
    ]);
    reply(res, 200, {
      lagna: kundli.data.lagna,
      planets: kundli.data.planets,
      chart_png: Buffer.from(png).toString("base64"),
    });
  } catch (e) {
    if (e instanceof ApiError && (e.status === 400 || e.status === 422)) {
      return reply(res, 422, { error: e.message, candidates: e.candidates });
    }
    console.error(e.code ?? e.name);
    reply(res, 502, { error: "The chart could not be calculated. Try again." });
  }
}).listen(process.env.PORT || 3000);

The backend makes two calls: POST /v1/kundli (2 credits) and POST /v1/charts/{varga}.png (3 credits). Start it with your key:

ASTROLOGICS_API_KEY=al_test_YOUR_KEY node server.mjs

2. Create the app

In another terminal:

flutter create kundli_app
cd kundli_app
flutter pub add http

3. Call the backend

Create lib/kundli_api.dart.

import 'dart:convert';
import 'dart:typed_data';

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

class Planet {
  Planet(this.label, this.sign, this.house, this.retrograde);

  factory Planet.fromJson(Map<String, dynamic> j) =>
      Planet(j['label'] as String, j['sign'] as String, j['house'] as int?, j['retrograde'] == true);

  final String label;
  final String sign;
  final int? house;
  final bool retrograde;
}

class Kundli {
  Kundli(this.lagna, this.planets, this.chartPng);

  final String lagna;
  final List<Planet> planets;
  final Uint8List chartPng;
}

class Candidate {
  Candidate(this.placeId, this.label);

  factory Candidate.fromJson(Map<String, dynamic> j) => Candidate(
        j['place_id'] as String,
        [j['name'], j['state'], j['country']].whereType<String>().join(', '),
      );

  final String placeId;
  final String label;
}

class KundliException implements Exception {
  KundliException(this.message, this.candidates);

  final String message;
  final List<Candidate> candidates;
}

class KundliApi {
  KundliApi(this.baseUrl, {http.Client? client}) : _client = client ?? http.Client();

  final String baseUrl;
  final http.Client _client;

  Future<Kundli> fetch({required String date, required String time, required String place, String? placeId}) async {
    final res = await _client.post(
      Uri.parse('$baseUrl/kundli'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'date': date, 'time': time, 'place': place, 'placeId': placeId}),
    );
    final j = jsonDecode(res.body) as Map<String, dynamic>;
    if (res.statusCode != 200) {
      final found = (j['candidates'] as List<dynamic>?) ?? const [];
      throw KundliException(
        j['error'] as String,
        found.map((c) => Candidate.fromJson(c as Map<String, dynamic>)).toList(),
      );
    }
    return Kundli(
      (j['lagna'] as Map<String, dynamic>)['sign_label'] as String,
      (j['planets'] as List<dynamic>).map((p) => Planet.fromJson(p as Map<String, dynamic>)).toList(),
      base64Decode(j['chart_png'] as String),
    );
  }
}

4. Add the screen

Replace lib/main.dart. The backend's address comes from the command line, so it never gets typed into the code.

import 'package:flutter/material.dart';

import 'kundli_api.dart';

const backendUrl = String.fromEnvironment('BACKEND_URL', defaultValue: 'http://localhost:3000');

void main() => runApp(const MaterialApp(title: 'Kundli', home: KundliScreen()));

class KundliScreen extends StatefulWidget {
  const KundliScreen({super.key});

  @override
  State<KundliScreen> createState() => _KundliScreenState();
}

class _KundliScreenState extends State<KundliScreen> {
  final _api = KundliApi(backendUrl);
  final _date = TextEditingController(text: '1990-08-13');
  final _time = TextEditingController(text: '06:30');
  final _place = TextEditingController(text: 'New Delhi');
  Kundli? _kundli;
  String? _error;
  List<Candidate> _candidates = const [];
  bool _busy = false;

  @override
  void dispose() {
    _date.dispose();
    _time.dispose();
    _place.dispose();
    super.dispose();
  }

  Future<void> _load([String? placeId]) async {
    setState(() {
      _busy = true;
      _error = null;
      _candidates = const [];
      _kundli = null;
    });
    try {
      final kundli = await _api.fetch(date: _date.text, time: _time.text, place: _place.text, placeId: placeId);
      if (mounted) setState(() => _kundli = kundli);
    } on KundliException catch (e) {
      if (mounted) {
        setState(() {
          _error = e.message;
          _candidates = e.candidates;
        });
      }
    } catch (_) {
      if (mounted) setState(() => _error = 'Could not reach the server. Try again.');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final kundli = _kundli;
    return Scaffold(
      appBar: AppBar(title: const Text('Kundli')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextField(controller: _date, decoration: const InputDecoration(labelText: 'Date (YYYY-MM-DD)')),
          TextField(controller: _time, decoration: const InputDecoration(labelText: 'Time (HH:MM)')),
          TextField(controller: _place, decoration: const InputDecoration(labelText: 'Place')),
          const SizedBox(height: 16),
          FilledButton(
            onPressed: _busy ? null : () => _load(),
            child: Text(_busy ? 'Calculating…' : 'Show chart'),
          ),
          if (_error != null) Padding(padding: const EdgeInsets.only(top: 16), child: Text(_error!)),
          for (final c in _candidates) TextButton(onPressed: () => _load(c.placeId), child: Text(c.label)),
          if (kundli != null) ...[
            const SizedBox(height: 16),
            Text('Lagna: ${kundli.lagna}', style: Theme.of(context).textTheme.titleLarge),
            Image.memory(kundli.chartPng, semanticLabel: 'Birth chart'),
            for (final p in kundli.planets)
              ListTile(
                title: Text(p.retrograde ? '${p.label} (R)' : p.label),
                subtitle: Text(p.sign),
                trailing: Text(p.house == null ? '–' : 'House ${p.house}'),
              ),
          ],
        ],
      ),
    );
  }
}

5. Run it

On an iOS simulator, the default address reaches your computer:

flutter run

On an Android emulator, your computer is at 10.0.2.2:

flutter run --dart-define=BACKEND_URL=http://10.0.2.2:3000

Before you go live

  • Host the backend over HTTPS and pass its address as BACKEND_URL in your release build.
  • Protect the backend. Anyone who finds its address can spend your credits. Require sign-in or add a rate limit of your own first.
  • Use a live key. Set ASTROLOGICS_API_KEY to your al_live_ key on the backend. It never goes in the app.
  • Android release builds need <uses-permission android:name="android.permission.INTERNET"/> in android/app/src/main/AndroidManifest.xml.
  • Take care with birth details. They are personal data. This backend stores nothing and does not log them. We never write them to our database or logs.
  • Unknown birth times. Add a way to send time_accuracy, rather than filling in a default time. See the birth object.

Next: mobile apps · errors · Next.js tutorial