Home  /  Blog  /  Prayer Times App Flutter

Build a Prayer Times App with Flutter

Flutter9 min readSeptember 2026

This post shows you how to build a Flutter app that fetches and displays today's prayer times using the user's real GPS location. You will also add a method picker so users can choose their preferred calculation method.

What You Will Build

By the end of this post, you will have a working Flutter app that does the following:

The full code is split into small pieces below so you can follow along step by step.

Project Setup

Create a new Flutter project and open your pubspec.yaml. You need two packages: geolocator for GPS access and http for API calls.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  geolocator: ^12.0.0
  http: ^1.2.2

Run flutter pub get to install them. Both packages are on pub.dev. No other packages are needed for the core app.

Next, add location permissions. Open android/app/src/main/AndroidManifest.xml and add these two lines inside the <manifest> tag:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

For iOS, open ios/Runner/Info.plist and add a NSLocationWhenInUseUsageDescription key with a short description string. The geolocator README has the exact snippet if you need it.

Fetching Prayer Times from UmmahAPI

The endpoint is simple. You pass a latitude and longitude and get back today's prayer times. Here is a plain Dart function that does exactly that. Put this in a file called lib/api_service.dart.

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

const _base = 'https://ummahapi.com/api';

// Fetch today's prayer times for a given location
Future<Map<String, dynamic>> fetchPrayerTimes(
  double lat,
  double lng, {
  int? method,
}) async {
  final params = {
    'lat': lat.toStringAsFixed(6),
    'lng': lng.toStringAsFixed(6),
    if (method != null) 'method': method.toString(),
  };
  final uri = Uri.parse('$_base/prayer-times').replace(queryParameters: params);
  final res = await http.get(uri);

  if (res.statusCode != 200) {
    throw Exception('Failed to load prayer times: ${res.statusCode}');
  }
  return jsonDecode(res.body) as Map<String, dynamic>;
}

// Fetch all available calculation methods
Future<List<dynamic>> fetchMethods() async {
  final uri = Uri.parse('$_base/prayer-times/methods');
  final res = await http.get(uri);

  if (res.statusCode != 200) {
    throw Exception('Failed to load methods');
  }
  return jsonDecode(res.body) as List<dynamic>;
}

The method parameter is optional. When you leave it out, the API uses a default method. Later you will pass a chosen method ID from the dropdown.

Tip. You can test these endpoints directly in your browser or with curl before writing Flutter code. Try https://ummahapi.com/api/prayer-times?lat=21.42&lng=39.82 to see the response shape. Check the full reference on the Prayer Times API page.

Getting the User's Location

Create a file called lib/location_service.dart. This handles permission requests and returns the current position.

import 'package:geolocator/geolocator.dart';

Future<Position> getCurrentLocation() async {
  // Check if location services are enabled at all
  final serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    throw Exception('Location services are disabled.');
  }

  var permission = await Geolocator.checkPermission();

  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      throw Exception('Location permission denied.');
    }
  }

  if (permission == LocationPermission.deniedForever) {
    throw Exception('Location permission permanently denied.');
  }

  return Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.high,
  );
}

This function throws an Exception for every failure case, so you can show the right error message in the UI depending on what went wrong.

Building the Main Screen

Now put everything together in lib/main.dart. The screen uses a StatefulWidget to hold the prayer times data and the selected method. On load, it gets the location and calls the API.

import 'package:flutter/material.dart';
import 'api_service.dart';
import 'location_service.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Prayer Times',
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      home: const PrayerTimesScreen(),
    );
  }
}

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

  @override
  State<PrayerTimesScreen> createState() => _PrayerTimesScreenState();
}

class _PrayerTimesScreenState extends State<PrayerTimesScreen> {
  Map<String, dynamic>? _times;
  List<dynamic> _methods = [];
  int? _selectedMethod;
  double? _lat, _lng;
  bool _loading = true;
  String? _error;

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

  Future<void> _init() async {
    try {
      final methods = await fetchMethods();
      final pos = await getCurrentLocation();
      setState(() {
        _methods = methods;
        _lat = pos.latitude;
        _lng = pos.longitude;
      });
      await _loadTimes();
    } catch (e) {
      setState(() {
        _error = e.toString();
        _loading = false;
      });
    }
  }

  Future<void> _loadTimes() async {
    if (_lat == null || _lng == null) return;
    setState(() { _loading = true; _error = null; });
    try {
      final data = await fetchPrayerTimes(
        _lat!,
        _lng!,
        method: _selectedMethod,
      );
      setState(() {
        _times = data;
        _loading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Prayer Times'),
        centerTitle: true,
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator())
          : _error != null
              ? Center(child: Text(_error!))
              : _buildContent(),
    );
  }

  Widget _buildContent() {
    final prayers = [
      'Fajr', 'Sunrise', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'
    ];
    return Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildMethodDropdown(),
          const SizedBox(height: 24),
          ...prayers.map((p) => _PrayerRow(
            name: p,
            time: _times?[p]?.toString() ?? '--:--',
          )),
        ],
      ),
    );
  }

  Widget _buildMethodDropdown() {
    return DropdownButtonFormField<int>(
      value: _selectedMethod,
      decoration: const InputDecoration(
        labelText: 'Calculation Method',
        border: OutlineInputBorder(),
      ),
      hint: const Text('Default'),
      items: _methods.map((m) {
        return DropdownMenuItem<int>(
          value: m['id'] as int,
          child: Text(m['name']?.toString() ?? ''),
        );
      }).toList(),
      onChanged: (val) {
        setState(() { _selectedMethod = val; });
        _loadTimes();
      },
    );
  }
}

class _PrayerRow extends StatelessWidget {
  const _PrayerRow({required this.name, required this.time});

  final String name;
  final String time;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 10),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
          Text(time, style: TextStyle(fontSize: 16, color: Theme.of(context).colorScheme.primary)),
        ],
      ),
    );
  }
}

When the user picks a different method from the dropdown, _loadTimes() runs again with the new method ID. The loading state shows a spinner so the user knows something is happening.

Understanding the API Response

Here is what the response from /api/prayer-times looks like. Knowing the shape helps you avoid typos when reading fields.

// GET https://ummahapi.com/api/prayer-times?lat=51.51&lng=-0.12
{
  "Fajr":    "04:12",
  "Sunrise": "05:58",
  "Dhuhr":   "13:01",
  "Asr":     "16:52",
  "Maghrib": "20:01",
  "Isha":    "21:41"
}

All six fields are strings in HH:MM format. In the Flutter code above, _times?[p]?.toString() reads them directly. If you want to highlight the next upcoming prayer, you can parse these strings with TimeOfDay and compare them to TimeOfDay.now().

Showing a Full Month of Times

If you want to show a monthly view, you can fetch all times for a given month. Just add month and year query parameters to the same endpoint. Here is a quick example function for that:

Future<List<dynamic>> fetchMonthlyTimes(
  double lat,
  double lng,
  int month,
  int year,
) async {
  final uri = Uri.parse('https://ummahapi.com/api/prayer-times').replace(
    queryParameters: {
      'lat': lat.toStringAsFixed(6),
      'lng': lng.toStringAsFixed(6),
      'month': month.toString(),
      'year': year.toString(),
    },
  );
  final res = await http.get(uri);
  if (res.statusCode != 200) {
    throw Exception('Monthly fetch failed: ${res.statusCode}');
  }
  return jsonDecode(res.body) as List<dynamic>;
}

The response is a list of daily objects. You can drop that into a ListView.builder and show each day as a row. This is useful if you want to build a full calendar screen inside the same app.

Tip. Cache the monthly response in your app's state or in shared preferences so you do not make a new network request every time the user navigates back to the calendar view. One fetch per month is enough.

Handling Errors and Edge Cases

A few things can go wrong, and it is worth handling them cleanly.

What to Build Next

The app works as a starting point. Here are a few natural next steps:

All of these features use endpoints from the same UmmahAPI, so you only need one base URL across your whole app.

FAQ

How do I get a user's location in Flutter for prayer times?

Use the geolocator package from pub.dev. Call Geolocator.getCurrentPosition() after requesting permission. This returns a Position object with latitude and longitude properties that you pass directly to the UmmahAPI endpoint.

Which UmmahAPI endpoint returns today's prayer times?

Send a GET request to https://ummahapi.com/api/prayer-times?lat=YOUR_LAT&lng=YOUR_LNG. The response includes Fajr, Sunrise, Dhuhr, Asr, Maghrib, and Isha as time strings. See the full parameter list on the Prayer Times API page.

Can I choose the calculation method for prayer times in the API?

Yes. Fetch the full list of 23 methods from /api/prayer-times/methods. Each method has an id and a name. Pass the id as a method query parameter in your prayer times request and the API will use that method for the calculation.

Do I need an API key to use UmmahAPI in my Flutter app?

No key is needed to start building. The free tier supports up to 5,000 requests per 15 minutes. When you are ready to ship a production app, register for a free key at ummahapi.com/register to get unlimited access.

Start Building for Free

UmmahAPI is free to use and covers prayer times, Quran, Hadith, Qibla, Hijri calendar, and more.

Read the docs