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 oturumun amacı, aşağıdakileri anlamanıza yardımcı olmaktır:
- Kullanıcı arayüzü durumu oluşturmak için hangi API'lerin kullanılacağı. Bu, tek yönlü veri akışı ilkelerine uygun olarak durum tutucularınızda bulunan durum değişikliği kaynaklarının yapısına bağlıdır.
- Kullanıcı arayüzü durumunun üretimini, sistem kaynaklarının farkında olacak şekilde nasıl kapsamlandıracağınızı öğrenin.
- Kullanıcı arayüzü durumunu, kullanıcı arayüzü tarafından kullanılacak şekilde nasıl kullanıma sunabilirsiniz?
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 aşağıdaki 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 Snackbar zaman aşımı etkinlikleri, kullanım alanları ya da depolar gibi veri katmanlarından 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 ardışık düzeni olarak düşünülebilir:
- Girişler: Durum değişikliğinin kaynakları. Şunlardan biri olabilir:
- Kullanıcı arayüzü katmanına özel: Bunlar, bir kullanıcının görev yönetimi uygulamasında "yapılacaklar" için başlık girmesi 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, kullanıcı arayüzü durumunda değişikliklere neden olan alan veya veri katmanlarındaki kaynaklardır. Örneğin,
NewsRepository'dan yüklenmesi tamamlanan haberler veya diğer etkinlikler. - Yukarıdakilerin bir karışımı.
- Kullanıcı arayüzü katmanına özel: Bunlar, bir kullanıcının görev yönetimi uygulamasında "yapılacaklar" için başlık girmesi 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 kullanıcı arayüzü mantığı uygulayan, kullanıcı etkinliklerini işleyerek kullanıcı arayüzü durumu oluşturan 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 takılmasını önlemek için Coroutines ve Flows gibi eşzamansız API'leri kullanarak kullanıcı arayüzü iş parçacığı dışında çalışın. |
| Çıkış | Durum değiştiğinde kullanıcı arayüzünü geçersiz kılmak ve yeniden oluşturmak için Compose State veya StateFlow gibi gözlemlenebilir veri tutucu API'lerini kullanın. Gözlemlenebilir veri sahipleri, kullanıcı arayüzünün ekranda her zaman gösterecek bir kullanıcı arayüzü durumuna sahip olmasını sağlar. |
Giriş için eşzamansız API seçimi, çıkış için gözlemlenebilir API seçimine kıyasla durum üretim ardışık düzeninin 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 aşağıdaki gibi 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. Jetpack Compose'da, composable'lar durum değişikliklerine göre güncellenebildiğinden durum tüketimi kullanıcı arayüzü için çok önemlidir.
Durum üretim işlem hatlarındaki girişler
Durum üretim ardışık düzenindeki girişler, durum değişikliği kaynaklarını aşağıdaki yollarla sağlar:
- Eşzamanlı veya eşzamansız olabilen tek seferlik işlemler (ör.
suspendişlevlerine yapılan çağrılar). - Akış API'leri (ör.
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
Gözlemlenebilir veri tutucularla durumu yönetin. mutableStateOf API'yi kullanın. Özellikle metin oluşturma API'leriyle çalışırken bu API'yi kullanmanız önerilir. Daha karmaşık durum yönetimi veya diğer mimari bileşenlerle entegrasyon için MutableStateFlow API'sini kullanın. Her iki API de, barındırdıkları değerlerde eşzamanlı veya eşzamansız 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.
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
}
}
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,
)
}
}
}
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.
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))
}
}
}
}
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))
}
}
}
}
}
Kullanıcı arayüzü durumunu arka plan iş parçacıklarından değiştirme
Kullanıcı arayüzü durumunun oluşturulması için Coroutine'leri ana dağıtıcıda başlatmak tercih edilir. Yani aşağıdaki kod snippet'lerinde withContext bloğunun dışında başlatmak tercih edilir.
Ancak kullanıcı arayüzü durumunu farklı bir arka plan bağlamında güncellemeniz gerekiyorsa şunları 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.withMutableSnapshotyöntemini 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.
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
}
}
}
}
}
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,
)
}
}
}
}
}
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şlemi combine işleviyle yapabilirsiniz.
Bunun bir örneğini "Now in Android" örneğinde görebilirsiniz:
InterestsViewModel:
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ü yalnızca görünür olduğunda etkin olması gerekebileceğinden, kullanıcı arayüzüne 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.WhileSubscribedkullanın. - Kullanıcı arayüze geri dönebileceği sürece ardışık düzenin etkin olması gerekiyorsa
SharingStarted.Lazilykullanın. Yani kullanıcı arayüzü, geri yığında veya ekran dışındaki başka bir sekmededir.
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, 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ığı şekilde 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ı oluşturabilirsiniz.
architecture-samples deposundaki TaskDetailViewModel dosyasını inceleyin. Kullanıcı arayüzü durumu, mevcut görev için bir akışa (_task) ve görev silindiğinde güncellenen tek seferlik bir kaynağa (_isTaskDeleted) bağlıdır. Bu işaret, bir görevin yanlış kimlik nedeniyle veritabanında bulunamadığı durum ile kullanıcının görevi silmesi nedeniyle bulunamadığı durumu ayırt etmek için gereklidir:
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, taskAsync ->
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
}
}
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, taskAsync ->
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 }
}
}
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 (ör. Compose) bağlıdır. Jetpack Compose, yerel kullanıcı arayüzü oluşturmak için önerilen modern araç setidir. Bu noktada dikkat edilmesi gerekenler:
- Okuma durumu, yaşam döngüsüne duyarlı bir şekilde.
- Durum tutucudan durumun bir veya daha fazla alanda gösterilip gösterilmeyeceği.
Aşağıdaki tabloda, Jetpack Compose kullanırken durum üretim ardışık düzeniniz için hangi API'lerin kullanılacağı özetlenmiştir:
| Giriş | Çıkış |
|---|---|
| Tek seferlik API'ler | StateFlow veya Oluştur'u State tıklayın. |
| Stream API'leri | StateFlow |
| Tek seferlik ve akış API'leri | 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 bir yüklemenin başlatılmasını içerebilir.
Mümkün olduğunda sistem kaynaklarını korumak için durum üretim ardışık düzenini geç başlatın. Pratikte bu genellikle çıktıyı tüketen bir öğe 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 initialize idempotent 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
}
}
}
Örnekler
Aşağıdaki Google örneklerinde, kullanıcı arayüzü katmanında durum oluşturma işlemi gösterilmektedir. Bu kılavuzun nasıl uygulandığını görmek için aşağıdaki kaynakları inceleyin:
Ek kaynaklar
Kullanıcı arayüzü durumu hakkında daha fazla bilgi için aşağıdaki ek kaynaklara bakın:
Belgeler
İçeriği görüntüleme
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}