İçeriğe geç
Tüm yazılar

2026'da Jetpack Glance: verisi hakkında asla yalan söylemeyen bir ana ekran widget'ı

Jetpack Glance widget'ları için pratik bir rehber — durum yönetimi, tıklama aksiyonları ve widget'ları eski veri göstermeye iten güncelleme kotası tuzağı.

MFKAPPS 4 dk okuma

Bir widget, ana ekrandaki yerini ancak doğruyu söylüyorsa hak eder. Dünün rakamını gösterdiği ya da bir dokunuşun üç saniye boyunca hiçbir şey yapmadığı an, kullanıcı onu bir klasöre sürükler ve bir daha asla bakmaz. Bu, kulağa geldiğinden daha yüksek bir çıta — gördüğüm widget kodlarının çoğu (kendi ilk denemem dahil) kurulumdan hemen sonra dürüst, bir saat sonra sessizce yanlıştır.

Bu yıl Hydrame’e bir ana ekran widget’ı ekledim: günün su tüketimi, bir bardak eklemek için tek dokunuş, uygulamayı açmaya gerek yok. İşte bunu doğru tutan Jetpack Glance mimarisi — ve arama yaptığınızda karşınıza çıkan “widget’ım takılı kaldı” bug raporlarının çoğuna sebep olan güncelleme sıklığı tuzağı.

RemoteViews yerine neden Glance

Eski AppWidgetProvider + RemoteViews API’si çalışır, ama arayüzü iki kez yazmak anlamına gelir — bir kez uygulama için Compose’da, bir kez widget için XML tabanlı RemoteViews’da, çok daha küçük bir desteklenen view seti ile. Jetpack Glance bu ayrımı ortadan kaldırır: widget’ı Compose’a benzer bir DSL’de yazarsınız, Glance bunu render sırasında sizin için RemoteViews’a derler.

class HydrationWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            val prefs = currentState<Preferences>()
            val consumedMl = prefs[intPreferencesKey("consumed_ml")] ?: 0
            val goalMl = prefs[intPreferencesKey("goal_ml")] ?: 2000

            GlanceTheme {
                Column(modifier = GlanceModifier.padding(12.dp)) {
                    Text("$consumedMl / $goalMl ml", style = TextStyle(fontSize = 18.sp))
                    Button(
                        text = "+ 250 ml",
                        onClick = actionRunCallback<LogGlassAction>(),
                    )
                }
            }
        }
    }
}

Normal Compose’a alışkınsanız iki şey dikkat çeker: provideGlance kendi yaşam döngüsünde çalışır, çoğu zaman uygulamanızın sürecinin dışında; ve okuduğu durum Glance’ın kendi Preferences deposundan gelir — bir ViewModel değil, bellekte tuttuğunuz bir StateFlow değil. Widget’ın telefon yeniden başladıktan saniyeler sonra, uygulamanız hiç çalışmamışken bile soğuk halde doğru render edebilmesi gerekir.

Durum doğrudan Room’da değil, bir DataStore’da yaşar

Glance widget’ları, bir Activity’nin yapabildiği gibi bir Room Dao’sundan canlı bir Flow tutamaz — onu taze tutacak uzun ömürlü bir collector yoktur. İşe yarayan desen, altta yatan Room verisi her değiştiğinde yazılan, Glance render ederken okunan, widget başına küçük bir Preferences DataStore’dur:

suspend fun syncWidgetState(context: Context, dao: HydrationDao) {
    val today = dao.totalForToday()
    val manager = GlanceAppWidgetManager(context)
    val ids = manager.getGlanceIds(HydrationWidget::class.java)

    ids.forEach { id ->
        updateAppWidgetState(context, id) { prefs ->
            prefs[intPreferencesKey("consumed_ml")] = today
        }
    }
    HydrationWidget().updateAll(context)
}

syncWidgetState’i, günün toplamını değiştiren her yazmadan hemen sonra çağırın — bir bardak eklemek, bir kaydı düzenlemek, gece yarısı sıfırlanması. Room’un (gerçek kaynak) ve widget’ın (önbelleğe alınmış bir anlık görüntü) birbirinden uzaklaşmasını önleyen tek şey bu çağrıdır. Widget’ı yazma anında yenilediğiniz bir okuma modeli olarak düşünün, canlı bir görünüm olarak değil.

Güncelleme kotası tuzağı

Android, bir widget’ın AppWidgetManager üzerinden ne sıklıkla güncellenebileceğini sınırlar — platform dokümanları periyodik güncellemeler için kabaca 30 dakikalık bir alt sınırdan bahseder, ve pratikte OEM pil yöneticileri bu sınırı bile güvenilmez hale getirir. Widget’ın XML meta verisindeki updatePeriodMillis’e güvenip işi bitmiş sayarsanız, bir kez doğru, günün geri kalanında eski kalan bir widget elde edersiniz.

Çözüm, widget’ı yoklama yapan bir şey olarak görmeyi bırakıp, her ilgili olayda ona bir şey ittiğiniz bir şey olarak görmektir:

  • updateAll()’u doğrudan yazma yolundan tetikleyin (yukarıdaki kod), bir arka plan zamanlamasından değil.
  • WorkManager’ı yalnızca gerçekten uygulama etkileşimi olmadan gerçekleşmesi gereken güncellemeler için kullanın — “günün toplamının” gece yarısı sıfırlanması gibi — ve bu işi seyrek ve pil dostu tutun.
  • 30 dakikalık alt sınırı daha sık bir periyodik worker ile aşmaya çalışmayın. Kazanamazsınız, ve işletim sisteminin zaten kısıtlayacağı bir sonuç için pil harcarsınız.

Widget yalnızca gerçek olaylara tepki olarak güncellendiğinde, eski veri raporları büyük ölçüde ortadan kalkar — çünkü widget’ın “bir sonraki yoklamayı bekliyor” olduğu bir pencere kalmaz.

Tıklama aksiyonları ayrı bir süreçte çalışır

actionRunCallback<LogGlassAction>(), widget sınıfınızda bir metot çağırmaz — Glance’ın taze örneklediği, uygulamanızın o an bellekte tuttuğu herhangi bir duruma garanti bir bağlantısı olmayan bir ActionCallback’i tetikler:

class LogGlassAction : ActionCallback {
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters,
    ) {
        val db = AppDatabase.get(context)
        db.hydrationDao().logGlass(amountMl = 250)
        syncWidgetState(context, db.hydrationDao())
    }
}

Callback’in ihtiyaç duyduğu her bağımlılık — bu örnekte veritabanı — yalnızca Context’ten çözülebilir olmalıdır, çünkü ortada bir Activity, bir ViewModel, çoğu zaman çalışan başka bir uygulama parçası bile yoktur. Onu, uygulama süreci az önce öldürülmüş gibi yazın — çünkü gerçek bir cihazda genellikle öldürülmüştür.

İlk widget’ını eklemek isteyen herkese söyleyeceğim

  1. Widget’ı canlı bir ayna değil, bir okuma modeli olarak tasarla. Yazmada senkronize et; onu yoklama.
  2. Her render’da soğuk başlangıç varsay. Güvenebileceğin bir ViewModel, önbelleğe alınmış bir singleton yok — her seferinde kalıcı depodan oku.
  3. Güncelleme alt sınırına karşı savaşma, ona saygı göster. Yazmada iten bir widget, sıkı bir periyodik zamanlamaya hiç ihtiyaç duymadan doğru kalır.
  4. Yalnızca temiz kurulumdan sonra değil, yeniden başlatma ve force-stop sonrasında da test et. Widget bug’larının çoğu gerçekte orada yaşar.

Hydrame’in widget’ı birkaç haftadır yayında ve akılda kalan ders, Android’in arka plan işlerinin her yerinde karşınıza çıkanla aynı: platform sebepsiz yere zorluk çıkarmıyor — pili, istediği zaman yoklayabileceğini varsayan koddan koruyor. Buna karşı savaşmak yerine bu varsayımın etrafında tasarım yapın, widget da fazla ekstra çaba olmadan dürüst kalsın.