Skip to main content Link Menu Expand (external link) Document Search Copy Copied

State production

UI State Production

Modern UI ler nadiren statiktir. Kullanıcı, UI ile etkileşime girdiğinde veya uygulamanın yeni verileri göstermesi gerektiğinde UI’in state’i değişir. Bu belge, UI state’inin üretimi(production) ve yönetimi için yönergeler belirler. Sonunda şunları yapmalısınız:

  • UI state’i oluşturmak(produce) için hangi API’leri kullanmanız gerektiğini bilin. Bu, tek yönlü veri akışı(UDF) ilkelerini izleyerek state holderlerinizde bulunan state değişikliği kaynaklarının doğasına bağlıdır.
  • Sistem kaynaklarının bilincinde olmak için UI state’inin üretimini nasıl kapsamanız(scope) gerektiğini öğrenin.
  • UI tarafından tüketim için UI stateini nasıl ortaya çıkarmanız gerektiğini bilin.

Temel olarak state üretimi(state production), bu değişikliklerin UI state’ine artımlı olarak uygulanmasıdır. State her zaman vardır ve eventler sonucunda değişir. Eventler ve state arasındaki farklar aşağıdaki tabloda özetlenmiştir:

EventState
Transient, unpredictable, and exist for a finite period.(Geçici, öngörülemeyen ve sonlu bir süre için var olan.)Always exists.(herzaman vardir)
The inputs of state production.(State üretiminin girdileridir.)The output of state production.(State uretiminin ciktisidir.)
The product of the UI or other sources.(UI’in veya diğer kaynaklarin urunudur.)Is consumed by the UI.(UI tarafindan tuketilir)

Yukarıdakileri özetleyen harika bir anımsatıcı şudur: state is; events happen. Aşağıdaki şema, eventler bir zaman çizelgesinde meydana geldikçe statedeki değişiklikleri görselleştirmeye yardımcı olur. Her event uygun state holder tarafından işlenir ve bir state değişikliğiyle sonuçlanır: Events cause state to change

Eventler şunlardan gelebilir:

  • Kullanıcılar: Uygulamanın UI ile etkileşime girdikçe.
  • Diğer state değişikliği kaynakları: UI’den, domainden veya snackbar zaman aşımı eventleri gibi data katmanlarından uygulama verileri sunan API’ler, sırasıyla use case siniflari veya repositoryler.

The UI state production pipeline

Android uygulamalarındaki state production, aşağıdakilerden oluşan bir processing pipeline olarak düşünülebilir:

  • Inputs;State’in kaynakları değişir. Olabilirler:
    • UI katmanında local: Bunlar, bir görev yönetimi uygulamasında “yapılacak iş” için bir başlık giren bir kullanıcı gibi kullanıcı eventleri veya UI state’indeki değişiklikleri yönlendiren UI logicine erişim sağlayan API’ler olabilir. Örneğin, Jetpack Compose’da DrawerState‘te open methodunu çağırmak.
    • UI katmanının dışında: Bunlar, UI state’inde değişikliklere neden olan domain veya data katmanlarından gelen kaynaklardır. Örneğin, bir NewsRepository’den yüklenmesi biten haberler veya diğer eventler.
    • Yukarıdakilerin hepsinin bir karışımı.
  • State holders;Business logici ve/veya UI logicini state değişikliği kaynaklarına uygulayan ve UI state oluşturmak(produce) için kullanıcı eventlerini işleyen türler.
  • Output;Uygulamanın, kullanıcılara ihtiyaç duydukları bilgileri sağlamak için işleyebileceği UI State.

The state production pipeline

State production APIs

Pipeline’in hangi aşamasında olduğunuza bağlı olarak state productionda kullanılan iki ana API vardır:

Pipeline stageAPI
InputYou should use asynchronous APIs to perform work off the UI thread to keep the UI jank free. For example, Coroutines or Flows in Kotlin, and RxJava or callbacks in the Java Programming Language.
OutputYou should use observable data holder APIs to invalidate and rerender the UI when state changes. For example, StateFlow, Compose State, or LiveData. Observable data holders guarantee the UI always has a UI state to display on the screen.

Bu ikisi arasından, input için asenkron API seçiminin, output için gözlemlenebilir API seçiminden çok, state production pipelinein doğası üzerinde daha büyük bir etkisi vardır. Bunun nedeni, inputlarin pipeline’a uygulanabilecek processing türünü dikte etmesidir.

State production pipeline assembly

Sonraki bölümler, çeşitli inputlar için en uygun state productin tekniklerini ve eşleşen output API’lerini kapsar. Her state production pipelie, inputlarin ve outputlarin bir kombinasyonudur ve şöyle olmalıdır:

  • Yaşam döngüsünün farkında(Lifecycle aware): UI’in visible veya active olmadığı durumlarda, açıkça gerekli olmadıkça state production pipeline herhangi bir kaynak tüketmemelidir.
  • Kullanımı kolay(Easy to consume): UI, üretilen UI state’ini kolayca oluşturabilmelidir(produce etmelidir). State production pipelinenin outputuna yönelik hususlar, View sistemi veya Jetpack Compose gibi farklı View API’lerinde değişiklik gösterecektir.

Not: İzleyen bölümlerde, tartışılan tüm API’ler deyimsel Kotlin ve Jetpack Compose kodunu kullanır. Ancak kılavuz, Java Programlama Dili veya Kotlin’deki diğer API’lerdeki eşdeğer analogları için geçerlidir.

Input in state production pipelines

Bir state production pipelinedeki inputlar, state değişikliği kaynaklarını şu yollarla sağlayabilir:

  • One-shot operations that may be synchronous or asynchronous, for example calls to suspend functions.
  • Stream APIs, for example Flows.
  • All of the above.

Aşağıdaki bölümlerde, yukarıdaki inputlarin her biri için bir state production pipline’i nasıl kurabileceğiniz ele alınmaktadır.

One-shot APIs as sources of state change

MutableStateFlow API’yi gözlemlenebilir, değiştirilebilir bir state containeri olarak kullanın. Jetpack Compose uygulamalarında, özellikle Compose text API‘leri ile çalışırken mutableStateOf‘u da düşünebilirsiniz. Her iki API de barındırdıkları değerlerde güvenli atomik güncellemelere izin veren methodlar sunar, güncellemeler senkron veya asenkro olsun ya da olmasın. Örneğin, basit bir zar atma uygulamasında state güncellemelerini düşünün. Kullanıcının attığı her zar, senkronize Random.nextInt() methodunu çağırır ve sonuç, UI state’ine 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,
      )
    }
  }
}

//compose
@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
  }
}

Mutating the UI state from asynchronous calls

Asenkron bir sonuç gerektiren state değişiklikleri için uygun CoroutineScope’ta bir Coroutine başlatın. Bu, CoroutineScope iptal edildiğinde uygulamanın işi silmesine izin verir. State holder daha sonra suspend method çağrısının sonucunu UI state’ini ortaya çıkarmak için kullanılan gözlemlenebilir API’ye yazar. Örneğin, [Architecture örneği](https://github.com/android/architecture-samplesndeki AddEditTaskViewModel’i göz önünde bulundurun. Askıya alınan(suspend edilen) saveTask() methodu bir taski asenkron olarak kaydettiğinde, MutableStateFlow’daki update methodu state değişikliğini UI state’ine 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))
                }
            }
        }
    }
}

//Compose State
@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))
      }
    }
  }
}

Not: Bir [AAC ViewModel](/docs/app-architecture/architecture-components/ui-layer-libraries/lifecycle-aware-components/ViewModel/about-viewmodel.md)'in viewModelScope'unda başlatılan coroutineler, istisnai olarak veya başka türlü tamamlanmaya çalışır. Bu, Coroutines açıkça iptal edilmedikçe veya ViewModel temizlenmedikçe, UI görünür olsun ya da olmasın gerçekleşir. Kısa ömürlü olma eğiliminde olduklarından, çoğu istek için bu genellikle uygundur. 5 saniye veya daha uzun süren istekleri çalıştırmak için viewModelScope kullanmamalısınız. Bunun yerine bunları WorkManager ile ertelenmiş veya uzun süreli işler olarak kuyruğa almalısınız.

Mutating the UI state from background threads

UI state’inin productionu için main dispacther Coroutines’in başlatılması tercih edilir. Yani, aşağıdaki kod parçacıklarındaki withContext bloğunun dışında. Ancak, UI state’ini farklı bir backgroud context’inde güncellemeniz gerekirse, bunu aşağıdaki API’leri kullanarak yapabilirsiniz:

  • Use the withContext method to run Coroutines in a different concurrent context.
  • When using MutableStateFlow, use the update method as usual.
  • When using Compose State, use the Snapshot.withMutableSnapshot to guarantee atomic updates to State in the concurrent context.

Örneğin, aşağıdaki DiceRollViewModel parçacığında, SlowRandom.nextInt()’in CPU’ya bağlı bir Coroutine’den çağrılması gereken hesaplama açısından yoğun bir askıya alma 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,
          )
        }
      }
    }
  }
}

// Compose State
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
        }
      }
    }
  }
}

Not: Başlatılan tüm coroutinelerin farklı bir contexden çağrılması gerekiyorsa, doğrudan viewModelScope.launch(defaultDispatcher){ } öğesini arayabilirsiniz.

Uyarı: Snapshot.withMutableSnapshot{ } kullanılmadan UI olmayan bir threadden Compose state’inin güncellenmesi, üretilen state’de tutarsızlıklara neden olabilir.

Stream APIs as sources of state change

Streamlerde zaman içinde birden çok değer üreten state değişikliği kaynakları için, tüm kaynakların outputlarini uyumlu bir bütün halinde birleştirmek, state üretimine(production) doğrudan bir yaklaşımdır.Kotlin Flows kullanırken bunu combine fonksiyonu ile başarabilirsiniz. Bunun bir örneği, InterestsViewModel’deki “Now in Android” ö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
    )
}

Not: Combined Flow’u, UI state için gözlemlenebilir API olarak bir [StateFlow](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow)'a dönüştürmek için [stateIn](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html) operatörünü kullanabilirsiniz.

StateFlows oluşturmak için stateIn operatörünün kullanılması, yalnızca UI görünürken aktif olması gerekebileceğinden, UI’ye state production pipeline’in activitysi üzerinde daha ayrıntılı kontrol sağlar.

  • Flow’un yaşam döngüsüne duyarlı bir şekilde collect edilmesi sırasında pipeline’in yalnızca UI görünür olduğunda etkin olması gerekiyorsa SharingStarted.WhileSubscription() öğesini kullanın.
  • Kullanıcı UI’e dönebildiği sürece, yani UI backstackde veya ekran dışında başka bir sekmede olduğu sürece pipeline’in aktif olması gerekiyorsa SharingStarted.Lazily kullanın.

Stream tabanlı state kaynaklarının collectinin geçerli olmadığı durumlarda, Kotlin Flows gibi stream API’leri, streamlerin UI state’ine işlenmesine yardımcı olmak için [merging](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/merge.html?query=fun%20%3CT%3E%20merge(vararg%20flows:%20Flow%3CT%3E), [flattening](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html?query=inline%20fun%20%3CT,%20R%3E%20Flow%3CT%3E.flatMapLatest(crossinline%20transform:%20suspend%20(T) vb. gibi zengin bir transformation seti sunar.

Anahtar Nokta: Çoğu durumda combine, stream API’lerinden state productiona yönelik tavsiye edilen bir yaklaşımdır.

One-shot and stream APIs as sources of state change

State production pipeline’in, state değişikliği kaynakları olarak hem tek seferlik(one-shot) çağrılara hem de streamlere bağlı olduğu durumda, tanımlayıcı kısıtlama akışlardır(streams are defining constraint). Bu nedenle, tek seferlik çağrıları stream API’lerine dönüştürün veya outputlarini streamlere aktarın ve yukarıdaki stream bölümünde açıklandığı gibi işlemeye devam edin.

Flowlarla, bu genellikle state değişikliklerini yaymak için bir veya daha fazla private backing MutableStateFlow instancei oluşturmak anlamına gelir. Compose state’inden snapshot flowlari da oluşturabilirsiniz. Aşağıdaki mimari örnekler repositorysinden TaskDetailViewModel’i göz önünde bulundurun:

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

//Compose State
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
  }
}

Not: Compose State, snapshotFlow { } API kullanılarak bir flowa dönüştürülür. Başka bir örnek için “Now In Android” örneğindeki ForYouViewModel‘e bakın.

Output types in state production pipelines

UI State için output API’sinin seçimi ve sunumunun doğası, büyük ölçüde uygulamanızın UI’ini oluşturmak için kullandığı API’ye bağlıdır. Android uygulamalarında Views veya Jetpack Compose kullanmayı seçebilirsiniz. Buradaki hususlar şunları içerir:

Aşağıdaki tablo, herhangi bir input ve consumer için state production pipeline için hangi API’lerin kullanılacağını özetlemektedir:

InputConsumerOutput
One-shot APIsViewsStateFlow or LiveData
One-shot APIsComposeStateFlow or Compose State
Stream APIsViewsStateFlow or LiveData
Stream APIsComposeStateFlow
One-shot and stream APIsViewsStateFlow or LiveData
One-shot and stream APIsComposeStateFlow

State production pipeline initialization

State production pipeline’larının başlatılması, pipeline’ın çalışması için ilk koşulların ayarlanmasını içerir. Bu, örneğin bir haber makalesinin detail view’i için bir ID gibi pipeline’ın başlatılması için kritik olan ilk girdi değerlerinin sağlanmasını veya asenkron bir yüklemenin başlatılmasını içerebilir.

Sistem kaynaklarını korumak için state production pipeline’ı mümkün olduğunca lazily başlatmalısınız. Pratikte bu genellikle çıktının bir tüketicisi olana kadar beklemek anlamına gelir. Flow API’leri, stateIn metodundaki started argümanı ile buna izin verir. Bunun uygulanamaz olduğu durumlarda, aşağıdaki kod parçasında gösterildiği gibi state production pipeline’ı explicit olarak başlatmak için idempotent bir initialize() fonksiyonu 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
        }
    }
}

Uyarı: Bir ViewModel'in init bloğunda veya constructor'ında asenkron işlemler başlatmaktan kaçının. Asenkron işlemler bir nesne oluşturmanın yan etkisi olmamalıdır çünkü asenkron kod, nesne tam olarak başlatılmadan önce nesneden okuyabilir veya nesneye yazabilir. Bu aynı zamanda leaking the object (nesnenin sızdırılması) olarak da adlandırılır ve ince ve teşhis edilmesi zor hatalara yol açabilir. Bu özellikle Compose State ile çalışırken önemlidir. ViewModel Compose State fieldlarini tuttuğunda, ViewModel'in init bloğunda Compose State fieldlarını güncelleyen bir Coroutine başlatmayın, aksi takdirde bir IllegalStateException oluşabilir.

Samples