This guide outlines the steps to build the native Android app for Sleepy.
The Android app will:
- Communicate with the Python FastAPI backend (running locally or on a server)
- Use GPS to detect location
- Display prayer times and optimal sleep schedule
- Show a home screen widget with sleep information
- Send notifications at sleep time with creative quotes
Android App (Kotlin)
↓ HTTP/REST
Python FastAPI Backend (localhost:8000 or deployed)
↓
Prayer Times APIs + Google Calendar + SQLite Cache
-
Create new Android project in Android Studio
- Language: Kotlin
- Minimum SDK: API 26 (Android 8.0) or higher
- Build configuration: Jetpack Compose (modern UI)
-
Add dependencies (in
build.gradle.kts):dependencies { // Networking implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") // Coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") // ViewModel & LiveData implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") // Location services implementation("com.google.android.gms:play-services-location:21.0.1") // WorkManager for notifications implementation("androidx.work:work-runtime-ktx:2.8.1") // Compose implementation("androidx.compose.ui:ui:1.5.4") implementation("androidx.compose.material3:material3:1.1.2") implementation("androidx.compose.ui:ui-tooling-preview:1.5.4") }
Create a Retrofit interface to communicate with the backend:
// ApiService.kt
interface SleepyApiService {
@GET("location/gps")
suspend fun getLocationFromGPS(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double
): LocationInfo
@POST("sleep-schedule/full")
suspend fun getFullSleepSchedule(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double,
@Query("create_calendar_event") createCalendarEvent: Boolean = false
): FullScheduleResponse
@GET("quotes/random")
suspend fun getRandomQuote(): QuoteResponse
}
// Data models
data class LocationInfo(
val city: String,
val country: String,
val latitude: Double,
val longitude: Double,
val timezone: String,
val is_uzbekistan: Boolean
)
data class PrayerTimes(
val date: String,
val fajr: String,
val sunrise: String,
val dhuhr: String,
val asr: String,
val maghrib: String,
val isha: String,
val city: String,
val country: String,
val latitude: Double,
val longitude: Double
)
data class SleepSchedule(
val date: String,
val sleep_start: String,
val sleep_end: String,
val duration_hours: Double,
val isha_time: String,
val fajr_time: String,
val notes: String?
)
data class FullScheduleResponse(
val location: LocationInfo,
val prayer_times: PrayerTimes,
val sleep_schedule: SleepSchedule,
val time_until_sleep: String?,
val notification_quote: String
)
data class QuoteResponse(val quote: String)// LocationManager.kt
class LocationManager(private val context: Context) {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
suspend fun getCurrentLocation(): Location? {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return null
}
return suspendCoroutine { continuation ->
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
continuation.resume(location)
}
.addOnFailureListener { e ->
continuation.resumeWithException(e)
}
}
}
}// MainActivity.kt
@Composable
fun SleepScheduleScreen(viewModel: SleepViewModel) {
val scheduleState by viewModel.scheduleState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Sleepy",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
when (scheduleState) {
is ScheduleState.Loading -> {
CircularProgressIndicator()
}
is ScheduleState.Success -> {
val data = (scheduleState as ScheduleState.Success).data
// Location & Prayer Times
Card(modifier = Modifier.padding(bottom = 16.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Location: ${data.location.city}, ${data.location.country}")
Text("Fajr: ${data.prayer_times.fajr}")
Text("Isha: ${data.prayer_times.isha}")
}
}
// Sleep Schedule
Card(modifier = Modifier.padding(bottom = 16.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Sleep Time: ${data.sleep_schedule.sleep_start}")
Text("Wake Time: ${data.sleep_schedule.sleep_end}")
Text("Duration: ${data.sleep_schedule.duration_hours} hours")
data.time_until_sleep?.let {
Text("Time until sleep: $it")
}
}
}
// Quote
Card {
Text(
text = data.notification_quote,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
is ScheduleState.Error -> {
Text("Error: ${(scheduleState as ScheduleState.Error).message}")
}
}
}
}// SleepWidget.kt
class SleepWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
SleepWidgetContent()
}
}
}
@Composable
fun SleepWidgetContent() {
// Fetch data from backend or local cache
val schedule = /* load from cache or API */
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(16.dp)
) {
Text("Sleep Time")
Text(schedule.sleep_start, style = TextStyle(fontSize = 24.sp))
Text("Prayer Times", modifier = GlanceModifier.padding(top = 8.dp))
Text("Fajr: ${schedule.fajr}")
Text("Isha: ${schedule.isha}")
Text("Duration: ${schedule.duration_hours} hrs")
}
}// NotificationWorker.kt
class SleepNotificationWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
// Get today's schedule
val schedule = fetchSleepSchedule()
// Get random quote
val quote = fetchRandomQuote()
// Show notification
showNotification(schedule.sleep_start, quote)
return Result.success()
}
private fun showNotification(sleepTime: String, quote: String) {
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_sleep)
.setContentTitle("Time to Sleep! ($sleepTime)")
.setContentText(quote)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
NotificationManagerCompat.from(applicationContext)
.notify(NOTIFICATION_ID, notification)
}
}Option 1: Local Backend (Development)
- Run Python backend on your computer:
python main.py - Connect Android emulator or phone to same WiFi
- Use computer's local IP (e.g.,
http://192.168.1.100:8000)
Option 2: Deployed Backend (Production)
- Deploy Python backend to a cloud service:
- Heroku
- Google Cloud Run
- AWS EC2
- DigitalOcean
- Update API base URL in Android app
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />-
Test API connection:
- Run backend:
python main.py - Test endpoint:
http://localhost:8000/docs
- Run backend:
-
Test Android app:
- Connect to backend via local network
- Test GPS location detection
- Verify prayer times display correctly
- Test sleep schedule calculation
- Verify notifications work
- Implement offline caching in Android (Room database)
- Add settings screen for customization
- Implement widget auto-refresh
- Add dark mode support
- Publish to Google Play Store
The Sleepy app features a unique, beautiful UI inspired by:
- Studio Ghibli films: Warm, dreamy color palettes and organic aesthetics
- Solarized color scheme: Eye-comfortable, carefully balanced contrasts
- Canvas/painting texture: Subtle paper grain and watercolor-like backgrounds
- Twitter/X layout: Full-screen, text-first information density
The app includes two gorgeous themes already implemented:
- Base: Warm cream background (#FDF6E3) like aged parchment
- Accents: Soft sage greens, gentle sky blues, warm peach tones
- Feel: Sunny afternoon in a Ghibli meadow
- Base: Deep forest blue-green (#1A1F1E) like moonlit woods
- Accents: Moonlit greens, twilight blues, warm amber glows
- Feel: Cozy Ghibli night scene
All UI components are in android/app/src/main/java/com/sleepy/ui/:
- Complete light/dark color schemes
- Prayer-time specific colors
- Gradient definitions for watercolor effects
- Material 3 integration
- Custom typography with generous line-heights (1.6-1.8)
- Composition locals for theme access
CanvasTextureLayer: Subtle canvas weave patternWatercolorOverlay: Organic gradient washOrganicDivider: Hand-drawn style separators- Perlin-noise based texture generation
- Multi-layer texture composition
- Vignette effect (darker edges like framed paintings)
PaintedText: Text with subtle shadows for depth
- Twitter/X-style edge-to-edge design
- Large, readable typography
- No cards/boxes - just text on textured canvas
- Sections: Sleep time, prayer times, wake time, quote
import com.sleepy.ui.theme.GhibliTheme
@Composable
fun MyApp() {
GhibliTheme(darkTheme = false) { // or true for dark theme
MainScreen(
sleepTime = "19:30",
wakeTime = "04:00",
fajrTime = "05:30",
ishaTime = "19:00",
duration = 7.0f,
quote = "Your brain cells are begging for a reboot!",
location = "Tashkent, Uzbekistan"
)
}
}import com.sleepy.ui.theme.GhibliTheme
@Composable
fun MyComponent() {
val fajrColor = GhibliTheme.colors.fajrColor
val ishaColor = GhibliTheme.colors.ishaColor
val sleepColor = GhibliTheme.colors.sleepTimeColor
Text("Fajr: 05:30", color = fajrColor)
}Adjust texture subtlety in TexturedBackground.kt:
CanvasTextureLayer(
textureColor = textureColor,
intensity = 0.04f, // 0.02-0.06 range recommended
seed = 42
)Pre-configured with excellent readability:
- Display Large: 72sp for main sleep time
- Headline: 24-32sp for prayer names
- Body Large: 18sp with 32sp line-height for quotes
- All with generous spacing and warm, readable tones
- Full-screen text: No wasted space with excessive chrome
- Organic elements: Hand-drawn dividers, subtle imperfections
- Canvas texture: 3-5% opacity, pixel-level variation (not literal paper images)
- Painting vibes: Watercolor washes, vignettes, soft shadows
- Eye comfort: Solarized-based colors tested for long reading sessions
- Information hierarchy: Typography size/weight, not boxes/cards