Uygulama temeli

Jetpack AppState ile Compose durumunu kullanıcı arayüzünün ötesine taşıma

Hoparlörler

  • Ben Sagmoe

    Geliştirici İlişkileri Mühendisi

  • Elif Bilgin Morris

    Yazılım Mühendisi

Hakkında

Konuşma hakkında

Compose'un anlık görüntü çalışma zamanı, otomatik bağımlılık izleme ve atomik güncellemeleri kullanıcı arayüzü katmanında durmak zorunda değildir. Jetpack AppState, Compose durumunu yukarı taşımanıza olanak tanır. Durumu kullanıcı arayüzüne ulaşmadan önce anlık görüntü sistemiyle yükseltip türeterek ekranlarınıza ulaşanları kolaylaştırır.

Bu oturumda, AppState'in iki temel kitaplığı hakkında bilgi edineceksiniz. Durumu yukarı taşımak için StateStore'u, düz Kotlin kontrol akışıyla durumu türetmek amacıyla oluşturmayı kullanmak için de Transform'u inceleyeceğiz. Böylece karmaşık Flow birleştirme zincirlerini ortadan kaldıracak ve kullanıcı arayüzünüzü zahmetsizce senkronize edebileceksiniz.

Özet

  • Compose Runtime, Compose UI olmadan genel amaçlı bir durum yönetimi motoru olarak çalışır. Anlık görüntü sistemi, okuma işlemlerini dinamik olarak izler ve eşzamanlı güncellemeleri özellikler arasında tutarlı tutar.
  • Jetpack AppState Transform, gözetimsiz composable bloklardan Compose durumunu hesaplar ve gösterir. Erken dönüşleri ve iç içe yerleştirilmiş durum türetmelerini desteklerken combine gibi karmaşık Flow operatör zincirlerinin yerini alır.
  • Dönüşüm, dönüşümleri kullanıcı arayüzü iş parçacığının dışında çalıştırmak için istekli bir başsız kare saati kullanır. İzleme durumları değişmediğinde saat durur ve gereksiz işlem yapılmaz.
  • Jetpack AppState StateStore, Compose durumunu etkinlikler ve arka plan hizmetleri gibi farklı bileşenler arasında paylaşır. DataStore ile entegre edildiğinde, özel standart kod olmadan işlem sonlandırma durumunda anahtarların korunmasını sağlar.

Kaynaklar

Gemini Notebook

Konular

  • jetpack-compose
  • jetpack-appstate
  • state-management
  • android-architecture
  • compose-runtime

Transkript

Giriş

  • Merhaba. Jetpack AppState ile Compose durumunu kullanıcı arayüzünün ötesine taşıma konulu oturuma hoş geldiniz.

  • Benim adım Ben. Jetpack mimari kitaplıkları üzerinde çalışan bir geliştirici ilişkileri mühendisiyim.

  • Ben de Elif. Uygulama mimarisi kitaplıkları üzerinde çalışan mühendislerden biriyim.

  • Bugün, Compose'un kullanıcı arayüzünün ötesine geçerek uygulamaların veri ve alan katmanlarına girdiği bir gelecekten bahsedeceğiz.

  • Artık hepinizin bildiği üzere Compose, Android'de kullanıcı arayüzü geliştirme için önerilen araç setidir.

  • Katlanabilir telefonlar ve tabletlerden widget'lara, saatlere, TV'lere ve XR cihazlara kadar her şeyi Compose destekler.

  • Ancak uygulamanız bu kullanıcı arayüzü yüzeyleri ve arka plan bileşenleri genelinde büyüdükçe, bunların arkasındaki durumu yönetmek, özellikle de iki yaygın sorunlu nokta etrafında zorlaşır.

  • Birincisi, karmaşık durumları türetmektir.

  • Birden fazla veri akışını, yerel kullanıcı arayüzü girişlerini ve ağ bağlantısı gibi ortam durumlarını bir araya getirmek genellikle combine, flatMapLatest ve distinctUntilChanged gibi operatörlerin karmaşık bir şekilde kullanılmasına neden olur.

  • İkincisi ise bileşenler arasında durum paylaşımıdır. Yani etkinlik, hizmet, uygulama widget'ı ve diğer bileşenleri senkronize tutmak ve gerektiğinde bu durumun işlem sonlandırmadan etkilenmemesini sağlamaktır.

  • Bu zorlukları incelediğimizde Compose'un nasıl oluşturulduğuna baktık.

  • Çoğu kişi Compose'u bir kullanıcı arayüzü araç seti olarak düşünür ancak aslında iki farklı katmandan oluşur: üstte Compose UI ve altta Compose Runtime.

  • Compose kullanıcı arayüzü; düzen, çizim ve giriş işlemlerini gerçekleştirirken Compose çalışma zamanı, durum yönetimi ve yeniden oluşturma için genel amaçlı bir motor görevi görür.

  • Kullanıcı arayüzüne bağımlı değildir.

  • Bu farkındalık, Jetpack AppState'i geliştirmemize yol açtı.

  • Bu, kullanıcı arayüzünün ötesinde Compose durumundan en iyi şekilde yararlanmaya odaklanan bir kitaplık ailesidir.

Compose State nedir?

  • Ancak AppState'e geçmeden önce, Compose durumu derken neyi kastettiğimiz konusunda hemfikir olduğumuzdan emin olalım.

  • Cevap: androidx.compose.runtime.State.

    /**
     * A value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value.
     */
    @Stable
    public interface State<out T> {
        public val value: T
    }
  • Daha aşina olduğunuz arayüz ise MutableState.

    /**
     * A mutable value holder where reads to the [value] property during the execution of a [Composable]
     * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
     * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
     * will be scheduled. If [value] is written to with the same value, no recompositions will be
     * scheduled.
     */
    @Stable
    public interface MutableState<T> : State<T> {
        override var value: T
        public operator fun component1(): T
        public operator fun component2(): (T) -> Unit
    }
  • Temel olarak bunlar yalnızca yalın container'lardır.

  • Bu işlevlerin çalışmasını sağlayan şey, Compose anlık görüntü sistemidir.

  • Yani Compose durumu derken aslında genellikle SnapshotState'i kastediyoruz.

  • Yani mutableStateOf yöntemini çağırdığınızda okuma ve yazma işlemlerini anlık görüntü sistemi üzerinden yönlendiren bir kapsayıcı oluşturmuş olursunuz.

  • Kodunuz, oluşturma gibi bir anlık görüntü gözlemcisinde her çalıştığında, Flow gibi bir akışın size vermediği iki özellik elde edersiniz.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
    }
  • Bunlardan ilki, otomatik ve dinamik okuma takibidir.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
  • Yani otomatik, abone olma veya toplama işlemi olmadığı anlamına gelir.

  • Abonelik, .value okumaktan ibarettir.

  • Buradaki Counter'a bakarsak count.value değerini doğrudan composable içinde okumak, okuma işlemini oluşturmaya bildirir ve Counter'ı okuyucu listesine kaydeder.

  • Şimdi de AddButton adlı kullanıcıya bakın.

    @Composable
    fun Counter() {
        val count = remember { mutableStateOf(0) }
        Column {
            Text("Count: ${count.value}")
            AddButton(onClick = { count.value++ })
        }
    }
    
    @Composable
    fun AddButton(onClick: () -> Unit) {
        Button(onClick = onClick) {
            Text("Add")
        }
    }
  • Yalnızca tıklamayı saymak için yazar.

  • Yani okuyucu değildir.

  • Kullanıcı AddButton düğmesini tıklayıp sayıyı değiştirdiğinde anlık görüntü sistemi bu yazma işlemini izler. Böylece oluşturma, AddButton öğesini değil, Counter öğesini geçersiz kılacağını bilir.

  • Dinamik ise okuma izlemenin, çalışma zamanında hangi kod yollarının gerçekten çalıştığına bağlı olarak gerçekleştiği anlamına gelir.

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(true) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • Burada, sayaç metnini bir isVisible işaretiyle koruyoruz.

  • isVisible doğru olduğunda hem isVisible hem de sayı okunur.

  • Bu nedenle Counter, her iki yayının da okuyucusu olarak kaydedilir.

  • isVisible yanlış olduğunda ise if dalı çalışmaz, count okunmaz. Bu nedenle Counter, count'un değil, yalnızca isVisible'nin okuyucusu olarak kaydedilir.

    @Composable
    fun Counter() {
        val isVisible = remember { mutableStateOf(false) }
        val count = remember { mutableStateOf(0) }
        Column {
            if (isVisible.value) {
                Text("Count: ${count.value}")
            } else {
                Text("Hidden")
            }
            AddButton(onClick = { count.value++ })
        }
    }
  • Bu nedenle, AddButton sayımı yazdığında geçersiz kılınacak bir şey kalmaz.

  • İkinci özellik ise tutarlılık ve ayrıntı düzeyi arasında seçim yapmak zorunda kalmamak.

  • Tutarlılık, bir anlık görüntüdeki birden fazla değeri güncellediğinizde, başka bir iş parçacığı aynı anda bu değerlere yazıyor olsa bile kodunuz çalışırken bu değişikliklerin izole kalması anlamına gelir.

  • Bu güncellemeler, anlık görüntü tamamlandığında tek seferde görünür hale gelir.

  • Ayrıntı düzeyi, ayrı mülklerin özellikle de birbiriyle ilişkili olmadıklarında ayrı ayrı tanımlanıp izlenebileceği anlamına gelir.

  • Bu nedenle, bir mülkün güncellenmesi yalnızca ona bağlı olan okuyucuları geçersiz kılar.

  • Şimdi bunu gerçek bir kodda inceleyelim.

  • Örneğin, üç durum parçası olan bir oyunu ele alalım: seviye adı, hayatta kalınan süre ve puan.

    val levelName: String = "Level 1 - Stay Alive!"
    val timeSurvived: Int = 0
    val score: Double = 0.0
  • Başlamak için her şeyi bir GameUiState sınıfında toplayarak ayrıntı düzeyinden ödün verip tutarlılığı seçen bir Flow uygulamasına bakalım.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
  • Yani updateScore her çalıştığında GameUiState'nin tamamını kopyalarsınız. Böylece hem timeSurvived hem de puan birlikte senkronize şekilde güncellenebilir.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
  • Kullanıcı arayüzünde GameScreen, kullanıcı arayüzü durumunu toplar ve alakalı özellikleri Başlık ve ScoreBadge'ya iletmek için bu durumdan okur.

    data class GameUiState(
        val levelName: String = "Level 1 - Stay Alive!",
        val timeSurvived: Int = 0,
        val score: Double = 0.0
    )
    
    class GameViewModel : ViewModel() {
        val uiState = MutableStateFlow(GameUiState())
        fun updateScore() = uiState.update {
            it.copy(
                timeSurvived = it.timeSurvived + 1,
                score = it.score + 10 + .01 * it.timeSurvived
            )
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state by viewModel.uiState.collectAsStateWithLifecycle()
        Column {
            Header(levelName = state.levelName)
            ScoreBadge(
                timeSurvived = state.timeSurvived,
                score = state.score
            )
        }
    }
  • GameScreen her durumu okuduğu için updateScore işlevine yapılan her çağrı, başlığın verileri hiç değişmemiş olsa bile GameScreen işlevini geçersiz kılar.

  • Ayrıntılı bilgi edinmek için her özelliği kendi akışına bölebilirsiniz ancak bu durumda tutarlılığı kaybedersiniz. Çünkü başka bir iş parçacığı, diğer güncelleme bloğu çalışırken bir değeri güncelleyebilir.

  • Şimdi de duruma dayalı alternatif bir uygulamayı ele alalım.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
  • Bu örnekte de GameUiState kullanıyoruz.

  • Ancak bu örnekte, her mülk mutableStateOf'e yetki verdiği için durum biraz farklı.

  • Burada updateScore, yazma işlemlerini withMutableSnapshot ile sarmalar. Bu da yazma işlemlerinde tutarlılık sağlar.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
  • Ardından kullanıcı arayüzünde GameScreen, State'i doğrudan Header ve ScoreBadge'e iletir.

    @Stable
    class GameUiState {
        var levelName by mutableStateOf("Level 1 - Stay Alive!")
        var timeSurvived by mutableStateOf(0)
        var score by mutableStateOf(0.0)
    }
    
    class GameViewModel : ViewModel() {
        val uiState = GameUiState()
        fun updateScore() = Snapshot.withMutableSnapshot {
            uiState.timeSurvived += 1
            uiState.score += 10 + .01 * uiState.timeSurvived
        }
    }
    
    @Composable
    fun GameScreen(viewModel: GameViewModel = viewModel()) {
        val state = viewModel.uiState
        Column {
            Header(state = state)
            ScoreBadge(state = state)
        }
    }
  • Özelliklerin hiçbirini okumaz. Bu nedenle, updateScore çalıştırıldığında GameScreen okuyucu olmaz.

  • Yalnızca gerçek bileşen (bu durumda ScoreBadge) geçersiz kılınır.

  • Son olarak, bunların hiçbirinin kullanıcı arayüzüne bağlı olmadığına dikkat edin.

  • Kullanıcı arayüzü, bu özelliklerin gerçekten öne çıktığı yerlerden yalnızca biridir.

  • Konuşmamıza katıldığınız için teşekkür ederiz.

  • Şaka bir yana, ilk kitaplık olan Transform'u anlatması için sözü Elif'e bırakıyorum.

Transform ile tanışın

  • Anladım.

  • Teşekkürler Berk.

  • Ben'in de belirttiği gibi, Compose anlık görüntü durumu bize otomatik ve dinamik okuma takibi, tutarlılık ve ayrıntı düzeyi sunuyor. Bu da beni şu soruya getiriyor: Veri akışlarımızı da bir Compose State Snapshot (Anlık Görüntü Durumu) tarafından yönlendirebilseydik ne olurdu?

  • Durum bizim veri modelimizse tüm tüketicileri için bir durumu nasıl hesaplar ve dönüştürürüz?

  • Yeni Jetpack AppState Transform kitaplığı bu noktada devreye girer.

  • Kütüphaneyi incelemeye başlamadan önce, kütüphanenin nasıl oluşturulduğundan bahsetmemiz gerekiyor.

  • Dolayısıyla, Headless oluşturmada durum değerleri üretmek için buradaki çalışma zamanı ve derleyici olan Compose motorundan yararlanma fikri yeni bir fikir değil.

  • Birçoğunuz Molecule kitaplığını zaten kullanıyor veya biliyor olabilirsiniz.

  • Burada "başsız" derken kullanıcı arayüzü olmadığını kastediyoruz.

  • Molecule, Compose Runtime'ın gücünü gösterme konusunda öncü oldu. Bunu, composable işlevlerin kullanıcı arayüzü düğümleri yayması gerekmediğini ve aslında saf bir veri modeli döndüren composable işlevler yazmak için kullanılabileceğini göstererek yaptı.

  • Molecule'de bu, Kotlin Flow veya StateFlow idi.

  • Bu sayede, combine gibi reaktif operatörleri zincirlemek yerine sıradan Kotlin kodları yazabiliyordunuz.

  • Ancak akışlarla uğraşmak yerine, kullanıcı arayüzünüz zaten Compose'da olduğu için veri modeliniz State olsaydı ne olurdu?

  • Transform ile veri modeliniz State olur.

  • Şimdi API'ye göz atalım.

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R>
  • API'nin tamamı bu kadardır.

  • Çok basit, tek bir işlevi var.

  • Flow veya StateFlow değil, düz bir gözlemlenebilir State döndürür.

  • Bu verileri kullananlar, verilerin bir dönüşüm tarafından üretildiğini bilmez veya önemsemez.

  • Burada asıl dikkat çekici olan bu blok.

  • Bu, Composable'dır.

  • Bu, bildiğiniz her şeyin bu araçta da geçerli olduğu anlamına gelir.

  • LaunchedEffect, collectAsState, erken dönüşler, basit if ifadeleri, Composable'ın içinden okuyabileceğiniz her şeyi bir Transform'dan türetebilirsiniz.

  • Bugün bu sunumda göreceğimiz tüm API'lerin alfa sürümünde olduğunu ve değişebileceğini belirtmek isteriz. Yine de bir örneğe göz atalım.

  • Çok basit ve küçük bir odaklanma zamanlayıcısı uygulaması ele alalım.

  • Bir görev seçip 25 dakikalık bir zamanlayıcı ayarlarsınız ve süre geri saymaya başlar.

  • Tek bir ekran oluşturduğunuz her seferde, bunun gibi basit bir tek ekran oluştururken bile, arka planda durumunuzu bir araya getirmek için birden fazla bağımsız veri akışını birleştirmeniz ve yönetmeniz gerekebilir.

  • Odaklanma ekranımız için iki tane olduğunu varsayalım: Odaklanma oturumunun kendisi, hangi aşamada olduğumuzu (duraklatılmış mı değil mi, kaç saniye kaldığı) gösterecek ve tüm bunlar, her saniye ilerleyecek bir StateFlow olarak zamanlayıcı motorundan gelecek.

  • Diğer veri akışı, kullanıcının üzerinde çalıştığı mevcut görev olacak ve Room veritabanından Flow olarak gelecek.

  • Yani sonuçta iki veri akışı ve bir ekran durumu olur.

  • ViewModel, bunların bir araya getirileceği yer olacak ve özet olarak adlandıracağımız çıkışı, bir sonraki adımda yazacağımız kod parçası olacak.

  • Geleneksel olarak, ViewModel içinde bu akışları zincirleme operatörler kullanarak birleştirmeniz gerekir.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: StateFlow<FocusSummary> =
            combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find { it.id == session.activeTaskId }
                    FocusSummary.Active(...)
                }
            }.stateIn(
                    scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = FocusSummary.Idle,
    )
    }
  • Bu nedenle, iki akışı birleştirip lambda içinde dallandırın. Yani, "if else" ifadesini buraya ekleyin ve sonuna bir durum ekleyin.

  • Transform ile bu ortak metin kodunun tamamını kaldırabiliriz.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
    initialValue = FocusSummary.Idle,
    scope = viewModelScope,
    container = handle.getOrCreateContainer("transform")
    ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
  • Bu çağrı, bir kapsamda iletilen bir başlangıç değerimiz olduğu için tanıdık geliyor ancak zincirleme yapılacak bir yukarı akış yok, türetme bloğun kendisi.

  • stateIn, yukarı akış akışlarını duraklatmak ve devam ettirmek için büyük olasılıkla bir WhileSubscribed zaman aşımı yapılandırmanız gerekir. Dönüşüm ise bu kapsamın kullanım ömrü boyunca sürekli olarak çalışır.

  • Kaynaklarımız akışlardır ve akışlar her zaman kabul edilir.

    class FocusScreenViewModel(
        focusManager: FocusSessionManager,
        taskRepository: TaskRepository
    ) : ViewModel() {
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
                initialValue = FocusSummary.Idle,
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val session by focusManager.sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                         FocusSummary.Active(...)
            }
    }
  • Her birini collectAsState ile bağlayacağız. Oturum StateFlow olduğu için başlangıç değerine ihtiyacımız yok ancak Room Flow soğuk olduğu için ona bir değer veriyoruz.

  • Abonelikle ilgili tüm bilgiler bu kadar.

  • Okuma, abone olmamızı sağlar.

  • Bu, Ben'in daha önce gördüğümüz örnekte bahsettiği otomatik okuma takibidir ancak ViewModel içinde yer alır.

  • Boşta olduğumuzda buradaki gibi erken dönüşler de ekleyebiliriz.

  • Yeni görevimiz yoksa geri kalanını işlemeyiz bile.

  • Dinamik okuma takibi burada da işe yarıyor.

  • Boşta olduğumuzda oda akışı hiç toplanmaz.

  • Son olarak özeti oluştururuz.

  • Yani kullanıcı görevi duraklatırsa veya zamanlayıcı çalışırsa her şey otomatik olarak güncellenir.

İç içe yerleştirilmiş dönüşümler

  • Tek bir ViewModel örneğinin dışına çıktığımızda işler biraz daha karmaşık hale gelir.

  • Bu oturumun iki yüzeyde daha gösterilmesi gerektiğini varsayalım: uygulamanın ekranının alt kısmında her ekranda bulunan mini zamanlayıcı çubuğu ve ana ekran widget'ı.

  • Her ikisi de oturum özetiyle ilgili aynı türetilmiş bilgilere ihtiyaç duyar: hangi aşama, duraklatıldı mı, zamanlayıcıda ne kadar süre kaldı?

  • Bu da her yüzey kendi kopyasını oluşturursa aynı türetmenin üç kez ve üç farklı yerde yapılması ve hepsinin birbiriyle uyumlu olması gerektiği anlamına gelir.

  • Bu nedenle, bunun yerine yapmamız gereken, özeti uygulama kapsamında bir kez türetmek ve ardından alt kısımdaki her şeyin bunu kullanmasını sağlamaktır.

  • Peki, böyle bir şeyi nasıl uygulayabiliriz?

  • Uygulamamızda muhtemelen üç parçaya ihtiyacımız olacak.

    class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
        ...
    }
    
    class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
        ...
    }
    
    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(context: Context, id: GlanceId) {
            provideContent {
                ...
            }
        }
    }
  • Bu nedenle, uygulama kapsamındaki bir oturum yöneticisi, özetin türetileceği yer olacak, mini zamanlayıcı ViewModel ve Glance Widget'ı, bu odak oturumu yöneticisinde türetilen özeti kullanacak.

  • Şimdi her birini önce geleneksel yöntemle, ardından Dönüştür ile oluşturalım.

  • Geleneksel olarak, uygulama kapsamında özeti bir kez StateFlow olarak türeterek başlarız.

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: StateFlow<FocusSummary> =
            combine(
                sessionFlow,
                taskRepository.tasksFlow
            ) { session, tasks ->
                if (session.phase == FocusPhase.Idle) {
                    FocusSummary.Idle
                } else {
                    val task = tasks.find {
                        it.id == session.activeTaskId
                    }
                    FocusSummary.Active(...)
                }
            }.stateIn(
    scope = appScope,
    started = SharingStarted.WhileSubscribed(5_000),
    initialValue = FocusSummary.Idle
    )
    }
  • Daha önce ViewModel örneğinde gördüğümüz combine ve stateIn çağrılarının aynısıdır ancak bir seviye yukarı taşınmıştır.

  • Dönüştürme ile daha önce gördüğümüz ViewModel öğesindeki aynı bildirimi de yeniden kullanabiliriz.

    class FocusSessionManager(
        taskRepository: TaskRepository,
        scope: CoroutineScope
    ) {
        val sessionFlow: StateFlow<FocusSession> = ...
        val summary: State<FocusSummary> =
            transform<FocusSummary>(
               initialValue = FocusSummary.Idle,
    scope = scope,
    container = handle.getOrCreateContainer("transform")
            ) {
                val session by sessionFlow.collectAsState()
                if (session.phase == FocusPhase.Idle)
                    return@transform FocusSummary.Idle
                val tasks by taskRepository.tasksFlow
                    .collectAsState(emptyList())
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
    }
  • Değişen tek şey, artık uygulama kapsamında olmasıdır.

  • Sırada mini zamanlayıcının ViewModel var.

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: StateFlow<MiniTimerUiState> =
            combine(
                focusManager.summary,
                isExpanded
            ) { summary, expanded ->
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = MiniTimerUiState()
    )
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • Burada da kendimizi biraz zorlamak istediğimizi varsayalım.

  • Mini zamanlayıcıda, halihazırda sahip olduğumuz iki veri akışına (StateFlow ve oda akışı) ek olarak zamanlayıcı çubuğunun genişletilip genişletilmediğini de izlemek istediğimizi varsayalım.

  • Bunun için MutableStateFlow kullanıyoruz.

  • Bunun için özet akışını ve isExpanded StateFlow öğesini izlemek üzere başka bir birleştirme işlemi kullanmamız gerekir.

  • İkinci bir durum elde ederiz.

  • Dönüştürme sayesinde, veri akışları arasında senkronizasyon sağlamak için bu zincirleme operatörlerden hiçbirini tekrar kullanmamız gerekmez.

    class MiniTimerViewModel(
        focusManager: FocusSessionManager
    ) : ViewModel() {
        val isExpanded = MutableStateFlow(false)
    
        val uiState: State<MiniTimerUiState> =
            transform(
                initialValue = MiniTimerUiState(),
                scope = viewModelScope,
                       container = handle.getOrCreateContainer("transform")
            ) {
                val summary by focusManager.summary
                       val expanded by isExpanded.collectAsState()
                MiniTimerUiState(
        summary = summary,
        showControls = expanded && summary is FocusSummary.Active
    )
            }
    
        fun toggleExpanded() { isExpanded.update { !it } }
    }
  • İç içe yerleştirilmiş dönüşümler yapabiliriz. Bu gerçekten harika bir özellik.

  • Burada ViewModel, diğer tüm durumlar gibi yukarı akış dönüşümünün çıkışı olan özeti okur ve ardından collectAsState ile isExpanded toplar.

  • Böylece, StateFlow içinde aynı blokta bir yukarı akış durumunu ve aşağı akış akışını karıştırabiliriz.

  • Ayrıntı düzeyini de unutmayın.

  • Yani isExpanded düğmesini açarsanız yalnızca bu blok yeniden çalıştırılır.

  • Uygulama düzeyindeki engelleme ve widget'ın bunu fark etmesi bile gerekmez.

  • Son olarak widget'ımız var.

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                    .collectAsState()
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • Glance, Compose olduğu için StateFlow değerini diğer Composable'lar gibi provideContent içinde collectAsState ile toplayabiliriz.

  • Dönüştürme özelliğiyle bu işlem daha da kolaylaşır.

    class FocusWidget : GlanceAppWidget() {
        override suspend fun provideGlance(
            context: Context,
            id: GlanceId
        ) {
            val focusManager = ...
            provideContent {
                val summary by focusManager.summary
                GlanceTheme { FocusWidgetContent(summary) }
            }
        }
    }
  • Odak yöneticimiz bize bir Compose State sağladığı için burada dönüştürmemiz gereken bir şey yok.

  • Sonuç olarak, mini zamanlayıcı çubuğu ve ana ekran widget'ı aynı durumu okuduğu için hiçbir zaman farklı bilgi göstermez.

  • Ayrıca bu durumda yapılan her değişiklik, bu durumun tüm tüketicilerine yansıtılır.

  • What does that mean?

  • Yani buradaki widget'ta duraklatma düğmesine basarsanız bu işlem yönetici aracılığıyla oturum akışına, ardından özete geri döner ve bu üç ekranın tümü senkronize olarak güncellenir.

  • Dönüştürme aracının nasıl kullanılacağına kısaca göz attığımıza göre, şimdi işin iç yüzünü daha ayrıntılı inceleyelim.

  • Daha önce gördüğümüz Transform API, aslında temel bir yönetilen Transform oluşturmak için kullanılan yardımcı bir işlevdir.

    public fun <R> transform(
        initialValue: R,
        scope: CoroutineScope,
        onUpdate: @Composable (R) -> R,
    ): State<R> {
            val transformScope = TransformScope()
        val transform = transformScope
    .launchTransform(initialValue, onUpdate)
        scope.launch(
    dispatcher,
    start = CoroutineStart.UNDISPATCHED
     ) {
            try { transformScope.run() } finally {
                transform.dispose()
            }
        }
        return transform
    }
  • Aslında TransformScope ve launchTransform uzantı işlevini kullanır. Bu işlev, bu TransformScope içinde bir Composable dönüşümü başlatır ve tek kullanımlık bir durum döndürür.

  • Yukarıdan aşağıya okuduğunuzda, her şeyi sizin için yaptığını göreceksiniz.

  • TransformScope oluşturur ve Dönüşüm'ünüzü bu ortamda başlatır. Bu ortam, geri döneceğiniz durum olacaktır.

  • Ardından, sağladığınız dağıtıcıda TransformScope içinde run işlevini başlatır. Böylece kapsamın kullanım ömrü ve iş parçacığı size ait olur.

  • Ardından, kapsam sona erdiğinde dönüşümünüzü siler.

  • Transform'un tüm yaşam döngüsü hikayesi bu şekildedir.

  • Şimdi TransformScope arayüzüne kısaca göz atalım.

    public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope
    
    public abstract class TransformScope {
            public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
    public abstract suspend fun run()
    }
    
    public fun <T> TransformScope.launchTransform(
    initialState: T,
    onUpdate: @Composable (T) -> T,
    ): DisposableState<T>
    
    public interface DisposableState<T> : State<T> {
    public fun dispose()
    }
  • Bu düğümün, TransformScope öğesine bağlı bir dönüşüm oluşturacak createTransform işlevine sahip olduğunu görebiliriz.

  • Ayrıca, TransformScope'yi başlatan ve mevcut durum dönüşümlerini güncelleme sürecini başlatan askıya alma çalıştırma işlevi de vardır.

  • What does that mean?

  • Yani run, aslında Recomposer'ı ve oluşturmayı ayarlar.

  • Kapsamdaki her Transform, bu oluşturma işleminin içinde yer alır ve kendi yeniden oluşturma kapsamına sahiptir.

  • Bu nedenle kapsamı paylaşmak istiyoruz ve TransformScope var.

  • Böylece bir durum değiştiğinde yalnızca bu durumu okuyan dönüşümler yeniden çalıştırılır.

  • Daha önce de belirttiğimiz gibi, run bir askıya alma işlevidir.

    override suspend fun run() {
            val context = currentCoroutineContext()
            val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
            ...
            val recomposer = Recomposer(context)
            val composition = Composition(UnitApplier, recomposer)
            try {
                withContext(clock) {
                    if (clock is HeadlessTransformClock) {
                        launch(start = CoroutineStart.UNDISPATCHED) {
                            clock.runClock()
                        }
                    }
                    ...
                }
            } finally {
                // dispose of composition, cancel the clock, close the recomposer
            }
        }
  • Bu nedenle, çağrıldığı eş yordam iptal edilene kadar o eş yordamda çalışır.

  • Şimdi, Recomposer'ın ne zaman kare üretmesi gerektiğini söylemek için bir kare saatine ihtiyacı olduğunu merak ediyor olabilirsiniz.

  • Compose kullanıcı arayüzünde bu, ekrandır ancak daha önce de belirttiğimiz gibi başsız bir ortamda olduğumuz için kullanıcı arayüzü yoktur.

  • Bu nedenle, bize bir saat sağlamazsanız kendi saatimizi oluştururuz.

  • Yeniden oluşturma işlemini ekran karesi saatinden ayırarak Transform, tüm dönüşümlerini ve durum türetmelerini kullanıcı arayüzü iş parçacığı dışında çalıştırır.

  • Bunu sağlamak için özel bir HeadlessTransformClock geliştirdik.

  • Çerçeve saatini dönüştürün.

  • Bir widget, hizmet veya başka bir Transform, Transform'dan gelecek bir değeri bekliyor olabileceğinden saatimiz isteklidir.

  • Bu, bir dönüşümün kullandığı herhangi bir durum değiştiği anda hemen yeni bir kare oluşturulduğu anlamına gelir.

  • Küçük ve kilitsiz bir durum makinesi olduğundan kare istemek basit bir karşılaştırma ve ayarlama işlemidir.

  • Durumda bir değişiklik olmadığında ise saat durur.

  • Bu nedenle, hiç değiştirilmemiş bir durumu kullanan bir Transform için herhangi bir ücret ödemezsiniz.

  • Sözü tekrar Ben'e vermeden önce, Transform'un artık rememberSaveable'ı desteklediğini ve böylece verilerinizin yapılandırma değişiklikleri boyunca kaydedildiğini de belirtelim.

  • Bu işlemi, Compose'un SaveableStateRegistry, SavedStateHandle ve SavedStateRegistry'sinin üzerine inşa edildiği yeni SavedStateContainer API aracılığıyla yapar.

  • Bunlardan herhangi birini SavedStateContainer'a dönüştürebilir veya alt kapsayıcılar oluşturup bunları Transform'unuza aktararak verilerinizin kaydedilmesini sağlayabilirsiniz.

  • Son olarak, bugün bahsettiğimiz tüm API'lerin alfa sürümünde olduğunu ve bu nedenle değişebileceğini tekrar hatırlatmak isteriz.

  • Şimdi sözü tekrar Ben'e bırakıyorum.

Kullanıcı arayüzü dışında Compose durumu

  • İşte böyle.

  • Tamamdır, teşekkürler Elif.

  • Oluşturma durumunun ne kadar güçlü olduğunu ve Transform'un bunu kullanıcı arayüzünün dışında türetmemize nasıl olanak tanıdığını gördük. Şimdiye kadarki örnekte fark etmiş olabileceğiniz bir şey, girişlerimizin collectAsState kullanarak köprü oluşturduğumuz akışlar olduğudur.

  • Ancak bu her zaman gerekli mi?

  • Neden doğrudan durum bilgisi depolayıcılarımızda Compose durumunu kullanmıyoruz?

  • Bu soruyu yanıtlamak için durumu üç geniş kategoriye ayırarak düşünmenin faydalı olacağını düşünüyorum.

  • Birincisi, yalnızca bellekte depolanan canlı durumdur.

  • Ağ veya Bluetooth bağlantı durumu gibi bilgileri düşünün.

  • Kalıcı bir değer kullanmak istemezsiniz.

  • İkincisi ise işlem sonlandırma sırasında korunması gereken ancak süresiz olarak saklanması gerekmeyen geçici durumdur.

  • Örneğin, bir kartın genişletilip genişletilmediği gibi kullanıcı arayüzü durumu ve gezinme eski yığını veya form taslağı gibi oturum durumu.

  • Üçüncüsü ise süresiz olarak saklanan kalıcı durumdur.

  • Kullanıcı tercihleri veya veritabanında bulunan bir medya kitaplığı gibi şeyleri düşünün.

  • Oluşturma durumunu hem canlı hem de geçici durum için nasıl kullanabileceğinizle başlayalım.

  • Şimdiye kadar mimari kılavuzumuzda, Compose durumunu kullanıcı arayüzü katmanında veya bazen ViewModel'lerde tutmanızı ve diğer her yerde Flow'u kullanmanızı öneriyorduk.

  • Peki, neden?

  • Bunun nedeni, bir durum özelliğini akışla birleştirmeniz gerektiğinde snapshotFlow kullanarak tekrar dönüştürmeniz gerekmesidir.

  • Ancak Transform ile bu dönüştürmeye gerek yoktur.

  • Şimdi, ViewModel itibarıyla bunun ne anlama geldiğine bakalım.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(
        private val tasksRepository: TasksRepository,
        private val networkMonitor: NetworkMonitor,
        savedStateHandle: SavedStateHandle,
    ) : ViewModel() {
        ...
    }
  • Buradaki selectedTaskIds gibi yerel ekran girişleri için MutableStateFlow yerine düz mutableStateOf özelliğini tanımlayabilirsiniz.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    }
  • Bu durum, işlem sonlandırma sırasında kalıcı olmasını istemediğiniz bir durum olduğundan yalnızca mutableStateOf kullanması sorun teşkil etmez.

  • Arama sorgusu gibi işlem sonlandırmadan etkilenmesini istemediğiniz öğelerin değişken durumunu doğrudan savedStateHandle.saveable ile destekleyebilirsiniz.

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    
        var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
            private set
    }
  • Daha önceki oyun örneğinde olduğu gibi, bir yöntem aynı anda birden fazla durum değerini güncellediğinde yazma işlemlerini withMutableSnapshot içine sarmak tutarlılık sağlar.updateScore

    @HiltViewModel
    class TaskListViewModel @Inject constructor(...) : ViewModel() {
        var selectedTaskIds by mutableStateOf(emptySet<String>())
            private set
    
        var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
            private set
    
        fun updateSearchQuery(query: String) {
            Snapshot.withMutableSnapshot {
                searchQuery = query
                selectedTaskIds = emptySet()
            }
        }
    }
  • Ardından, searchQuery ile bir Oda Akışı'nı birleştirmek için Akış'ı toplarken doğrudan Dönüşüm'ün içinde okursunuz.

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • Peki, ağ bağlantısı gibi canlı durumlar ne olacak?

    @Singleton
    class NetworkMonitor @Inject constructor(
        @ApplicationContext context: Context,
    ) {
        private val connectivityManager =
            context.getSystemService(ConnectivityManager::class.java)
        ...
    }
  • Bu genellikle bir Singleton'da bulunur. Bunun nedeni, birden fazla ekranda, hatta hizmetler gibi birden fazla uygulama bileşeni tarafından okunmasıdır.

  • Ancak bu, verileri depolamak için Compose State'i kullanamayacağınız anlamına gelmez.

  • Aslında genellikle akış tabanlı bir sürümden daha basittir.

  • Uyarlama yapmak için geri çağırma akışına veya başka bir şeye ihtiyacınız yoktur. Nokta değerini doğrudan geri çağırmalarda güncelleyebilirsiniz.

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    }
  • Tüketicinin hâlâ bir akışa ihtiyacı varsa geri köprü oluşturmak için snapshotFlow kullanabilirsiniz.

    @Singleton
    class NetworkMonitor @Inject constructor(...) {
        ...
        private val _isOnline = mutableStateOf(checkCurrentConnectivity())
        val isOnline: State<Boolean> = _isOnline
    
        private val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) { _isOnline.value = true }
            override fun onLost(network: Network) { _isOnline.value = false }
        }
    
        init {
            connectivityManager.registerDefaultNetworkCallback(callback)
        }
    }
  • Şimdi, ViewModel'da bunun nasıl göründüğüne bakalım.

  • isOnline değerini doğrudan bir Transform içinde okuyabilirsiniz. Değer değiştiğinde yeniden oluşturulur.

    val tasksState: State<TaskListState> = transform(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        if (!networkMonitor.isOnline.value) {
            return@transform TaskListState.Offline
        }
    
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading
    
        TaskListState.Success(tasks = tasks)
    }
  • Tüm bunlar doğrudan gerçekleşir. Durumu güncellemek için uygulama kapsamı veya eş yordam kapsamı gibi bir şey sağlamanız gerekmez. Doğrudan, eşzamanlı bir güncelleme yapılır.

  • Bu nedenle, Transform ile ViewModel, Singleton yöneticilerinden gelen State'i, bir ViewModel içindeki State'i ve Repositories'den gelen Flow'ları tek bir Transform bloğunda birleştirebilir.

  • Şimdi geçici duruma geri dönelim.

  • Daha önce iki türden bahsetmiştim: kullanıcı arayüzü durumu ve oturum durumu.

  • Kullanıcı arayüzü durumu ve hatta tek bir etkinlikte bulunan oturum durumu için ViewModel ve SavedState harika çalışır.

  • Bu araçlar tam da bu amaçla tasarlanmıştır.

  • Ancak bu oturum durumunun bir dizi farklı kullanıcı arayüzü ve uygulama bileşeni arasında paylaşılması gerektiğinde sorunlar ortaya çıkmaya başlar.

  • Bu durum ViewModel dışına çıktığında mevcut API'lerimiz ve kılavuzlarımız biraz yetersiz kalıyor.

  • SavedState API'leri önce bir etkinliğin paketine kaydeder ancak bu, hizmet gibi diğer uygulama bileşenlerinde kullanılamaz.

  • İkinci olarak, mutableStateOf yalnızca bellekte bulunan bir Singleton'da olduğundan işlem sonlandırmadan etkilenir.

StateStore ile tanışın

  • Jetpack AppState StateStore de bu boşluğu dolduruyor.

  • Bu, uygulama bileşenleri arasında paylaşılabilen ve işlem sonlandırmadan etkilenmemesi için DataStore tarafından desteklenen, Compose State'in tür güvenli bir eşlemesidir.

  • DataStore öğesini her Singleton'a kendiniz bağlayabilirsiniz ancak bu, her özellik için kaydetme ve geri yükleme standart kodunu yazmanız gerektiği anlamına gelir.

  • StateStore DataStore entegrasyonu sayesinde DataStore'i bir kez bağladıktan sonra kalıcı olmasını istediğiniz her bir durum parçasını etkinleştirebilirsiniz.

  • Şimdi bu özelliği odaklanma zamanlayıcısı uygulamamıza ekleyelim.

  • Önce bellek içi olarak başlayacağız, ardından DataStore kalıcılığı ekleyeceğiz.

  • Öncelikle, boşta, çalışıyor ve duraklatıldı durumlarına sahip kapalı bir arayüz olarak FocusSession modellemesi yapıyoruz.

    sealed interface FocusSession {
        data object Idle : FocusSession
        data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
  • Ardından, bu durumu StateStore içinde depolamak için türü FocusSession olan bir StateStoreKey ve varsayılan değer olarak "idle" (boşta) değerini tanımladık.

    sealed interface FocusSession { ... }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
  • Ardından, Hilt kitaplığında Singleton StateStore sağlarız. Bunu, Odak Oturumu Yöneticisi gibi ihtiyacımız olan farklı bileşenlere yerleştirebiliriz.

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(): StateStore = StateStore()
    }
    
    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        ...
    }
  • Bu, ViewModel'nin SavedStateHandle eklemesine benzer.

  • Anladım.

  • Artık bir StateStore var. Peki bunu nasıl kullanacağız?

  • Neyse ki küçük bir API.

  • Yalnızca üç yöntemi var. Bunları tek tek inceleyeceğiz.

  • Birincisi, bir anahtar için yeni bir değer yazan setState yöntemidir.

    fun startFocus(taskId: String, durationMinutes: Int = 25) {
        val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
        stateStore.setState(
            FocusSessionKey,
            FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
        )
    }
    
    fun endFocus() {
        stateStore.setState(FocusSessionKey, FocusSession.Idle)
    }
  • Bir oturum başladığında oturumu "çalışıyor", oturum sona erdiğinde ise "boşta" olarak ayarlarız.

  • Bu kadar basit.

  • İkinci olarak, mevcut değere göre yeni bir değer hesaplamanıza olanak tanıyan updateState var. Bu, çalışan ve duraklatılan durumlar arasında geçiş yaparken kalan süreyi hesaplamak gibi işlemler için kullanılabilir.

    fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Running) return@updateState session
            FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
        }
    }
    
    fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
        stateStore.updateState(FocusSessionKey) { session ->
            if (session !is FocusSession.Paused) return@updateState session
            FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
        }
    }
  • Son olarak, bir anahtar için salt okunur bir durum döndüren getState vardır. Bu, belirli bir çağrı sitesinde değiştirmek isterseniz varsayılan değeri geçersiz kılma seçeneğine sahiptir.

    @Singleton
    class FocusSessionManager @Inject constructor(
        private val stateStore: StateStore,
    ) {
        val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
        ...
    }
  • Ayrıca, bu yöneticide FocusSession durumunu göstermek, tüketicilerin StateStore veya anahtarlarına doğrudan erişmesi gerekmediği anlamına gelir.

  • Yazma ve okuma kapsüllemesini koruyabilirsiniz.

  • Ardından, tüketiciler yerine Focus Session Manager'ı eklerler. Oturumu kontrol etmeleri gerektiği için muhtemelen zaten eklemişlerdir. Sonra da State'i okuyabilirler.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() { ... }
    
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface WidgetEntryPoint {
        fun focusSessionManager(): FocusSessionManager
    }
  • Ardından bu okuyucular, State'i doğrudan yeniden gösterebilir veya daha önce Mini-Timer ViewModel örneğinde gördüğümüz gibi yeni ve güncellenmiş bir State oluşturmak için Transform'u kullanabilir.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
  • Ardından, kullanıcı arayüzü composable'ları bu durumu diğer tüm Compose durumları gibi okuyabilir ve gerektiğinde yeniden oluşturabilir.

    @HiltViewModel
    class MiniTimerViewModel @Inject constructor(
        focusSessionManager: FocusSessionManager,
    ) : ViewModel() {
        val session: State<FocusSession> = focusSessionManager.session
    }
    
    @Composable
    fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
        val session by viewModel.session
        if (session !is FocusSession.Idle) {
            TimerReadout(session = session)
        }
    }
  • Ancak FocusSession şu ana kadar yalnızca anılarda yaşadı.

  • Şimdi, işlem sonlandırmadan kurtulmak için ne yapılması gerektiğine bakalım.

  • Bu nedenle, mağazayı oluştururken syncToDataStore uygulamasını başlatarak tek seferlik bir kurulum yapmamız gerekir.

    @Module
    @InstallIn(SingletonComponent::class)
    object AppModule {
        @Provides
        @Singleton
        fun provideStateStore(
            @ApplicationContext context: Context,
            @ApplicationScope scope: CoroutineScope,
        ): StateStore = StateStore().apply {
            val path = context.dataStoreFile("app_state.json").absolutePath
            scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
        }
    }
  • Ardından, kalıcı olmasını istediğimiz her anahtarı etkinleştiririz.

    @Serializable
    sealed interface FocusSession {
        @Serializable data object Idle : FocusSession
        @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
        @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
    }
    
    object FocusSessionKey :
        StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
        PersistToDataStore
  • Tek yapmamız gereken, türün serileştirilebilir olduğundan ve anahtarın PersistToDataStore arayüzünü uyguladığından emin olmaktır.

  • "Tamam, bu kolay görünüyor." diye düşünebilirsiniz.

  • Peki, anahtarı kalıcı hale getirmek için aslında ne yapar?

  • Yani arka planda syncToDataStore, Transform'un dinleyici API'sini kullanır. Bu API, Transform ile tamamen aynıdır ancak hiçbir şey döndürmez, yalnızca gözlem ve yan etkiler için kullanılır.

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener { … }
    }
  • Ardından, bu dinleyicide mağazadaki her bir anahtarı okur ve bunlardan birinin PersistToDataStore anahtarı olup olmadığını kontrol eder.

    suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
        listener {
            for (key in keys) {
                if (key is PersistToDataStore) {
                    val value = getState(key).value
    
                    LaunchedEffect(key) {
                        // Restore saved value from dataStore on first access
                    }
    
                    LaunchedEffect(key, value) {
                        // Flush updated value to dataStore on change
                    }
                }
            }
        }
    }
  • Bu durumda, söz konusu değerler her değiştiğinde bunları DataStore ile senkronize etmek için LaunchedEffect kullanılır.

  • Burada belirtmek istediğim nokta, StateStore gibi standart bir kapsayıcıya sahip olmanın bu hazır entegrasyonları sunmamıza olanak tanımasıdır.

  • DataStore, inceleyeceğimiz ilk araç.

Compose State kategorilerinin özeti

  • Durum kategorilerimize geri dönersek artık Compose durumunu uygulamanızın tamamında nasıl kullanabileceğiniz konusunda oldukça net bir fikriniz var.

  • Canlı durum için mutableStateOf, ekrana yerel sunum durumu için SavedState API'leri, bileşenler arası oturum durumu için StateStore ve son olarak Flow'larla birlikte çalışabilirlik için collectAsState var.

  • Ancak birlikte çalışabilirlik tek bir kategoriyle sınırlı değildir. Gözlemlenebilir türleri Compose State'e bağlamanız gerektiğinde collectAsState gibi yardımcıları kullanabilirsiniz.

  • Ancak kalıcı durum için baktığımız bir diğer konu da Compose ile çalışmanın veya DataStore ile Room'un Compose durumunu doğrudan göstermesini sağlamanın yolları. Böylece, bunu yapmak için kendi söz diziminizi yazmanız gerekmez.

  • Bu doğrultuda, Transform, StateStore ve DataStore entegrasyonunun alfa sürümünün kullanıma sunulduğunu duyurmaktan mutluluk duyuyorum. Ayrıca, tüm belgeleri developer.android.com adresinde yayınladık ve GitHub'da hem StateStore hem de Transform'un kullanım örneklerini gösteren bir AppState Recipes depomuz var.

    [versions]
    appstate = "1.0.0-alpha01"
    ...
    
    [libraries]
    ...
    transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
    transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
    statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
    statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }
  • Bu özellikleri uygulamanızda denemenizi öneririz.

  • Geri bildirimde bulunabilirsiniz.

  • Şu anda alfa aşamasındayız. Bu aşamada, kullanım alanlarınıza uygun değişiklikler yapabiliriz.

Kod

Kod snippet'leri

"What is Compose State?" (Oluşturma Durumu Nedir?) videosunda 01:56'daki Kotlin snippet'i
/**
 * A value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value.
 */
@Stable
public interface State<out T> {
    public val value: T
}
"What is Compose State?" (Oluşturma Durumu Nedir?) videosunda 02:00'daki Kotlin snippet'i
/**
 * A mutable value holder where reads to the [value] property during the execution of a [Composable]
 * function, the current [RecomposeScope] will be subscribed to changes of that value. When the
 * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s
 * will be scheduled. If [value] is written to with the same value, no recompositions will be
 * scheduled.
 */
@Stable
public interface MutableState<T> : State<T> {
    override var value: T
    public operator fun component1(): T
    public operator fun component2(): (T) -> Unit
}
"What is Compose State?" videosunda 02:23'teki Kotlin snippet'i
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
}
"What is Compose State?" videosunda 02:32'deki Kotlin snippet'i
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}
"What is Compose State?" (Compose Durumu Nedir?) videosunda 02:52'deki Kotlin snippet'i
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    Column {
        Text("Count: ${count.value}")
        AddButton(onClick = { count.value++ })
    }
}

@Composable
fun AddButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Add")
    }
}
"What is Compose State?" (Compose State Nedir?) videosunda 03:08'deki Kotlin snippet'i
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(true) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
"What is Compose State?" (Compose State Nedir?) videosunda 03:26'daki Kotlin snippet'i
@Composable
fun Counter() {
    val isVisible = remember { mutableStateOf(false) }
    val count = remember { mutableStateOf(0) }
    Column {
        if (isVisible.value) {
            Text("Count: ${count.value}")
        } else {
            Text("Hidden")
        }
        AddButton(onClick = { count.value++ })
    }
}
"What is Compose State?" (Oluşturma Durumu Nedir?) videosunda 04:15'teki Kotlin snippet'i
val levelName: String = "Level 1 - Stay Alive!"
val timeSurvived: Int = 0
val score: Double = 0.0
"What is Compose State?" (Compose Durumu Nedir?) videosunda 04:23'teki Kotlin snippet'i
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)
"What is Compose State?" (Oluşturma Durumu Nedir?) videosunda 04:33'teki Kotlin snippet'i
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}
"What is Compose State?" (Compose Durumu Nedir?) videosunda 04:42'deki Kotlin snippet'i
data class GameUiState(
    val levelName: String = "Level 1 - Stay Alive!",
    val timeSurvived: Int = 0,
    val score: Double = 0.0
)

class GameViewModel : ViewModel() {
    val uiState = MutableStateFlow(GameUiState())
    fun updateScore() = uiState.update {
        it.copy(
            timeSurvived = it.timeSurvived + 1,
            score = it.score + 10 + .01 * it.timeSurvived
        )
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    Column {
        Header(levelName = state.levelName)
        ScoreBadge(
            timeSurvived = state.timeSurvived,
            score = state.score
        )
    }
}
"What is Compose State?" videosunda 05:09'daki Kotlin snippet'i
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}
"What is Compose State?" (Compose Durumu Nedir?) videosunda 05:21'deki Kotlin snippet'i
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}
"What is Compose State?" videosunda 05:30'daki Kotlin snippet'i
@Stable
class GameUiState {
    var levelName by mutableStateOf("Level 1 - Stay Alive!")
    var timeSurvived by mutableStateOf(0)
    var score by mutableStateOf(0.0)
}

class GameViewModel : ViewModel() {
    val uiState = GameUiState()
    fun updateScore() = Snapshot.withMutableSnapshot {
        uiState.timeSurvived += 1
        uiState.score += 10 + .01 * uiState.timeSurvived
    }
}

@Composable
fun GameScreen(viewModel: GameViewModel = viewModel()) {
    val state = viewModel.uiState
    Column {
        Header(state = state)
        ScoreBadge(state = state)
    }
}
"Introducing Transform" (Transform'a Giriş) videosunda 07:35'teki Kotlin snippet'i
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R>
"Introducing Transform" (Transform'a Giriş) videosunda 09:20'deki Kotlin snippet'i
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: StateFlow<FocusSummary> =
        combine(focusManager.sessionFlow, taskRepository.tasksFlow) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find { it.id == session.activeTaskId }
                FocusSummary.Active(...)
            }
        }.stateIn(
                scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = FocusSummary.Idle,
)
}
"Introducing Transform" (Transform'a Giriş) videosunda 09:37'deki Kotlin snippet'i
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
initialValue = FocusSummary.Idle,
scope = viewModelScope,
container = handle.getOrCreateContainer("transform")
) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle) return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find { it.id == session.activeTaskId }
            FocusSummary.Active(...)
"Introducing Transform" (Dönüşümle Tanışın) videosunda 10:01'deki Kotlin snippet'i
class FocusScreenViewModel(
    focusManager: FocusSessionManager,
    taskRepository: TaskRepository
) : ViewModel() {
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
            initialValue = FocusSummary.Idle,
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val session by focusManager.sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow.collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
                     FocusSummary.Active(...)
        }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 11:39'daki Kotlin snippet'i
class FocusSessionManager(taskRepository: TaskRepository, scope: CoroutineScope) {
    ...
}

class MiniTimerViewModel(focusManager: FocusSessionManager) : ViewModel() {
    ...
}

class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            ...
        }
    }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 12:03'teki Kotlin snippet'i
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: StateFlow<FocusSummary> =
        combine(
            sessionFlow,
            taskRepository.tasksFlow
        ) { session, tasks ->
            if (session.phase == FocusPhase.Idle) {
                FocusSummary.Idle
            } else {
                val task = tasks.find {
                    it.id == session.activeTaskId
                }
                FocusSummary.Active(...)
            }
        }.stateIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FocusSummary.Idle
)
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 12:17'deki Kotlin snippet'i
class FocusSessionManager(
    taskRepository: TaskRepository,
    scope: CoroutineScope
) {
    val sessionFlow: StateFlow<FocusSession> = ...
    val summary: State<FocusSummary> =
        transform<FocusSummary>(
           initialValue = FocusSummary.Idle,
scope = scope,
container = handle.getOrCreateContainer("transform")
        ) {
            val session by sessionFlow.collectAsState()
            if (session.phase == FocusPhase.Idle)
                return@transform FocusSummary.Idle
            val tasks by taskRepository.tasksFlow
                .collectAsState(emptyList())
            val task = tasks.find {
                it.id == session.activeTaskId
            }
            FocusSummary.Active(...)
        }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 12:28'deki Kotlin snippet'i
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: StateFlow<MiniTimerUiState> =
        combine(
            focusManager.summary,
            isExpanded
        ) { summary, expanded ->
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = MiniTimerUiState()
)

    fun toggleExpanded() { isExpanded.update { !it } }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 12:56'daki Kotlin snippet'i
class MiniTimerViewModel(
    focusManager: FocusSessionManager
) : ViewModel() {
    val isExpanded = MutableStateFlow(false)

    val uiState: State<MiniTimerUiState> =
        transform(
            initialValue = MiniTimerUiState(),
            scope = viewModelScope,
                   container = handle.getOrCreateContainer("transform")
        ) {
            val summary by focusManager.summary
                   val expanded by isExpanded.collectAsState()
            MiniTimerUiState(
    summary = summary,
    showControls = expanded && summary is FocusSummary.Active
)
        }

    fun toggleExpanded() { isExpanded.update { !it } }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 13:37'deki Kotlin snippet'i
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
                .collectAsState()
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 13:47'deki Kotlin snippet'i
class FocusWidget : GlanceAppWidget() {
    override suspend fun provideGlance(
        context: Context,
        id: GlanceId
    ) {
        val focusManager = ...
        provideContent {
            val summary by focusManager.summary
            GlanceTheme { FocusWidgetContent(summary) }
        }
    }
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 14:33'teki Kotlin snippet'i
public fun <R> transform(
    initialValue: R,
    scope: CoroutineScope,
    onUpdate: @Composable (R) -> R,
): State<R> {
        val transformScope = TransformScope()
    val transform = transformScope
.launchTransform(initialValue, onUpdate)
    scope.launch(
dispatcher,
start = CoroutineStart.UNDISPATCHED
 ) {
        try { transformScope.run() } finally {
            transform.dispose()
        }
    }
    return transform
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 15:18'deki Kotlin snippet'i
public fun TransformScope(context: CoroutineContext = Dispatchers.Default): TransformScope

public abstract class TransformScope {
        public fun <T> createTransform(initialState: T, onUpdate: @Composable (T) -> T): DisposableState<T>
public abstract suspend fun run()
}

public fun <T> TransformScope.launchTransform(
initialState: T,
onUpdate: @Composable (T) -> T,
): DisposableState<T>

public interface DisposableState<T> : State<T> {
public fun dispose()
}
"İç içe yerleştirilmiş dönüşümler" bölümünde 16:02'deki Kotlin snippet'i
override suspend fun run() {
        val context = currentCoroutineContext()
        val clock = context[MonotonicFrameClock] ?: HeadlessTransformClock()
        ...
        val recomposer = Recomposer(context)
        val composition = Composition(UnitApplier, recomposer)
        try {
            withContext(clock) {
                if (clock is HeadlessTransformClock) {
                    launch(start = CoroutineStart.UNDISPATCHED) {
                        clock.runClock()
                    }
                }
                ...
            }
        } finally {
            // dispose of composition, cancel the clock, close the recomposer
        }
    }
"Compose State outside of the UI" videosunda 19:12'deki Kotlin snippet'i
@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    private val networkMonitor: NetworkMonitor,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    ...
}
"Kullanıcı arayüzü dışında Compose durumu" videosunda 19:17'deki Kotlin snippet'i
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set
}
"Kullanıcı arayüzü dışında Compose durumu" videosunda 19:33'teki Kotlin snippet'i
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set
}
"Compose State outside of the UI" (Kullanıcı arayüzü dışında Compose durumu) videosunda 19:41'deki Kotlin snippet'i
@HiltViewModel
class TaskListViewModel @Inject constructor(...) : ViewModel() {
    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set

    fun updateSearchQuery(query: String) {
        Snapshot.withMutableSnapshot {
            searchQuery = query
            selectedTaskIds = emptySet()
        }
    }
}
"Compose State outside of the UI" videosunda 19:52'deki Kotlin snippet'i
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
"Compose State outside of the UI" videosunda 20:00'daki Kotlin snippet'i
@Singleton
class NetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)
    ...
}
"Compose State outside of the UI" videosunda 20:17'deki Kotlin snippet'i
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline
}
"Compose State outside of the UI" videosunda 20:23'teki Kotlin snippet'i
@Singleton
class NetworkMonitor @Inject constructor(...) {
    ...
    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { _isOnline.value = true }
        override fun onLost(network: Network) { _isOnline.value = false }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }
}
"Compose State outside of the UI" videosunda 20:32'deki Kotlin snippet'i
val tasksState: State<TaskListState> = transform(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    if (!networkMonitor.isOnline.value) {
        return@transform TaskListState.Offline
    }

    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(tasks = tasks)
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 22:07'deki Kotlin snippet'i
sealed interface FocusSession {
    data object Idle : FocusSession
    data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 22:14'teki Kotlin snippet'i
sealed interface FocusSession { ... }

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle)
"Introducing StateStore" (StateStore ile Tanışın) videosunda 22:23'teki Kotlin snippet'i
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(): StateStore = StateStore()
}

@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    ...
}
"Introducing StateStore" videosunda 22:42'deki Kotlin snippet'i
fun startFocus(taskId: String, durationMinutes: Int = 25) {
    val endTime = System.currentTimeMillis() + durationMinutes * 60_000L
    stateStore.setState(
        FocusSessionKey,
        FocusSession.Running(taskId = taskId, endTimeMillis = endTime),
    )
}

fun endFocus() {
    stateStore.setState(FocusSessionKey, FocusSession.Idle)
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 22:52'deki Kotlin snippet'i
fun pauseFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Running) return@updateState session
        FocusSession.Paused(session.taskId, session.endTimeMillis - nowMillis)
    }
}

fun resumeFocus(nowMillis: Long = System.currentTimeMillis()) {
    stateStore.updateState(FocusSessionKey) { session ->
        if (session !is FocusSession.Paused) return@updateState session
        FocusSession.Running(session.taskId, nowMillis + session.remainingMillis)
    }
}
"Introducing StateStore" (StateStore'a Giriş) videosunda 23:03'teki Kotlin snippet'i
@Singleton
class FocusSessionManager @Inject constructor(
    private val stateStore: StateStore,
) {
    val session: State<FocusSession> = stateStore.getState(FocusSessionKey)
    ...
}
"Introducing StateStore" (StateStore'a Giriş) videosunda 23:22'deki Kotlin snippet'i
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() { ... }

@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetEntryPoint {
    fun focusSessionManager(): FocusSessionManager
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 23:33'teki Kotlin snippet'i
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}
"Introducing StateStore" (StateStore'a Giriş) videosunda 23:45'teki Kotlin snippet'i
@HiltViewModel
class MiniTimerViewModel @Inject constructor(
    focusSessionManager: FocusSessionManager,
) : ViewModel() {
    val session: State<FocusSession> = focusSessionManager.session
}

@Composable
fun MiniTimerBar(viewModel: MiniTimerViewModel = hiltViewModel()) {
    val session by viewModel.session
    if (session !is FocusSession.Idle) {
        TimerReadout(session = session)
    }
}
"Introducing StateStore" (StateStore'a Giriş) videosunda 23:58'deki Kotlin snippet'i
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideStateStore(
        @ApplicationContext context: Context,
        @ApplicationScope scope: CoroutineScope,
    ): StateStore = StateStore().apply {
        val path = context.dataStoreFile("app_state.json").absolutePath
        scope.launch { syncToDataStore(path = path, coroutineScope = scope) }
    }
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 24:04'teki Kotlin snippet'i
@Serializable
sealed interface FocusSession {
    @Serializable data object Idle : FocusSession
    @Serializable data class Running(val taskId: String, val endTimeMillis: Long) : FocusSession
    @Serializable data class Paused(val taskId: String, val remainingMillis: Long) : FocusSession
}

object FocusSessionKey :
    StateStoreKey<FocusSession>(defaultValue = FocusSession.Idle),
    PersistToDataStore
"Introducing StateStore" (StateStore'a Giriş) videosunda 24:22'deki Kotlin snippet'i
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener { … }
}
"Introducing StateStore" (StateStore ile Tanışın) videosunda 24:33'teki Kotlin snippet'i
suspend fun StateStore.syncToDataStore(dataStore: DataStore<StateStorePreferences>) {
    listener {
        for (key in keys) {
            if (key is PersistToDataStore) {
                val value = getState(key).value

                LaunchedEffect(key) {
                    // Restore saved value from dataStore on first access
                }

                LaunchedEffect(key, value) {
                    // Flush updated value to dataStore on change
                }
            }
        }
    }
}
"Summary of Compose State categories" videosunda 25:39'daki TOML snippet'i
[versions]
appstate = "1.0.0-alpha01"
...

[libraries]
...
transform = { module = "androidx.appstate.transform:transform", version.ref = "appstate" }
transform-testing = { module = "androidx.appstate.transform:transform-testing", version.ref = "appstate" }
statestore = { module = "androidx.appstate.statestore:statestore", version.ref = "appstate" }
statestore-datastore = { module = "androidx.appstate.statestore:statestore-datastore", version.ref = "appstate" }