Dikkat edilmesi gereken diğer noktalar

Görünümler'den Compose'a geçiş tamamen kullanıcı arayüzüyle ilgili olsa da güvenli ve aşamalı bir geçiş gerçekleştirmek 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 yer almaktadır.

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

Materyal Tasarım, Android uygulamalarına tema uygulamak için önerilen tasarım sistemidir.

Görünüm tabanlı uygulamalar için üç Material sürümü mevcuttur:

  • AppCompat kitaplığını kullanan Materyal Tasarım 1 (ör. Theme.AppCompat.*)
  • MDC-Android kitaplığını kullanan Materyal Tasarım 2 (ör. Theme.MaterialComponents.*)
  • MDC-Android kitaplığını (ör. Theme.Material3.*) kullanan Materyal Tasarım 3

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

  • Compose Material kitaplığını kullanan Materyal Tasarım 2 (ör. androidx.compose.material.MaterialTheme)
  • Compose Material 3 kitaplığını kullanan Materyal Tasarım 3 (ör. androidx.compose.material3.MaterialTheme)

Uygulamanızın tasarım sistemi uygunsa en yeni sürümü (Material 3) kullanmanızı öneririz. Hem Görünümler hem de Compose için geçiş rehberleri mevcuttur:

Compose'da yeni ekranlar oluştururken, kullandığınız Materyal Tasarım sürümünden bağımsız olarak, Compose Material kitaplıklarından kullanıcı arayüzü oluşturan tüm composable'lardan önce bir MaterialTheme uyguladığınızdan emin olun. Materyal bileşenleri (Button, Text vb.) bir MaterialTheme öğesinin mevcut olmasına bağlıdır ve bu öğe olmadan davranışları tanımlanmaz.

Tüm Jetpack Compose örnekleri, MaterialTheme üzerine oluşturulmuş ö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 Parça tabanlı bir uygulamada Compose'dan gezinme ve Jetpack Navigation'ı Navigation Compose'a taşıma başlıklı makaleleri inceleyin.

Karma Compose/Views 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 yapmak ç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 etmenize olanak tanıyan 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. Kullanıcı arayüzü test çerçeveleriyle birlikte çalışabilirlik için Espresso ile birlikte çalışabilirlik ve UiAutomator ile birlikte çalışabilirlik bölümlerine 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, Model View Presenter (MVP) gibi başka mimari kalıplar kullanıyorsa kullanıcı arayüzünün bu bölümünü Compose'u kullanmaya başlamadan önce veya kullanmaya başlarken UDF'ye taşımanızı öneririz.

Oluşturma'da 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 kullanmaya başlarken farklı composable'larda aynı ViewModel türünü kullanmaya dikkat edin. ViewModel öğeleri, görünüm yaşam döngüsü kapsamlarına uyar. Kapsam; ana etkinlik, parça veya Gezinme kitaplığı kullanılıyorsa gezinme grafiği olur.

Örneğin, composable'lar bir etkinlikte barındırılıyorsa viewModel() her zaman yalnızca etkinlik bittiğinde temizlenen aynı örneği döndürür. Aşağıdaki örnekte, aynı kullanıcı ("user1") iki kez karşılanır çünkü aynı GreetingViewModel örneği, ana makine etkinliğinin altındaki tüm composable'larda yeniden kullanılı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
    }
}

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

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

Eyalet veri kaynağı

Kullanıcı arayüzünün bir bölümünde Compose'u kullandığınızda Compose ve View sistemi kodunun veri paylaşması gerekebilir. Mümkün olduğunda, bu paylaşılan durumu her iki platform tarafından kullanılan UDF en iyi uygulamalarını izleyen başka bir sınıfta kapsüllemenizi ö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, sistemlerden biri doğru bilgi kaynağı olmalı ve veri güncellemelerini diğer sistemle 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 Compose

Compose durumunu Compose dışı koda yayınlamak için SideEffect composable'ını kullanın. Bu durumda, doğru kaynak, durum güncellemeleri gönderen bir composable'da tutulur.

Örneğin, analiz kitaplığınız, sonraki tüm analiz etkinliklerine özel meta veriler (bu örnekte kullanıcı özellikleri) ekleyerek kullanıcı kitlenizi 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'da yan etkiler başlıklı makaleyi inceleyin.

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

Durum, görünüm sistemine aitse ve Compose ile paylaşılıyorsa Compose için iş parçacığı açısından güvenli hale getirmek amacıyla durumu mutableStateOf nesnelerine sarmalamanızı öneririz. Bu yaklaşımı kullanırsanız composable işlevler basitleştirilir çünkü artık doğru kaynak onlarda değildir. Ancak Görünüm sisteminin, değiştirilebilir durumu ve bu durumu kullanan Görünümleri güncellemesi gerekir.

Aşağıdaki örnekte, CustomViewGroup bir TextView ve içinde TextField composable'ı olan bir ComposeView içerir. 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 görünüm sisteminde paylaşılan kullanıcı arayüzü öğelerini kullanmanız gerekebilir. Örneğin, uygulamanızda özel bir CallToActionButton bileşeni varsa bunu hem Oluşturma hem de Görünüm tabanlı ekranlarda kullanmanız gerekebilir.

Compose'da, paylaşılan kullanıcı arayüzü öğeleri, öğenin XML kullanılarak stillendirilmesine veya özel bir görünüm olmasına bakılmaksızın uygulama genelinde yeniden kullanılabilen composable'lar haline gelir. Örneğin, özel harekete geçirici mesaj Button bileşeniniz için bir CallToActionButton composable oluşturursunuz.

Oluşturulabilir öğeyi görünüm tabanlı ekranlarda kullanmak için AbstractComposeView'dan genişletilen özel bir görünüm sarmalayıcı oluşturun. Geçersiz kılınan Content composable'ında, oluşturduğunuz composable'ı aşağıdaki örnekte gösterildiği gibi Compose temanıza sarmalanmış ş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)
        }
    }
}

Oluşturulabilir parametrelerin, özel görünümde değiştirilebilir değişkenlere dönüştüğüne dikkat edin. Bu sayede özel CallToActionViewButton görünümü, geleneksel bir görünüm gibi şişirilebilir ve kullanılabilir hale gelir. Görünüm bağlama ile ilgili bir ö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 Durum için doğru kaynak bölümüne bakın.

Durumu sunudan ayırmaya öncelik verme

Geleneksel olarak View durum bilgisi içerir. View, nasıl gösterileceğinin yanı sıra neyin gösterileceğini açıklayan alanları yönetir. Bir View öğesini Compose'a dönüştürürken, 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 dikkat edin.

Örneğin, bir View, görünür, görünmez veya kaybolmuş olup olmadığını açıklayan bir visibility özelliğine sahiptir. Bu, View modelinin doğal bir özelliğidir. Diğer kod parçaları bir View öğesinin görünürlüğünü değiştirebilse de yalnızca View öğesinin kendisi mevcut görünürlüğünün ne olduğunu gerçekten bilir. 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.

Buna karşılık Compose, Kotlin'de koşullu mantık kullanarak tamamen farklı composable'ları görüntülemeyi kolaylaştırır:

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

Tasarımı gereği CautionIcon, neden gösterildiğini bilmek veya önemsemek zorunda değildir ve visibility kavramı yoktur: Ya oluşturmada yer alır ya da yer almaz.

Durum yönetimi ve sunum mantığını net bir şekilde ayırarak içeriği kullanıcı arayüzüne dönüştürme şeklinizi daha özgürce değiştirebilirsiniz. Gerektiğinde durumu yukarı çekebilmek, durum sahipliği daha esnek olduğundan composable'ları daha yeniden kullanılabilir hale getirir.

Kapsüllenmiş ve yeniden kullanılabilir bileşenleri teşvik etme

View öğeleri genellikle nerede bulundukları hakkında bir fikre sahiptir: Activity, Dialog, Fragment veya başka bir View hiyerarşisinin içinde. Genellikle statik düzen dosyalarından şişirildikleri için View'nin genel yapısı çok katı olma eğilimindedir. Bu durum, daha sıkı bağlantıya neden olur ve View 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 çocuk ekranına sahip olduğunu varsayabilir ve bazı işlemlere yanıt olarak özelliklerini doğrudan değiştirebilir. Bu durum, View öğelerini birbirine sıkıca bağlar: Özel View, alt öğeyi bulamazsa kilitlenebilir veya bozulabilir ve alt öğe, özel View üst öğesi olmadan yeniden kullanılamayabilir.

Yeniden kullanılabilir bileşenler sayesinde Compose'da bu sorun daha az görülür. Ebeveynler, durumu ve geri çağırmaları kolayca belirtebilir. Böylece, tam olarak nerede kullanılacaklarını 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 de daha kapsüllenmiş ve daha az bağlıdır:

  • ImageWithEnabledOverlay, yalnızca mevcut isEnabled durumunun ne olduğunu bilmelidir. ControlPanelWithToggle'nin var olduğunu veya nasıl kontrol edilebileceğini bilmesi gerekmez.

  • ControlPanelWithToggle, ImageWithEnabledOverlay adlı bir dosyanın varlığından haberdar değil. isEnabled, sıfır, bir veya daha fazla şekilde gösterilebilir ve ControlPanelWithToggle'in değişmesi gerekmez.

  • Üst öğe için ImageWithEnabledOverlay veya ControlPanelWithToggle öğelerinin ne kadar iç içe yerleştirildiği önemli değildir. Bu alt öğeler değişiklikleri canlandırıyor, içerikleri değiştiriyor veya içerikleri diğer alt öğelere aktarıyor olabilir.

Bu kalıp, denetimin tersine çevrilmesi olarak bilinir. Bu konu hakkında daha fazla bilgiyi CompositionLocal belgelerinde bulabilirsiniz.

Ekran boyutu değişikliklerini işleme

Farklı pencere boyutları için farklı kaynaklar kullanmak, duyarlı View düzenleri oluşturmanın temel yollarından biridir. Ekran düzeyinde düzen kararları için nitelikli kaynaklar 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 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 görünüm öğeleri ile kaydırılabilir composable'lar arasında her iki yönde iç içe yerleştirilmiş kaydırma birlikte çalışabilirliğini nasıl etkinleştireceğiniz hakkında daha fazla bilgi için İç içe yerleştirilmiş kaydırma birlikte çalışabilirliği başlıklı makaleyi inceleyin.

RecyclerView uygulamasında oluşturun

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

WindowInsets, Görünümler ile birlikte çalışabilir

Ekranınızda aynı hiyerarşide hem Görünüm hem de Oluşturma kodu varsa varsayılan iç boşlukları geçersiz kılmanız gerekebilir. Bu durumda, hangisinin içe yerleştirmeleri kullanması, hangisinin yoksayması gerektiğini açıkça belirtmeniz gerekir.

Örneğin, en dıştaki düzeniniz bir Android View düzeniyse iç boşlukları View sisteminde kullanmanız ve Oluşturma için yoksaymanız gerekir. Alternatif olarak, en dıştaki düzeniniz bir composable ise Compose'da iç boşlukları kullanmanız ve AndroidView composable'ları buna göre doldurmanız gerekir.

Varsayılan olarak her ComposeView, WindowInsetsCompat tüketim düzeyindeki tüm iç kısımları 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ümanlarını okuyun.