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 ş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üzerindeopenyö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,
NewsRepositoryveya diğer etkinliklerden yüklenen haberler. - Yukarıdakilerin tümünün bir karışımı.
- 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
- 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.
Ü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.
suspendiş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
withContextyöntemini kullanın. MutableStateFlowkullanırkenupdateyö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.withMutableSnapshotkullanı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.Lazilykullanı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:
- Yaşam döngüsüne duyarlı bir şekilde okuma durumu.
- Eyaletin, bir veya birden fazla alanda durum bilgisi depolayıcı tarafından gösterilip gösterilmeyeceği.
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:
Sizin için önerilenler
- Not: JavaScript kapalıyken bağlantı metni gösterilir.
- Kullanıcı arayüzü katmanı
- Önceliği çevrimdışı kullanıma veren bir uygulama oluşturma
- Durum tutucular ve kullanıcı arayüzü durumu {:#mad-arch}