Integrating MABIMS API into iOS & Android Apps

In a previous post we covered showing Hijri dates on websites: just fetch /today, cache for an hour on the server, done. Easy, because websites are assumed to always be online.
Mobile apps are a different story. Users might open the app in airplane mode, in a weak signal area, or intentionally go offline. If the app only relies on fetch('/today') with no fallback, the Hijri date simply won’t appear when offline. For mosque/pesantren widgets or worship apps, this is a real problem — not just a UX hiccup.
So for native apps (Swift/Kotlin), the strategy needs two layers: one for online conditions (accurate, real-time), and one for offline fallback (from local cache).
Why MABIMS (Not Umm al-Qura)?
Section titled “Why MABIMS (Not Umm al-Qura)?”If you install a Hijri calendar library from a package manager, almost all default to Umm al-Qura criteria (Saudi Arabia’s official calendar) — easy to find, well-documented, but can differ ±1 day from the date announced by Indonesia’s Kemenag. For apps used by Indonesian users (fasting schedules, holidays, etc.), a one-day difference can confuse ordinary people. The full story is in The Story Behind MABIMS API, on why this post and API were created.
2-Layer Architecture
Section titled “2-Layer Architecture”Layer 1: Annual cache (/year) as offline fallback
Fetch once:
GET https://api.mabims.dev/api/v1/year?year=2026&calendar=gregorianUse calendar=gregorian (not hijri), because Kemenag’s calendar is published per Gregorian year (“Calendar 2026”), so it’s more natural to store and refresh per Gregorian year too. One fetch gives you January–December complete, easy to look up from the device’s Gregorian date.
Store the result locally (Room/SharedPreferences on Android, or JSON file + UserDefaults on iOS). This is what you use when the app is offline.
Layer 2: /today live every time the app opens & is online
GET https://api.mabims.dev/api/v1/today?tz={device_timezone}This is the most accurate source of truth, because it can catch the latest data updates from Kemenag. The app’s read flow:
- Has connection? → hit
/today, display the result. - Fails (timeout/no network)? → fallback: look up today’s date in the local
/yearcache.
Network-first, not cache-first, because /today is cheap (small response) and already served via CDN edge cache with a TTL that expires exactly at local midnight, so it almost never hits the origin.
When to Refresh the /year Cache?
Section titled “When to Refresh the /year Cache?”Simple: scheduled once at the end of December, re-fetch /year for the next Gregorian year (background task: BGAppRefreshTask on iOS, periodic WorkManager on Android).
One important note: this approach is intentionally simple and sufficient for most cases, because calendar=gregorian makes the refresh cycle align with the Gregorian year (not the Hijri year which shifts ~11 days each year if you cache per Hijri year). But still provide a safety net: if /today fails and the cache lookup returns nothing (data hasn’t been refreshed, or the app was first installed mid-year), trigger a /year fetch on the spot right then — don’t wait for the next December schedule.
Code Example: Swift (iOS)
Section titled “Code Example: Swift (iOS)”import Foundation
struct HijriOutput: Codable { let date: String let day: Int let month: Int let month_name: String let year: Int}
struct TodayResponse: Codable { let output: HijriOutput let source: String let warnings: [String]}
final class HijriRepository { private let baseURL = "https://api.mabims.dev/api/v1" private let cacheKey = "hijri_year_cache" private let cacheYearKey = "hijri_year_cache_year"
// Layer 2: live, called every time the app opens func today() async -> (date: String, warnings: [String]) { let tz = TimeZone.current.identifier if let url = URL(string: "\(baseURL)/today?tz=\(tz)") { if let (data, _) = try? await URLSession.shared.data(from: url), let decoded = try? JSONDecoder().decode(TodayResponse.self, from: data) { return (decoded.output.date, decoded.warnings) } } // fallback to local cache return fallbackFromCache() ?? ("-", []) }
private func fallbackFromCache() -> (String, [String])? { guard let cached = UserDefaults.standard.data(forKey: cacheKey) else { return nil } // parse & find the entry where gregorian date == today // (/year structure: { months: { "1": [...], "2": [...] } }) // ...lookup logic per your model return nil // placeholder }
// Layer 1: annual refresh, called from BGAppRefreshTask at end of December // or on-the-spot if fallback fails to find data func refreshYearCache(year: Int) async { guard let url = URL(string: "\(baseURL)/year?year=\(year)&calendar=gregorian") else { return } if let (data, _) = try? await URLSession.shared.data(from: url) { UserDefaults.standard.set(data, forKey: cacheKey) UserDefaults.standard.set(year, forKey: cacheYearKey) } }}Register the refresh task in AppDelegate/SceneDelegate using BGTaskScheduler, scheduled once per year around December 28–31.
Code Example: Kotlin (Android)
Section titled “Code Example: Kotlin (Android)”data class HijriOutput( val date: String, val day: Int, val month: Int, val month_name: String, val year: Int)data class TodayResponse( val output: HijriOutput, val source: String, val warnings: List<String>)
class HijriRepository( private val api: MabimsApi, // Retrofit interface private val prefs: SharedPreferences) { // Layer 2: live, called every time the app opens suspend fun today(): Pair<String, List<String>> { return try { val tz = java.util.TimeZone.getDefault().id val response = api.getToday(tz = tz) response.output.date to response.warnings } catch (e: Exception) { fallbackFromCache() ?: ("-" to emptyList()) } }
private fun fallbackFromCache(): Pair<String, List<String>>? { val cachedJson = prefs.getString("hijri_year_cache", null) ?: return null // lookup today's gregorian date from cached JSON (/year response) return null // placeholder, parse per your model }
// Layer 1: annual refresh via WorkManager, end of December // or on-the-spot if fallback fails to find data suspend fun refreshYearCache(year: Int) { val response = api.getYear(year = year, calendar = "gregorian") prefs.edit().putString("hijri_year_cache", response.toRawJson()).apply() prefs.edit().putInt("hijri_year_cache_year", year).apply() }}Schedule via WorkManager periodic request (1-year interval, or more practical: a one-time request that reschedules itself each time it runs, triggered around end of December).
Edge Cases to Handle
Section titled “Edge Cases to Handle”- First launch with no internet at all. If the app is just installed and opened offline, the
/yearcache doesn’t exist yet. Options: bundle a minimal data snapshot inside the app (assets/res/rawon Android, bundle resource on iOS), or force the user online once during onboarding. - Timezone. Don’t hardcode
Asia/Jakarta. Get it fromTimeZone.current.identifier(Swift) orTimeZone.getDefault().id(Kotlin), and pass it as thetzparam. warningsfield. If it’s not empty (e.g., date is near hilal visibility threshold), show a small indicator in the UI, like “may shift 1 day from official announcement”, so users aren’t surprised if a revision comes.
Bonus: Using React Native/Expo?
Section titled “Bonus: Using React Native/Expo?”If your stack is React Native, a simpler alternative: use the mabims-hijri package — 2023–2026 data is bundled directly in the package, so no manual cache architecture is needed for common use cases.
npm install mabims-hijriWrapping Up
Section titled “Wrapping Up”Full endpoint reference at:
If your app needs higher rate limits or SLA (e.g., for large-scale apps), contact halo@pixostudio.id.
