Kullanıcı Arayüzü Durumu üretimi

Modern kullanıcı arayüzleri nadiren statiktir. Kullanıcı, kullanıcı arayüzüyle etkileşimde bulunduğunda veya uygulamanın yeni veriler görüntülemesi gerektiğinde kullanıcı arayüzünün durumu değişir.

Bu belgede, kullanıcı arayüzü durumunun üretimi ve yönetimi ile ilgili yönergeler açıklanmaktadır. Etkinliğin sonunda:

  • Kullanıcı arayüzü durumu oluşturmak için hangi API'leri kullanmanız gerektiğini bilin. Bu, tek yönlü veri akışı ilkelerini izleyerek eyalet sahiplerinizde mevcut olan durum değişikliği kaynaklarının doğasına bağlıdır.
  • Sistem kaynaklarının bilincinde olmak için kullanıcı arayüzü durumunun üretim kapsamını nasıl belirlemeniz gerektiğini bilin.
  • Kullanıcı arayüzü durumunu, kullanıcı arayüzünün kullanımına nasıl sunmanız gerektiğini öğrenin.

Eyalet üretimi temelde bu değişikliklerin kullanıcı arayüzü durumuna kademeli olarak uygulanmasıdır. Durum her zaman mevcuttur ve etkinlikler sonucunda değişir. Olaylar ve durum arasındaki farklar aşağıdaki tabloda özetlenmiştir:

Etkinlikler Eyalet
Geçici, öngörülemez ve sınırlı bir süre boyunca devam eden Her zaman vardır.
Eyalet üretimi girdileri. Eyalet üretimi çıktısı.
Kullanıcı arayüzünün veya diğer kaynakların ürünü. Kullanıcı arayüzü tarafından tüketilir.

Yukarıdakileri özetleyen en iyi anımsatıcı durumdur; olaylar yaşanır. Aşağıdaki şema, bir zaman çizelgesinde etkinlikler meydana geldikçe durum değişikliklerinin görselleştirilmesine yardımcı olur. Her etkinlik, uygun eyalet sahibi tarafından işlenir ve bir durum değişikliğiyle sonuçlanır:

Etkinlikler ve eyalet
Şekil 1: Etkinlikler durumun değişmesine neden olur

Etkinlikler şuralardan gelebilir:

  • Kullanıcılar: Uygulamanın kullanıcı arayüzüyle etkileşimde bulundukları sırada.
  • Diğer durum değişikliği kaynakları: Sırasıyla atıştırmalık çubuğu zaman aşımı etkinlikleri, kullanım alanları veya depolar gibi kullanıcı arayüzünden, alandan veya veri katmanlarından uygulama verileri sunan API'ler.

Kullanıcı arayüzü durumu üretim ardışık düzeni

Android uygulamalarında durum üretimi, aşağıdakileri içeren bir işleme ardışık düzeni olarak düşünülebilir:

  • Girişler: Durum değişikliğinin kaynakları. Bunlar şunlar olabilir:
    • Kullanıcı arayüzü katmanında yerel: Bunlar, bir kullanıcının görev yönetimi uygulamasında "yapılacaklar" için bir başlık girmesi gibi kullanıcı etkinlikleri veya kullanıcı arayüzü durumunda değişikliklere yol açan kullanıcı arayüzü mantığına erişim sağlayan API'ler olabilir. Örneğin, Jetpack Compose'da DrawerState için open yöntemini çağırabilirsiniz.
    • Kullanıcı arayüzü katmanının dışında: Bunlar, kullanıcı arayüzü durumunda değişikliklere neden olan, alan veya veri katmanlarından gelen kaynaklardır. Örneğin, bir NewsRepository kaynağından yükleme işlemi biten haberler veya diğer etkinlikler.
    • Yukarıdakilerin bir karışımı.
  • Durum sahipleri: Durum değişikliği kaynaklarına iş mantığı ve/veya kullanıcı arayüzü mantığı uygulayan türler ve kullanıcı arayüzü durumu oluşturmak için kullanıcı etkinliklerini işler.
  • Çıkış: Kullanıcılara ihtiyaç duydukları bilgileri sağlamak için uygulamanın oluşturabileceği kullanıcı arayüzü durumudur.
Durum üretim hattı
Şekil 2: Durum üretim hattı

Durum üretim API'leri

Ardışık düzenin hangi aşamasında olduğunuza bağlı olarak durum üretiminde kullanılan iki ana API vardır:

Ardışık düzen aşaması API
Giriş Kullanıcı arayüzü sorunlarını gidermek amacıyla kullanıcı arayüzü iş parçacığı üzerinde çalışmak için eşzamansız API'leri kullanmanız gerekir. Örneğin, Kotlin ve RxJava'daki eş yordamlar veya akışlar ya da Java Programlama Dili'ndeki geri çağırmalar.
Çıkış Durum değiştiğinde kullanıcı arayüzünü geçersiz kılmak ve yeniden oluşturmak için gözlemlenebilir veri sahibi API'lerini kullanmanız gerekir. Örneğin StateFlow, Compose State veya LiveData. Gözlemlenebilir veri sahipleri, kullanıcı arayüzünün her zaman ekranda gösterilecek bir kullanıcı arayüzü durumunun olmasını garanti eder

Bu iki seçenek arasından, giriş için eşzamansız API'nin seçilmesinin durum üretim hattının yapısı üzerinde, çıkış için gözlemlenebilir API seçiminden daha fazla etkisi vardır. Bunun nedeni, girişlerin ardışık düzene uygulanabilecek işleme türünü belirlemesidir.

Devlet üretim hattı derlemesi

Sonraki bölümlerde, çeşitli girişler için en uygun durum üretim teknikleri ve eşleşen çıkış API'leri ele alınmaktadır. Her durum üretim ardışık düzeni, giriş ve çıkışların bir kombinasyonudur ve:

  • Yaşam döngüsüne duyarlı: Kullanıcı arayüzünün görünür veya etkin olmadığı durumlarda, durum üretim ardışık düzeni açıkça gerekli olmadığı sürece hiçbir kaynağı tüketmemelidir.
  • Kullanımı kolay: Kullanıcı arayüzünün, oluşturulan kullanıcı arayüzü durumunu kolayca oluşturabilmesi gerekir. Durum üretim ardışık düzeni çıkışı için dikkat edilmesi gereken noktalar, View sistemi veya Jetpack Compose gibi farklı View API'leri arasında farklılık gösterir.

Durum üretim ardışık düzenlerindeki girişler

Durum üretim ardışık düzenindeki girişler, durum değişikliği kaynaklarını şunlar aracılığıyla sağlayabilir:

  • Eşzamanlı veya eşzamansız olabilen tek seferlik işlemler (ör. suspend işlevlerine yapılan çağrılar).
  • Akış API'leri (örneğin, Flows).
  • Yukarıdakilerin tümü

Aşağıdaki bölümlerde, yukarıdaki girişlerin her biri için nasıl durum üretim ardışık düzeni derleyebileceğiniz ele alınmaktadır.

Durum değişikliği kaynakları olarak tek seferlik API'ler

MutableStateFlow API'yi gözlemlenebilir ve değişebilir bir durum kapsayıcısı olarak kullanın. Jetpack Compose uygulamalarında, özellikle Compose metin API'leriyle çalışırken mutableStateOf'i de kullanabilirsiniz. Her iki API de güncellemelerin eşzamanlı veya eşzamansız olup olmamasına bakılmaksızın, barındırdıkları değerlerde güvenli atom güncellemelerine olanak tanıyan yöntemler sunar.

Örneğin, basit bir zar atma uygulamasındaki durum güncellemelerini düşünün. Kullanıcıdan gelen her zar eşzamanlı Random.nextInt() yöntemini çağırır ve sonuç kullanıcı arayüzü durumuna yazılır.

Durum Akışı

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Called from the UI
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
            firstDieValue = Random.nextInt(from = 1, until = 7),
            secondDieValue = Random.nextInt(from = 1, until = 7),
            numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Oluşturma Durumu

@Stable
interface DiceUiState {
    val firstDieValue: Int?
    val secondDieValue: Int?
    val numberOfRolls: Int?
}

private class MutableDiceUiState: DiceUiState {
    override var firstDieValue: Int? by mutableStateOf(null)
    override var secondDieValue: Int? by mutableStateOf(null)
    override var numberOfRolls: Int by mutableStateOf(0)
}

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

    // Called from the UI
    fun rollDice() {
        _uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.numberOfRolls = _uiState.numberOfRolls + 1
    }
}

Kullanıcı arayüzü durumunu eşzamansız çağrılardan değiştirme

eşzamansız sonuç gerektiren durum değişiklikleri için uygun CoroutineScope içinde bir eş yordam başlatın. Bu izin, CoroutineScope iptal edildiğinde uygulamanın çalışmayı silmesine olanak tanır. Durum sahibi daha sonra askıya alma yöntemi çağrısının sonucunu, kullanıcı arayüzü durumunu göstermek için kullanılan gözlemlenebilir API'ye yazar.

Örneğin, Mimari örneğindeki AddEditTaskViewModel öğesini göz önünde bulundurun. saveTask() yönteminin askıya alınması bir görevi eşzamansız olarak kaydettiğinde MutableStateFlow'daki update yöntemi durum değişikliğini kullanıcı arayüzü durumuna yayar.

Durum Akışı

data class AddEditTaskUiState(
    val title: String = "",
    val description: String = "",
    val isTaskCompleted: Boolean = false,
    val isLoading: Boolean = false,
    val userMessage: String? = null,
    val isTaskSaved: Boolean = false
)

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableStateFlow(AddEditTaskUiState())
   val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.update {
                    it.copy(isTaskSaved = true)
                }
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.update {
                    it.copy(userMessage = getErrorMessage(exception))
                }
            }
        }
    }
}

Oluşturma Durumu

@Stable
interface AddEditTaskUiState {
    val title: String
    val description: String
    val isTaskCompleted: Boolean
    val isLoading: Boolean
    val userMessage: String?
    val isTaskSaved: Boolean
}

private class MutableAddEditTaskUiState : AddEditTaskUiState() {
    override var title: String by mutableStateOf("")
    override var description: String by mutableStateOf("")
    override var isTaskCompleted: Boolean by mutableStateOf(false)
    override var isLoading: Boolean by mutableStateOf(false)
    override var userMessage: String? by mutableStateOf<String?>(null)
    override var isTaskSaved: Boolean by mutableStateOf(false)
}

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableAddEditTaskUiState()
   val uiState: AddEditTaskUiState = _uiState

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.isTaskSaved = true
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.userMessage = getErrorMessage(exception))
            }
        }
    }
}

Arka plandaki iş parçacıklarından kullanıcı arayüzü durumunu değiştirme

Kullanıcı arayüzü durumunun üretimi için ana görev dağıtıcıda eş yordamların başlatılması tercih edilir. Yani aşağıdaki kod snippet'lerinde withContext bloğunun dışındadır. Ancak kullanıcı arayüzü durumunu farklı bir arka plan bağlamında güncellemeniz gerekiyorsa bunu aşağıdaki API'leri kullanarak yapabilirsiniz:

  • Eş yordamları farklı bir eşzamanlı bağlamda çalıştırmak için withContext yöntemini kullanın.
  • MutableStateFlow kullanırken her zamanki gibi update yöntemini kullanın.
  • Oluşturma Durumu'nu kullanırken, eşzamanlı bağlamda Durum'a atomik güncellemeleri garanti etmek için Snapshot.withMutableSnapshot özelliğini kullanın.

Örneğin, aşağıdaki DiceRollViewModel snippet'inde SlowRandom.nextInt() öğesinin CPU'ya bağlı bir Coroutine'den çağrılması gereken işlem açısından yoğun bir suspend işlevi olduğunu varsayalım.

Durum Akışı

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            …
            withContext(defaultDispatcher) {
                _uiState.update { currentState ->
                    currentState.copy(
                        firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        numberOfRolls = currentState.numberOfRolls + 1,
                    )
                }
            }
        }
    }
}

Oluşturma Durumu

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            …
            withContext(defaultDispatcher) {
                Snapshot.withMutableSnapshot {
                    _uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.numberOfRolls = _uiState.numberOfRolls + 1
                }
            }
        }
    }
}

Durum değişikliği kaynakları olarak akış API'leri

Akışlarda zaman içinde birden fazla değer üreten durum değişikliği kaynakları için tüm kaynakların çıktılarının tutarlı bir bütün halinde toplanması, durum üretimine doğrudan bir yaklaşımdır.

Kotlin Flows'u kullanırken bu amaca ulaşmak için combine işlevini kullanabilirsiniz. Bunun bir örneği, InterestsViewModel'deki "Now Android'de" örneğinde görülebilir:

class InterestsViewModel(
    authorsRepository: AuthorsRepository,
    topicsRepository: TopicsRepository
) : ViewModel() {

    val uiState = combine(
        authorsRepository.getAuthorsStream(),
        topicsRepository.getTopicsStream(),
    ) { availableAuthors, availableTopics ->
        InterestsUiState.Interests(
            authors = availableAuthors,
            topics = availableTopics
        )
    }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = InterestsUiState.Loading
    )
}

StateFlows oluşturmak için stateIn operatörünün kullanılması, yalnızca kullanıcı arayüzü görünür olduğunda etkin olması gerekebileceği için kullanıcı arayüzüne durum üretim hattının etkinliği üzerinde daha ayrıntılı bir kontrol sağlar.

  • Ardışık düzenin yalnızca, akışı yaşam döngüsüne duyarlı bir şekilde toplarken kullanıcı arayüzü görünür olduğunda etkin olması gerekiyorsa SharingStarted.WhileSubscribed() kullanın.
  • Ardışık düzenin, kullanıcı kullanıcı arayüzüne dönebileceği, yani kullanıcı arayüzü arka yığında veya ekran dışında başka bir sekmede olduğu sürece etkin olması gerekiyorsa SharingStarted.Lazily kullanın.

Akış tabanlı durum kaynaklarını birleştirmenin geçerli olmadığı durumlarda, Kotlin Flows gibi akış API'leri, akışların kullanıcı arayüzü durumuna işlenmesine yardımcı olmak için birleştirme, birleştirme gibi zengin bir dönüşüm grubu sunar.

Durum değişikliği kaynakları olarak tek seferlik ve akış API'leri

Durum üretim ardışık düzeninin durum değişikliği kaynağı olarak hem tek seferlik çağrılara hem de akışlara bağlı olduğu durumlarda belirleyici kısıtlama akışlardır. Bu nedenle, tek seferlik çağrıları akış API'lerine dönüştürün veya bunların çıkışını akışlara aktarıp işlemeye yukarıdaki akışlar bölümünde açıklandığı gibi devam edin.

Akışlar söz konusu olduğunda genellikle durum değişikliklerini yaymak için bir veya daha fazla gizli yedek MutableStateFlow örneği oluşturmanız gerekir. Oluştur durumundan anlık görüntü akışları da oluşturabilirsiniz.

Aşağıdaki architecture-samples deposundaki TaskDetailViewModel öğesini düşünün:

Durum Akışı

class TaskDetailViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _isTaskDeleted = MutableStateFlow(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        _isTaskDeleted,
        _task
    ) { isTaskDeleted, task ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted.update { true }
    }
}

Oluşturma Durumu

class TaskDetailViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private var _isTaskDeleted by mutableStateOf(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        snapshotFlow { _isTaskDeleted },
        _task
    ) { isTaskDeleted, task ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted = true
    }
}

Durum üretim ardışık düzenlerindeki çıkış türleri

Kullanıcı arayüzü durumu için çıkış API'sinin seçimi ve sunumunun yapısı büyük ölçüde uygulamanızın kullanıcı arayüzünü oluşturmak için kullandığı API'ye bağlıdır. Android uygulamalarında, Görünümler veya Jetpack Compose'u kullanmayı seçebilirsiniz. Bu aşamada göz önünde bulundurulması gereken noktalar:

Aşağıdaki tabloda, herhangi bir giriş ve tüketici için durum üretim ardışık düzeniniz için hangi API'lerin kullanılacağı özetlenmektedir:

Giriş Tüketici Çıkış
Tek seferlik API'ler Görüntüleme sayısı StateFlow veya LiveData
Tek seferlik API'ler Oluştur StateFlow veya Oluştur State
Akış API'leri Görüntüleme sayısı StateFlow veya LiveData
Akış API'leri Oluştur StateFlow
Tek seferlik ve akış API'leri Görüntüleme sayısı StateFlow veya LiveData
Tek seferlik ve akış API'leri Oluştur StateFlow

Durum üretim ardışık düzeni başlatma

Durum üretim ardışık düzenlerinin başlatılması, ardışık düzenin çalışması için ilk koşulların ayarlanmasını içerir. Bu, ardışık düzenin başlatılması için kritik öneme sahip ilk giriş değerlerinin sağlanmasını (örneğin, bir haber makalesinin ayrıntılı görünümü için id) veya eşzamansız yük başlatmayı içerebilir.

Sistem kaynaklarını korumak için mümkün olduğunda durum üretim ardışık düzenini gecikmeli olarak başlatmanız gerekir. Pratikte bunun için genellikle sonuç tüketicisi olana kadar beklemek gerekir. Flow API'leri buna stateIn yöntemindeki started bağımsız değişkeniyle izin verir. Bunun geçerli olmadığı durumlarda, aşağıdaki snippet'te gösterildiği gibi durum üretim hattını açıkça başlatmak için bir idempotent initialize() işlevi tanımlayın:

class MyViewModel : ViewModel() {

    private var initializeCalled = false

    // This function is idempotent provided it is only called from the UI thread.
    @MainThread
    fun initialize() {
        if(initializeCalled) return
        initializeCalled = true

        viewModelScope.launch {
            // seed the state production pipeline
        }
    }
}

Sana Özel

Aşağıdaki Google örnekleri, kullanıcı arayüzü katmanında durum üretimini göstermektedir. Bu kılavuzu inceleyerek örnekleri inceleyin: