Home  /  Blog  /  Display Quran Verses in Flutter

Display Quran Verses in a Flutter App

Flutter9 min readSeptember 2026

This post walks you through building a Quran Flutter app that fetches Arabic verses, English translations, and reciter audio from UmmahAPI. You will handle HTTP requests, render right-to-left Arabic text, and wire up audio playback, all with real working code.

What You Will Build

By the end you will have a Flutter screen that lets a user pick a surah, see its ayahs with Arabic text and an English translation side by side, and tap a play button to hear a reciter. The code is split into small pieces so you can drop each part into an existing project if you want.

Here is what you need before starting:

Add Dependencies

Open pubspec.yaml and add these three packages. The version numbers shown were current at the time of writing, so check pub.dev for newer releases.

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  just_audio: ^0.9.39
  google_fonts: ^6.2.1

Then run flutter pub get in your terminal.

Fetch a Surah from UmmahAPI

The endpoint /api/quran/surah/:number returns all ayahs for a surah. You can attach query parameters to choose a script style and one or more translations. The example below fetches Surah Al-Fatiha (surah 1) with the Uthmani script and an English translation.

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

const String apiKey = 'YOUR_API_KEY';
const String baseUrl = 'https://ummahapi.com';

Future<Map<String, dynamic>> fetchSurah(int surahNumber) async {
  final uri = Uri.parse(
    '$baseUrl/api/quran/surah/$surahNumber'
    '?script=uthmani&translations=en&reciter=alafasy',
  );

  final response = await http.get(uri, headers: {
    'x-api-key': apiKey,
  });

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

The response will have a surah object with metadata and an ayahs array. Each ayah has arabic, translations, and audio_url fields. See the full shape in the API docs.

Tip. Pass ?reciter=alafasy to get audio URLs in the ayah objects. Available reciters are alafasy, abdul_basit, sudais, husary, ajamy, shaatri, muaiqly, and dosari. Swap the name to change the voice.

Create a Simple Data Model

Keep things tidy by mapping the JSON to a Dart class. This makes it easier to pass data around your widget tree without casting maps everywhere.

class Ayah {
  final int number;
  final String arabic;
  final String translation;
  final String? audioUrl;

  const Ayah({
    required this.number,
    required this.arabic,
    required this.translation,
    this.audioUrl,
  });

  factory Ayah.fromJson(Map<String, dynamic> json) {
    return Ayah(
      number: json['number'] as int,
      arabic: json['arabic'] as String,
      translation: (json['translations']?['en'] ?? '') as String,
      audioUrl: json['audio_url'] as String?,
    );
  }
}

List<Ayah> parseAyahs(Map<String, dynamic> data) {
  final ayahList = data['ayahs'] as List<dynamic>;
  return ayahList
      .map((a) => Ayah.fromJson(a as Map<String, dynamic>))
      .toList();
}

Render Arabic Text in Flutter

Arabic text needs two things to display correctly in Flutter. First, set textDirection to TextDirection.rtl. Second, use a font that has full Arabic glyph support. The Amiri font from Google Fonts works well and is easy to add with the google_fonts package.

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

class ArabicText extends StatelessWidget {
  final String text;

  const ArabicText({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      textDirection: TextDirection.rtl,
      textAlign: TextAlign.right,
      style: GoogleFonts.amiri(
        fontSize: 26,
        height: 2.0, // extra line height for diacritics
        color: Colors.black87,
      ),
    );
  }
}

The height: 2.0 is important. Arabic text with tashkeel (vowel marks) sits above and below the baseline. Without extra line height, marks from one line will overlap into the next line.

Tip. The Uthmani script (?script=uthmani) includes full tashkeel. The IndoPak script (?script=indopak) uses a different style common in South Asia. Pick the one your users expect.

Build the Surah Screen

Now put it all together in a screen widget. It uses a FutureBuilder to handle loading and error states, then shows each ayah in a card with Arabic text on top and the English translation below.

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

class SurahScreen extends StatelessWidget {
  final int surahNumber;
  final String surahName;

  const SurahScreen({
    super.key,
    required this.surahNumber,
    required this.surahName,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(surahName)),
      body: FutureBuilder<Map<String, dynamic>>(
        future: fetchSurah(surahNumber),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          }

          final ayahs = parseAyahs(snapshot.data!);

          return ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: ayahs.length,
            itemBuilder: (context, index) {
              final ayah = ayahs[index];
              return AyahCard(ayah: ayah);
            },
          );
        },
      ),
    );
  }
}

class AyahCard extends StatelessWidget {
  final Ayah ayah;

  const AyahCard({super.key, required this.ayah});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Ayah number badge
            Row(
              children: [
                CircleAvatar(
                  radius: 14,
                  backgroundColor: Colors.teal.shade100,
                  child: Text(
                    '${ayah.number}',
                    style: const TextStyle(fontSize: 11, color: Colors.teal),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 12),
            // Arabic text
            ArabicText(text: ayah.arabic),
            const Divider(height: 24),
            // English translation
            Text(
              ayah.translation,
              style: GoogleFonts.inter(fontSize: 15, color: Colors.black54),
            ),
          ],
        ),
      ),
    );
  }
}

Add Audio Playback

Each ayah object from the API includes an audio_url when you pass the reciter query parameter. Use the just_audio package to play it. The example below adds a play button to the AyahCard. The player is created once per card and disposed when the widget leaves the tree.

import 'package:just_audio/just_audio.dart';

class AyahCard extends StatefulWidget {
  final Ayah ayah;

  const AyahCard({super.key, required this.ayah});

  @override
  State<AyahCard> createState() => _AyahCardState();
}

class _AyahCardState extends State<AyahCard> {
  final _player = AudioPlayer();
  bool _isPlaying = false;

  Future<void> _togglePlay() async {
    if (widget.ayah.audioUrl == null) return;

    if (_isPlaying) {
      await _player.stop();
      setState(() => _isPlaying = false);
    } else {
      await _player.setUrl(widget.ayah.audioUrl!);
      await _player.play();
      setState(() => _isPlaying = true);
      // Reset state when playback finishes
      _player.playerStateStream.listen((state) {
        if (state.processingState == ProcessingState.completed) {
          setState(() => _isPlaying = false);
        }
      });
    }
  }

  @override
  void dispose() {
    _player.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CircleAvatar(
                  radius: 14,
                  backgroundColor: Colors.teal.shade100,
                  child: Text('${widget.ayah.number}',
                      style: const TextStyle(fontSize: 11)),
                ),
                if (widget.ayah.audioUrl != null)
                  IconButton(
                    icon: Icon(_isPlaying ? Icons.stop : Icons.play_arrow),
                    color: Colors.teal,
                    onPressed: _togglePlay,
                  ),
              ],
            ),
            const SizedBox(height: 12),
            ArabicText(text: widget.ayah.arabic),
            const Divider(height: 24),
            Text(widget.ayah.translation,
                style: GoogleFonts.inter(fontSize: 15, color: Colors.black54)),
          ],
        ),
      ),
    );
  }
}

Fetch a Single Ayah

Sometimes you just want one verse, for example on a home screen widget or a splash screen. The endpoint /api/quran/surah/:s/ayah/:a returns a single ayah. Here is a quick helper for that.

Future<Ayah> fetchSingleAyah(int surah, int ayah) async {
  final uri = Uri.parse(
    '$baseUrl/api/quran/surah/$surah/ayah/$ayah'
    '?script=uthmani&translations=en&reciter=sudais',
  );

  final response = await http.get(uri, headers: {'x-api-key': apiKey});

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body) as Map<String, dynamic>;
    return Ayah.fromJson(data['ayah'] as Map<String, dynamic>);
  } else {
    throw Exception('Request failed: ${response.statusCode}');
  }
}

// Example: fetch Ayat al-Kursi (2:255)
final ayatAlKursi = await fetchSingleAyah(2, 255);

You can also call /api/quran/random to get a random ayah. That is useful for a "verse of the day" feature on your home screen.

List All Surahs for Navigation

A full app needs a surah list so users can browse and pick one. Call /api/quran/surahs to get all 114 surahs with their names in Arabic and English and their ayah count.

Future<List<dynamic>> fetchSurahList() async {
  final uri = Uri.parse('$baseUrl/api/quran/surahs');
  final response = await http.get(uri, headers: {'x-api-key': apiKey});

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body) as Map<String, dynamic>;
    return data['surahs'] as List<dynamic>;
  }
  throw Exception('Could not load surah list');
}

// Render as a ListView
// Each item has: number, name_arabic, name_english, ayah_count
ListTile(
  leading: CircleAvatar(child: Text('${surah["number"]}')),
  title: Text(surah['name_english']),
  trailing: ArabicText(text: surah['name_arabic']),
  onTap: () => Navigator.push(
    context,
    MaterialPageRoute(
      builder: (_) => SurahScreen(
        surahNumber: surah['number'] as int,
        surahName: surah['name_english'] as String,
      ),
    ),
  ),
),

Caching to Reduce API Calls

The free tier gives you 5,000 requests per 15 minutes without a key, and unlimited with a free key from ummahapi.com/register. Still, caching surah data locally is good practice. The simplest approach in Flutter is to store the decoded JSON in a Map in memory during the session. For persistence across app restarts, write the JSON string to a file using the path_provider package.

final Map<int, Map<String, dynamic>> _cache = {};

Future<Map<String, dynamic>> fetchSurahCached(int number) async {
  if (_cache.containsKey(number)) {
    return _cache[number]!;
  }
  final data = await fetchSurah(number);
  _cache[number] = data;
  return data;
}

For a production app, look at the hive or flutter_cache_manager packages on pub.dev. They make disk caching straightforward without much boilerplate.

FAQ

How do I render Arabic text correctly in Flutter?

Set textDirection: TextDirection.rtl on your Text widget and use an Arabic-supporting font like Amiri or Scheherazade New. Get Amiri from the google_fonts package with GoogleFonts.amiri(). Also set a line height of at least 1.8 so diacritics do not overlap between lines.

Can I get Quran audio in a Flutter app?

Yes. Add ?reciter=alafasy (or any other supported reciter name) to your API request. UmmahAPI will include an audio_url field in each ayah object. Pass that URL to AudioPlayer().setUrl() from the just_audio package and call play().

Which package should I use to make HTTP requests in Flutter?

The http package is the simplest option for basic REST calls and works fine for this use case. If you need request cancellation, interceptors, or automatic retries, use dio instead.

Does UmmahAPI support multiple translations for Quran verses?

Yes. Add ?translations=en,ur,fr to your request URL. You can include any combination of supported language codes. The response will have a translations object with a key for each language you requested.

Build your Quran Flutter app today

UmmahAPI gives you free access to the full Quran, translations, audio URLs, and more with a single base URL.

Read the docs