Dikkat edilmesi gereken diğer noktalar

Views'dan Compose'a geçiş tamamen kullanıcı arayüzüyle ilgili olsa da güvenli ve kademeli bir geçiş yapmak için dikkate alınması gereken birçok nokta vardır. Bu sayfada, Görünüm tabanlı uygulamanızı Compose'a taşırken dikkat etmeniz gereken bazı noktalar açıklanmaktadır.

Uygulamanızın temasını taşıma

Material Design, Android uygulamalarını temalandırmak için önerilen tasarım sistemidir.

Görünüme dayalı uygulamalarda üç Material sürümü kullanılabilir:

  • AppCompat kitaplığının kullanıldığı Material Design 1 (ör. Theme.AppCompat.*)
  • MDC-Android kitaplığı (ör. Theme.MaterialComponents.*) kullanılarak Material Design 2
  • MDC-Android kitaplığı (ör. Theme.Material3.*) kullanılarak Material Design 3

Compose uygulamaları için iki Material sürümü mevcuttur:

  • Compose Material kitaplığını kullanarak Material Design 2 (ör. androidx.compose.material.MaterialTheme)
  • Compose Material 3 kitaplığını kullanarak Material Design 3 (ör. androidx.compose.material3.MaterialTheme)

Uygulamanızın tasarım sistemi destekliyorsa en yeni sürümü (Material 3) kullanmanızı öneririz. Hem Views hem de Compose için taşıma kılavuzları mevcuttur:

Compose'da yeni ekranlar oluştururken, kullandığınız Material Design sürümü ne olursa olsun, Compose Material kitaplıklarından kullanıcı arayüzü yayan composable'lardan önce MaterialTheme uyguladığınızdan emin olun. Material bileşenleri (Button, Text vb.), MaterialTheme öğesinin yerinde olmasına bağlıdır ve bu öğe olmadan davranışları tanımlanmamıştır.

Tüm Jetpack Compose örnekleri, MaterialTheme üzerine kurulu özel bir Compose teması kullanır.

Daha fazla bilgi edinmek için Compose'da tasarım sistemleri ve XML temalarını Compose'a taşıma başlıklı makaleleri inceleyin.

Uygulamanızda Navigation bileşenini kullanıyorsanız daha fazla bilgi için Compose ile gezinme - Birlikte çalışabilirlik ve Jetpack Navigation'ı Navigation Compose'a taşıma başlıklı makaleleri inceleyin.

Karma Compose/Görünümler kullanıcı arayüzünüzü test etme

Uygulamanızın bazı bölümlerini Compose'a taşıdıktan sonra, herhangi bir şeyi bozmadığınızdan emin olmak için test yapmanız çok önemlidir.

Bir etkinlik veya parça Compose'u kullandığında ActivityScenarioRule yerine createAndroidComposeRule kullanmanız gerekir. createAndroidComposeRule, Compose ve View kodunu aynı anda test etmenizi sağlayan bir ComposeTestRule ile ActivityScenarioRule entegre olur.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Test hakkında daha fazla bilgi edinmek için Oluşturma düzeninizi test etme başlıklı makaleyi inceleyin. UI testi çerçeveleriyle birlikte çalışabilirlik için Espresso ile birlikte çalışabilirlik ve UiAutomator ile birlikte çalışabilirlik başlıklı makalelere bakın.

Compose'u mevcut uygulama mimarinizle entegre etme

Tek Yönlü Veri Akışı (UDF) mimari kalıpları, Compose ile sorunsuz bir şekilde çalışır. Uygulama bunun yerine Model View Presenter (MVP) gibi başka mimari kalıplar kullanıyorsa Compose'u kullanmaya başlamadan önce veya kullanırken kullanıcı arayüzünün bu bölümünü UDF'ye taşımanızı öneririz.

Yazma işleminde ViewModel kullanma

Architecture Components ViewModel kitaplığını kullanıyorsanız Compose ve diğer kitaplıklar bölümünde açıklandığı gibi viewModel() işlevini çağırarak herhangi bir composable'dan ViewModel öğesine erişebilirsiniz.

Compose'u kullanırken ViewModel öğeleri View yaşam döngüsü kapsamlarını izlediğinden farklı composable'larda aynı ViewModel türünü kullanmamaya dikkat edin. Kapsam, Navigation kitaplığı kullanılıyorsa ana makine etkinliği, parça veya gezinme grafiği olur.

Örneğin, composable'lar bir etkinlikte barındırılıyorsa viewModel() her zaman aynı örneği döndürür. Bu örnek yalnızca etkinlik tamamlandığında temizlenir. Aşağıdaki örnekte, aynı kullanıcı ("user1") iki kez karşılanıyor. Bunun nedeni, ana makine etkinliği altındaki tüm composable'larda aynı GreetingViewModel örneğinin yeniden kullanılmasıdır. Oluşturulan ilk ViewModel örneği diğer composable'larda yeniden kullanılır.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Navigasyon grafikleri ViewModel öğelerini de kapsadığından, navigasyon grafiğinde hedef olan composable'lar farklı bir ViewModel örneğine sahiptir. Bu durumda, ViewModel hedef yaşam döngüsüyle sınırlanır ve hedef, geri yığından kaldırıldığında temizlenir. Aşağıdaki örnekte, kullanıcı Profil ekranına gittiğinde GreetingViewModel öğesinin yeni bir örneği oluşturulur.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Doğru kaynağın durumu

Compose'u kullanıcı arayüzünün bir bölümünde kullandığınızda Compose ve View sistemi kodunun verileri paylaşması gerekebilir. Mümkün olduğunda, bu paylaşılan durumu her iki platform tarafından kullanılan UDF en iyi uygulamalarına uygun başka bir sınıfta kapsamanızı öneririz. Örneğin, veri güncellemelerini yayınlamak için paylaşılan verilerin akışını gösteren bir ViewModel içinde.

Ancak paylaşılacak veriler değiştirilebiliyorsa veya bir kullanıcı arayüzü öğesine sıkı bir şekilde bağlıysa bu her zaman mümkün olmayabilir. Bu durumda, bir sistem doğruluk kaynağı olmalı ve bu sistem, diğer sistemle tüm veri güncellemelerini paylaşmalıdır. Genel bir kural olarak, doğruluk kaynağı, kullanıcı arayüzü hiyerarşisinin köküne daha yakın olan öğeye ait olmalıdır.

Doğru bilgi kaynağı olarak oluşturma

Compose durumunu Compose olmayan koda yayınlamak için SideEffect composable'ı kullanın. Bu durumda, doğruluk kaynağı, durum güncellemeleri gönderen bir composable içinde tutulur.

Örneğin, analiz kitaplığınız, sonraki tüm analiz etkinliklerine özel meta veriler (bu örnekte kullanıcı özellikleri) ekleyerek kullanıcı popülasyonunuzu segmentlere ayırmanıza olanak tanıyabilir. Mevcut kullanıcının kullanıcı türünü analiz kitaplığınıza iletmek için SideEffect kullanarak değerini güncelleyin.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Daha fazla bilgi için Compose'daki yan etkiler başlıklı makaleyi inceleyin.

Sistemi doğru bilgi kaynağı olarak görme

Durum, View sistemi tarafından sahiplenilip Compose ile paylaşılıyorsa Compose'da iş parçacığı güvenli hale getirmek için durumu mutableStateOf nesnelerine sarmalamanızı öneririz. Bu yaklaşımı kullanırsanız composable işlevler basitleştirilir. Çünkü artık tek bir doğru kaynağa sahip değildirler. Ancak View sisteminin, değiştirilebilir durumu ve bu durumu kullanan View'ları güncellemesi gerekir.

Aşağıdaki örnekte, CustomViewGroup, TextView ve TextField composable'ı içeren bir ComposeView içeriyor. TextView, kullanıcının TextField alanına yazdığı içeriği göstermelidir.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

Paylaşılan kullanıcı arayüzünü taşıma

Compose'a kademeli olarak geçiş yapıyorsanız hem Compose'da hem de View sisteminde paylaşılan kullanıcı arayüzü öğelerini kullanmanız gerekebilir. Örneğin, uygulamanızda özel bir CallToActionButton bileşen varsa hem Compose hem de View tabanlı ekranlarda bu bileşeni kullanmanız gerekebilir.

Compose'da, paylaşılan kullanıcı arayüzü öğeleri, XML kullanılarak stil verilen öğe veya özel görünümden bağımsız olarak uygulama genelinde yeniden kullanılabilen composable'lara dönüşür. Örneğin, CallToActionButton harekete geçirici mesaj bileşeniniz için composable oluşturursunuz Button.

Görünüme dayalı ekranlarda composable'ı kullanmak için AbstractComposeView'dan türeyen özel bir görünüm sarmalayıcısı oluşturun. Geçersiz kılınan Content composable'ında, aşağıdaki örnekte gösterildiği gibi, oluşturduğunuz composable'ı Compose temanıza sarılmış şekilde yerleştirin:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Birleştirilebilir parametrelerin, özel görünümde değiştirilebilir değişkenler haline geldiğini unutmayın. Bu sayede, özel CallToActionViewButton görünümü geleneksel bir görünüm gibi şişirilebilir ve kullanılabilir hale gelir. View Binding ile ilgili örneği aşağıda görebilirsiniz:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

Özel bileşen değiştirilebilir durum içeriyorsa Doğruluğun durum kaynağı bölümüne bakın.

Durumu sunudan ayırmaya öncelik verin

Geleneksel olarak View durum bilgisi içerir. View, nasıl gösterileceğinin yanı sıra ne gösterileceğini açıklayan alanları yönetir. Bir View öğesini Compose'a dönüştürdüğünüzde, state hoisting bölümünde daha ayrıntılı olarak açıklandığı gibi tek yönlü bir veri akışı elde etmek için oluşturulan verileri ayırmaya çalışın.

Örneğin, bir View, görünür, görünmez veya kayıp olup olmadığını açıklayan bir visibility özelliğine sahiptir. Bu, View'nın doğasında olan bir özelliktir. Diğer kod parçaları bir View öğesinin görünürlüğünü değiştirebilir ancak mevcut görünürlüğünün ne olduğunu gerçekten bilen tek şey View öğesinin kendisidir. Bir View öğesinin görünür olmasını sağlama mantığı hataya açık olabilir ve genellikle View öğesinin kendisine bağlıdır.

Bunun aksine, Compose, Kotlin'deki koşullu mantığı kullanarak tamamen farklı composable'ları görüntülemeyi kolaylaştırır:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

CautionIcon, tasarım gereği neden gösterildiğini bilmek veya önemsemek zorunda değildir. visibility kavramı yoktur: ya Kompozisyon'dadır ya da değildir.

Durum yönetimi ile sunum mantığını net bir şekilde ayırarak, içeriği durumdan kullanıcı arayüzüne dönüştürürken nasıl göstereceğinizi daha özgürce değiştirebilirsiniz. Gerekli olduğunda durumu yükseltebilmek, durum sahipliği daha esnek olduğundan composable'ları daha fazla yeniden kullanılabilir hale getirir.

Kapsüllenmiş ve yeniden kullanılabilir bileşenleri tanıtın

View öğeleri genellikle nerede bulundukları hakkında bir fikre sahiptir: Activity, Dialog, Fragment veya başka bir View hiyerarşisinin içinde bir yerde. Genellikle statik düzen dosyalarından şişirildikleri için View öğesinin genel yapısı çok katı olma eğilimindedir. Bu durum, daha sıkı bir bağlantıya yol açar ve View öğesinin değiştirilmesini veya yeniden kullanılmasını zorlaştırır.

Örneğin, özel bir View belirli bir kimliğe sahip belirli bir türde alt görünümü olduğunu varsayabilir ve bazı işlemlere yanıt olarak özelliklerini doğrudan değiştirebilir. Bu durum, söz konusu View öğelerini birbirine sıkıca bağlar: Alt öğeyi bulamazsa özel View kilitlenebilir veya bozulabilir. Alt öğe ise özel View üst öğesi olmadan yeniden kullanılamaz.

Yeniden kullanılabilir composable'lar sayesinde Compose'da bu sorun daha az görülür. Ebeveynler durumu ve geri çağırmaları kolayca belirleyebilir. Böylece, kullanılacakları yeri tam olarak bilmenize gerek kalmadan yeniden kullanılabilir composable'lar yazabilirsiniz.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

Yukarıdaki örnekte, üç bölümün tamamı daha fazla kapsüllenmiş ve daha az bağlıdır:

  • ImageWithEnabledOverlay yalnızca mevcut isEnabled durumunun ne olduğunu bilmesi gerekir. ControlPanelWithToggle öğesinin varlığını veya nasıl kontrol edilebileceğini bilmesi gerekmez.

  • ControlPanelWithToggle, ImageWithEnabledOverlay'nin varlığından haberdar değildir. isEnabled öğesinin gösterilme şekli sıfır, bir veya daha fazla olabilir ve ControlPanelWithToggle öğesinin değişmesi gerekmez.

  • Ebeveyn için ImageWithEnabledOverlay veya ControlPanelWithToggle öğelerinin ne kadar iç içe yerleştirildiği önemli değildir. Bu çocuklar değişiklikleri animasyonla gösterebilir, içerikleri değiştirebilir veya diğer çocuklara iletebilir.

Bu kalıp, kontrolün tersine çevrilmesi olarak bilinir. Bu konu hakkında daha fazla bilgiyi CompositionLocal dokümanlarında bulabilirsiniz.

Ekran boyutu değişikliklerini işleme

Farklı pencere boyutları için farklı kaynaklara sahip olmak, duyarlı View düzenler oluşturmanın temel yollarından biridir. Nitelikli kaynaklar, ekran düzeyindeki düzen kararları için hâlâ bir seçenek olsa da Compose, normal koşullu mantıkla düzenleri tamamen kodda değiştirmeyi çok daha kolay hale getirir. Daha fazla bilgi edinmek için Pencere boyutu sınıflarını kullanma başlıklı makaleyi inceleyin.

Ayrıca, Compose'un uyarlanabilir kullanıcı arayüzleri oluşturmak için sunduğu teknikler hakkında bilgi edinmek üzere Farklı ekran boyutlarını destekleme başlıklı makaleyi inceleyin.

Görünümlerle iç içe kaydırma

Kaydırılabilir View öğeleri ile her iki yönde de iç içe yerleştirilmiş kaydırılabilir composable'lar arasında iç içe kaydırma birlikte çalışabilirliğini etkinleştirme hakkında daha fazla bilgi için İç içe kaydırma birlikte çalışabilirliği başlıklı makaleyi inceleyin.

RecyclerView uygulamasında oluşturma

RecyclerView sürümü 1.3.0-alpha02'den itibaren RecyclerView içindeki composable'lar yüksek performanslıdır. Bu avantajlardan yararlanmak için RecyclerView uygulamasının en az 1.3.0-alpha02 sürümünü kullandığınızdan emin olun.

WindowInsets Görünümlerle birlikte çalışabilirlik

Ekranınızda aynı hiyerarşide hem Görünümler hem de Compose kodu varsa varsayılan iç boşlukları geçersiz kılmanız gerekebilir. Bu durumda, hangi öğenin iç kısımları kullanacağını ve hangisinin bunları yoksayacağını açıkça belirtmeniz gerekir.

Örneğin, en dıştaki düzeniniz bir Android View düzeniyse View sistemindeki iç kısımları kullanmanız ve bunları Compose için yoksaymanız gerekir. Alternatif olarak, en dıştaki düzeniniz composable ise Compose'daki yerleşimleri kullanmanız ve AndroidView composable'ları buna göre doldurmanız gerekir.

Varsayılan olarak her ComposeView, WindowInsetsCompat tüketim düzeyindeki tüm ekleri tüketir. Bu varsayılan davranışı değiştirmek için ComposeView.consumeWindowInsets değerini false olarak ayarlayın.

Daha fazla bilgi için Compose'da WindowInsets dokümanını inceleyin.