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

Android uygulama kısayolları ve Hızlı Ayarlar Karosu: uygulamayı açmadan su kaydetmek

Android'in dinamik ShortcutManager API'sine ve TileService'e pratik bir rehber — tek dokunuşlu bir eylemin uygulamayı tamamen atlamasını nasıl sağlarsınız, çoğu uygulamayı çuvallatan tuzaklarla birlikte.

MFKAPPS 4 dk okuma

Herhangi bir uygulama eyleminin en hızlı hâli, uygulamayı hiç açmayandır. Hydrame’in çekirdek döngüsü — bir bardak su kaydetmek — günde onlarca kez gerçekleşiyor, ve birinden telefonun kilidini açmasını, ikonu bulmasını, soğuk başlatmayı beklemesini, sonra bir düğmeye dokunmasını istemek, tek adım olması gereken bir şey için üç adım fazla. Android bunu tek dokunuşa indirmenin iki yolunu sunar: ana ekran uygulama kısayolları ve bir Hızlı Ayarlar Karosu (Quick Settings Tile). Aynı sorunu farklı giriş noktalarından çözerler, ve ikisi de göründüğünden daha küçük bir uygulama işi. İşte her birinin gerçekte nasıl çalıştığı.

İki giriş noktası, iki farklı iş

Uygulama kısayolları başlatıcı ikonunun altında yaşar — uzun basarsınız ve tanımladığınız eylemlerle bir menü açılır. Bunlar bu belirli uygulamaya bağlı, küçük bir yüksek frekanslı eylem setine yöneliktir; ikonu zaten görünen herkes tarafından keşfedilebilirler.

Bir Hızlı Ayarlar Karosu bildirim gölgesinde, Wi-Fi ve Bluetooth’un yanında yaşar. Birinin her yerden, bir ikon aramaya bile gerek kalmadan erişilebilir olmasını istediği tek bir eylem içindir — bir uygulama özelliğinden çok bir donanım düğmesine yakın.

Hydrame ikisini birden sunar: başlatıcı ikonunun altında “250ml kaydet” ve “500ml kaydet” için bir kısayol, ve kilit ekranı gölgesinden tek dokunuşla varsayılan bir miktarı kaydeden bir Hızlı Ayarlar Karosu. Bunlar ayrı API’ler, ayrı kayıt yolları, ve onları birleştirmeye çalışmak yerine ayrı ayrı inşa etmeye değer.

Dinamik kısayollar: ShortcutManagerCompat

Kısayollar iki çeşit gelir — statik (XML’de tanımlanan, derleme zamanında sabit) ve dinamik (çalışma zamanında gönderilen, uygulama durumuna göre değişebilen). Sabit bir “250ml kaydet” değişmeye ihtiyaç duymaz, dolayısıyla statik işe yarardı, ama dinamik kısayollar miktarların birinin gerçekten içtiğini yansıtmasını sağlar — uygulamanın tahmin ettiği iki değer değil, en çok kullandığı iki boyut.

val shortcut = ShortcutInfoCompat.Builder(context, "log_250ml")
    .setShortLabel("250ml")
    .setLongLabel("Log 250ml of water")
    .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_glass))
    .setIntent(
        Intent(context, LogIntakeReceiverActivity::class.java).apply {
            action = ACTION_LOG_INTAKE
            putExtra(EXTRA_AMOUNT_ML, 250)
        }
    )
    .build()

ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut250, shortcut500))

Intent’in hedefi bir Activity olmak zorunda, bir BroadcastReceiver değil — bu, kısayol API’sinin dayattığı tek katı kısıtlama, bir alıcının doğru tercih olduğu bildirim-eylemi deseninin aksine. Google’ın kendi tavsiyesi, o Activity’yi mümkün olduğunca görünmez yapmaktır: layout yok, yazma işleminden hemen sonra finish().

class LogIntakeReceiverActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val amount = intent.getIntExtra(EXTRA_AMOUNT_ML, 0)

        lifecycleScope.launch {
            intakeDao.logIntake(amount, timestamp = System.currentTimeMillis())
            Toast.makeText(this@LogIntakeReceiverActivity, "Logged ${amount}ml", Toast.LENGTH_SHORT).show()
            finish()
        }
    }
}

Ona şeffaf bir tema verin (windowIsTranslucent ile geçersiz kılınmış Theme.Material3.DayNight.NoActionBar) ve geçiş bir uygulama başlatma değil, bir sistem onayı gibi okunur — bunların hepsi orta segment bir cihazda yarım saniyenin altında çözülür.

Sıralama ve tavan: kısayollar bir menü değildir

ShortcutManagerCompat.setDynamicShortcuts() her çağrıda tüm seti değiştirir, ve sistem sayıya bir tavan koyar — getMaxShortcutCountPerActivity() — genellikle başlatıcıya bağlı olarak dört veya beş civarında. Bundan fazlasını gönderirseniz fazlalıklar kuyruğa alınmaz, sessizce düşürülür. Sıra da önemlidir: kısayollar onları verdiğiniz sırayla render edilir, dolayısıyla birinin en çok kaydettiği miktar sonda değil başta olmalıdır.

Diğer tuzak, setDynamicShortcuts()’ı çok sık çağırmaktır. Her çağrı gerçek durum değişiklikleri içindir — bir kullanıcının alışkanlıklarının haftalar içinde değişmesi, her uygulama başlatmasında değil. Aynı iki kısayolu her soğuk başlatmada yeniden yazmak size bir sistem çağrısına mal olmaktan başka hiçbir işe yaramaz; göndermeden önce setin gerçekten değişip değişmediğini kontrol edin.

Hızlı Ayarlar Karosu: TileService

Bir karo bir Activity değil, bir servistir — Android onun yaşam döngüsünü kontrol eder, ve önemli olan tam olarak bir giriş noktanız vardır: onClick().

class LogWaterTileService : TileService() {
    override fun onStartListening() {
        super.onStartListening()
        qsTile?.apply {
            label = "Log water"
            icon = Icon.createWithResource(this@LogWaterTileService, R.drawable.ic_tile_glass)
            state = Tile.STATE_ACTIVE
            updateTile()
        }
    }

    override fun onClick() {
        super.onClick()
        val pendingResult = goAsync()
        lifecycleScope.launch {
            try {
                intakeDao.logIntake(defaultAmountMl(), timestamp = System.currentTimeMillis())
            } finally {
                pendingResult.finish()
            }
        }
    }
}

goAsync() burada, bir BroadcastReceiver’da olduğu aynı nedenle karşınıza çıkar: onClick()’in hızlı dönmesi beklenir, ve ana thread’de bir Room yazması gölgenin kapanma animasyonunu takılmalı yapar. Karonun manifestoda BIND_QUICK_SETTINGS_TILE izniyle tanımlanması gerekir, ve — insanları yakalayan kısım burası — hiçbir yerde otomatik olarak görünmez. Kullanıcının onu gölgenin düzenleme ekranından etkin karolarına sürüklemesi gerekir. TileService.requestListeningState(), Android 13+‘ı onu eklemeyi önermeye teşvik edebilir, ama yerleşimi zorlamanın bir yolu yoktur, ve çoğu kullanıcının bu zahmete gireceğini beklemek için bir sebep yoktur. Karoyu birincil bir etkileşim yolu değil, ileri düzey kullanıcı özelliği olarak ele alın.

Karonun durumunu dürüst tutmak

Uygulama durumundan bağımsız olarak her zaman aynı ikonu gösteren bir karo, ilk bozulduğunda bozuk görünür. Hydrame’in günlük hedefi zaten karşılanmışsa, onStartListening() bunu yansıtmanın yeridir:

override fun onStartListening() {
    super.onStartListening()
    lifecycleScope.launch {
        val goalMet = intakeDao.todayTotal() >= goalDao.currentGoal()
        qsTile?.apply {
            state = if (goalMet) Tile.STATE_INACTIVE else Tile.STATE_ACTIVE
            subtitle = if (goalMet) "Goal reached" else null
            updateTile()
        }
    }
}

onStartListening(), karonuzu içeren gölge her görünür olduğunda tetiklenir, bir zamanlayıcıda değil, dolayısıyla bu okuma ucuz ve tanım gereği güncel olur — yoklama yok, senkron tutmak için arka plan işi yok.

Bunun neye değdiği

Bu API’lerden hiçbiri, dokümanlardan açıkça anlaşılmayan iki kuralı bildiğinizde zor değildir: kısayollar arka plandaki bir yazma için bile bir Activity’e ihtiyaç duyar, ve bir karo, bir bildirim eylemi alıcısının aynı nedenle goAsync()’e ihtiyaç duyar. Kazandırdıkları şey koduyla orantısız — birinin her gün tekrarladığı bir avuç eylem için tam bir uygulama başlatmasının yerini alan bir uzun basış veya bir gölge dokunuşu. Bu, kullanıcıların bilinçli olarak fark etmediği türden bir sürtünme — onu kaldırırsanız yalnızca yokluğunu fark ederler. Bir veya iki baskın eylemi olan herhangi bir uygulama için — bir şeyi kaydetmek, bir şeyi başlatmak, bir şeyi açıp kapatmak — bir öğleden sonraya değer.