Kullanıcı Arayüzü Durumu üretimi

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

Bu belgede, kullanıcı arayüzü durumunun üretimi ve yönetimiyle ilgili yönergeler açıklanmaktadır. Bu sürenin sonunda yapmanız gerekenler:

  • Kullanıcı arayüzü durumu oluşturmak için hangi API'leri kullanmanız gerektiğini bilin. Bu, tek yönlü veri akışı ilkeleri uyarınca, durum tutucularınızda bulunan durum değişikliği kaynaklarının yapısına bağlıdır.
  • Sistem kaynaklarını göz önünde bulundurmak için kullanıcı arayüzü durumunun üretimini nasıl kapsamlandırmanız gerektiğini bilin.
  • Kullanıcı arayüzü durumunu, kullanıcı arayüzü tarafından kullanılmak üzere nasıl kullanıma sunmanız gerektiğini bilin.

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

Etkinlikler Eyalet
Geçici, tahmin edilemez ve sınırlı bir süre için geçerlidir. Her zaman vardır.
Devlet üretiminin girdileri. Devlet üretiminin çıktısı.
Kullanıcı arayüzü veya diğer kaynakların ürünü Kullanıcı arayüzü tarafından tüketilir.

Yukarıdakileri özetleyen harika bir anımsatıcı: Durum vardır; olaylar gerçekleşir. Aşağıdaki diyagram, zaman çizelgesinde etkinlikler gerçekleşirken durumdaki değişiklikleri görselleştirmeye yardımcı olur. Her etkinlik, uygun durum bilgisi depolayıcı tarafından işlenir ve durum değişikliğiyle sonuçlanır:

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

Etkinlikler şu kaynaklardan gelebilir:

  • Kullanıcılar: Uygulamanın kullanıcı arayüzüyle etkileşim kurarken.
  • Durum değişikliğinin diğer kaynakları: Sırasıyla kullanıcı arayüzü, alan veya veri katmanlarından (ör. snackbar zaman aşımı etkinlikleri, kullanım alanları ya da depolar) uygulama verilerini sunan API'ler.

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

Android uygulamalarındaki durum üretimi, aşağıdakilerden oluşan bir işleme hattı olarak düşünülebilir:

  • Girişler: Durum değişikliğinin kaynakları. Bunlar:
    • Kullanıcı arayüzü katmanına özgü: Bunlar, bir görev yönetimi uygulamasında "yapılacaklar" için başlık girme gibi kullanıcı etkinlikleri veya kullanıcı arayüzü durumundaki değişiklikleri yönlendiren kullanıcı arayüzü mantığına erişim sağlayan API'ler olabilir. Örneğin, Jetpack Compose'da DrawerState üzerinde open yöntemini çağırma.
    • Kullanıcı arayüzü katmanının dışında: Bunlar, alan veya veri katmanlarından gelen ve kullanıcı arayüzü durumunda değişikliklere neden olan kaynaklardır. Örneğin, NewsRepository veya diğer etkinliklerden yüklenen haberler.
    • Yukarıdakilerin tümünün bir karışımı.
  • Durum tutucular: Durum değişikliği kaynaklarına iş mantığı ve/veya kullanıcı arayüzü mantığı uygulayan ve kullanıcı arayüzü durumu oluşturmak için kullanıcı etkinliklerini işleyen türler.
  • Çıkış: Uygulamanın, kullanıcılara ihtiyaç duydukları bilgileri sağlamak için oluşturabileceği kullanıcı arayüzü durumu.
Durum prodüksiyon ardışık düzeni
Şekil 2: Durum ardışık düzeni

Üretim API'lerini kullanma

İşlem hattının hangi aşamasında olduğunuza bağlı olarak durum oluşturmada kullanılan iki temel API vardır:

Ardışık düzen aşaması API
Giriş Kullanıcı arayüzünün duraklamasını önlemek için kullanıcı arayüzü iş parçacığı dışında çalışmak üzere eşzamansız API'ler kullanmanız gerekir. Örneğin, Kotlin'de eş yordamlar veya akışlar, Java programlama dilinde ise RxJava veya 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 tutucu API'lerini kullanmanız gerekir. Örneğin, StateFlow, Compose State veya LiveData. Gözlemlenebilir veri tutucular, kullanıcı arayüzünün ekranda her zaman gösterebileceği bir kullanıcı arayüzü durumuna sahip olmasını sağlar.

İkisi arasında, giriş için asenkron API seçimi, çıkış için gözlemlenebilir API seçimine kıyasla durum üretim hattının doğası üzerinde daha büyük bir etkiye sahiptir. Bunun nedeni, girişlerin işlem hattına uygulanabilecek işlem türünü belirlemesidir.

Durum bilgili üretim ardışık düzeni montajı

Sonraki bölümlerde, çeşitli girişlere en uygun durum üretimi teknikleri ve bunlara karşılık gelen çıkış API'leri ele alınmaktadır. Her durum üretim hattı, giriş ve çıkışların bir kombinasyonudur ve şu özelliklere sahip olmalıdır:

  • Yaşam döngüsüne duyarlı: Kullanıcı arayüzünün görünür veya etkin olmadığı durumlarda, durum üretim hattı açıkça gerekmedikçe herhangi bir kaynak tüketmemelidir.
  • Kolay anlaşılırlık: Kullanıcı arayüzü, oluşturulan kullanıcı arayüzü durumunu kolayca oluşturabilmelidir. Durum üretim ardışık düzeninin çıkışıyla ilgili hususlar, View sistemi veya Jetpack Compose gibi farklı View API'lerinde değişiklik gösterir.

Durum üretim işlem hatlarındaki girişler

Bir durum üretim hattındaki girişler, durum değişikliği kaynaklarını şu yollarla sağlayabilir:

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

Aşağıdaki bölümlerde, yukarıdaki girişlerin her biri için nasıl bir durum üretim ardışık düzeni oluşturabileceğiniz açıklanmaktadır.

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

MutableStateFlow API'yi, gözlemlenebilir ve değiştirilebilir bir durum kapsayıcısı olarak kullanın. Jetpack Compose uygulamalarında, özellikle Compose metin API'leriyle çalışırken mutableStateOf öğesini de kullanabilirsiniz. Her iki API de, güncellemeler eşzamanlı veya eşzamansız olsun olmasın, barındırdıkları değerlerde güvenli atomik güncellemeler yapılmasına olanak tanıyan yöntemler sunar.

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

StateFlow

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
    }
}

Eşzamansız aramalardan kullanıcı arayüzü durumunu değiştirme

Asenkron sonuç gerektiren durum değişiklikleri için uygun CoroutineScope içinde bir Coroutine başlatın. Bu, uygulamanın CoroutineScope iptal edildiğinde çalışmayı silmesine olanak tanır. Durum bilgisi depolayıcı 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 simgesini ele alalım. Askıya alma saveTask() yöntemi 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.

StateFlow

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))
            }
        }
    }
}

Kullanıcı arayüzü durumunu arka plan iş parçacıklarından değiştirme

Kullanıcı arayüzü durumu oluştururken eş yordamları ana dağıtıcıda başlatmak tercih edilir. Yani aşağıdaki kod snippet'lerindeki withContext bloğunun dışında. Ancak kullanıcı arayüzü durumunu farklı bir arka plan bağlamında güncellemeniz gerekiyorsa aşağıdaki API'leri kullanarak bunu yapabilirsiniz:

  • Farklı bir eşzamanlı bağlamda Coroutine'leri çalıştırmak için withContext yöntemini kullanın.
  • MutableStateFlow kullanırken update yöntemini her zamanki gibi kullanın.
  • Compose State'i kullanırken eşzamanlı bağlamda State'in atomik güncellemelerini garanti etmek için Snapshot.withMutableSnapshot kullanın.

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

StateFlow

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 çıkışlarını tutarlı bir bütün hâlinde toplamak, durum üretimi için basit bir yaklaşımdır.

Kotlin Flow'ları kullanırken bu işlevi combine işleviyle gerçekleştirebilirsiniz. Bunun bir örneğini InterestsViewModel'daki "Now in Android" örneğinde görebilirsiniz:

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
    )
}

stateIn operatörünün StateFlows oluşturmak için kullanılması, kullanıcı arayüzü görünür olduğunda etkin olması gerekebileceğinden durum üretimi ardışık düzeninin etkinliği üzerinde daha ayrıntılı kontrol sağlar.

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

Durumun akış tabanlı kaynaklarının toplandığı durumların 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 ve düzleş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 hem tek seferlik çağrılara hem de akışlara durum değişikliği kaynağı olarak bağlı olduğu durumlarda, akışlar belirleyici kısıtlamadır. Bu nedenle, tek seferlik çağrıları akış API'lerine dönüştürün veya çıkışlarını akışlara yönlendirip yukarıdaki akışlar bölümünde açıklandığı gibi işlemeye devam edin.

Akışlarda bu genellikle durum değişikliklerini yaymak için bir veya daha fazla özel yedekleme MutableStateFlow örneği oluşturmak anlamına gelir. Ayrıca, Compose durumundan anlık görüntü akışları da oluşturabilirsiniz.

Aşağıdaki architecture-samples deposundaki TaskDetailViewModel örneğini inceleyin:

StateFlow

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
    }
}

Duruma dayalı üretim işlem hatlarındaki çıkış türleri

Kullanıcı arayüzü durumu için çıkış API'sinin seçimi ve sunum şekli 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'i veya Jetpack Compose'u kullanmayı seçebilirsiniz. Bu noktada dikkat edilmesi gerekenler:

Aşağıdaki tabloda, belirli bir giriş ve tüketici için durum üretim hattınızda hangi API'lerin kullanılacağı özetlenmiştir:

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'u State tıklayın.
Stream API'leri Görüntüleme sayısı StateFlow veya LiveData
Stream 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

Üretim ardışık düzeni başlatma durumu

Durum üretim ardışık düzenlerini başlatmak için ardışık düzenin çalıştırılacağı başlangıç koşulları ayarlanır. Bu, işlem hattının başlatılması için kritik olan ilk giriş değerlerinin sağlanmasını (ör. bir haber makalesinin ayrıntılı görünümü için id) veya eşzamansız yüklemenin başlatılmasını içerebilir.

Sistem kaynaklarını korumak için mümkün olduğunda durum üretim hattını geç başlatmalısınız. Pratikte bu genellikle çıktının tüketicisi olana kadar beklemek anlamına gelir. Flow API'leri, stateIn yöntemindeki started bağımsız değişkeniyle buna olanak tanır. Bu durumun geçerli olmadığı durumlarda, aşağıdaki snippet'te gösterildiği gibi durum oluşturma ardışık düzenini açıkça başlatmak için idempotent initialize() işlevini 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
        }
    }
}
ViewModel

Örnekler

Aşağıdaki Google örnekleri, kullanıcı arayüzü katmanında durum oluşturmayı gösterir. Bu kılavuzun nasıl uygulandığını görmek için aşağıdaki kaynakları inceleyin: