Manuel bağımlı yerleştirme

Android'in önerdiği uygulama mimarisi, kodu sınıflara bölerek sorumlulukların ayrılmasından yararlanmanızı teşvik eder. Bu ilkeye göre, hiyerarşideki her sınıfın tek bir tanımlanmış sorumluluğu vardır. Bu durum, birbirlerinin bağımlılıklarını karşılamak için bağlanması gereken daha fazla ve daha küçük sınıflara yol açar.

Android uygulamaları genellikle birçok sınıftan oluşur ve bazıları birbirine bağlıdır.
Şekil 1. Android uygulamasının uygulama grafiği modeli

Sınıflar arasındaki bağımlılıklar, her sınıfın bağlı olduğu sınıflara bağlandığı bir grafik olarak gösterilebilir. Tüm sınıflarınızın ve bağımlılıklarının temsili, uygulama grafiğini oluşturur. Şekil 1'de uygulama grafiğinin bir soyutlamasını görebilirsiniz. A sınıfı (ViewModel) B sınıfına (Repository) bağlı olduğunda, bu bağımlılığı temsil eden ve A'dan B'ye doğru yönelen bir çizgi vardır.

Bağımlılık ekleme, bu bağlantıların kurulmasına yardımcı olur ve test için uygulamaları değiştirmenize olanak tanır. Örneğin, bir depoya bağlı olan bir ViewModel test ederken farklı durumları test etmek için Repository öğesinin farklı uygulamalarını sahtelerle veya taklitlerle iletebilirsiniz.

Manuel bağımlılık eklemenin temelleri

Bu bölümde, gerçek bir Android uygulaması senaryosunda manuel bağımlılık eklemenin nasıl uygulanacağı açıklanmaktadır. Bu dokümanda, uygulamanızda bağımlılık eklemeyi kullanmaya nasıl başlayabileceğinize dair yinelemeli bir yaklaşım açıklanmaktadır. Bu yaklaşım, Dagger'ın sizin için otomatik olarak oluşturacağı yaklaşıma çok benzeyen bir noktaya ulaşana kadar iyileştirilir. Dagger hakkında daha fazla bilgi için Dagger'ın temel özellikleri başlıklı makaleyi inceleyin.

Akış, uygulamanızdaki bir özelliğe karşılık gelen bir grup ekran olarak kabul edilir. Giriş, kayıt ve ödeme işlemleri akış örnekleridir.

Tipik bir Android uygulamasının giriş akışını kapsarken LoginActivity, LoginViewModel'e, LoginViewModel ise UserRepository'e bağlıdır. Ardından UserRepository, UserLocalDataSource ve UserRemoteDataSource'a bağlıdır. Bu da Retrofit hizmetine bağlıdır.

LoginActivity, oturum açma akışının giriş noktasıdır ve kullanıcı, etkinlikle etkileşimde bulunur. Bu nedenle, LoginActivity, tüm bağımlılıklarıyla birlikte LoginViewModel oluşturmalıdır.

Akışın Repository ve DataSource sınıfları şu şekilde görünür:

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }
class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

Compose'da ComponentActivity giriş noktasıdır. Bağımlılık bağlantısı onCreate içinde bir kez yapılır ve kullanıcı arayüzü, setContent'den çağrılan composable'lar tarafından tanımlanır:

class ApiService {
    /* Your API implementation here */
}

class UserRepository(private val apiService: ApiService) {
    /* Your implementation here */
}

class LoginActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Satisfy the dependencies of LoginViewModel recursively,
        // then pass what the UI needs into setContent.
        val apiService = ApiService()
        val userRepository = UserRepository(apiService)

        setContent {
            LoginScreen(userRepository)
        }
    }
}

@Composable
fun LoginScreen(userRepository: UserRepository) {
    val viewModel: LoginViewModel = viewModel(
        factory = LoginViewModelFactory(userRepository)
    )
    // ...
}

Bu yaklaşımla ilgili sorunlar vardır:

  1. Bağımlılıklar sırayla beyan edilmelidir. UserRepository öğesini oluşturmak için LoginViewModel öğesinden önce başlatmanız gerekir.
  2. Nesneleri yeniden kullanmak zordur. UserRepository öğesini birden fazla özellikte yeniden kullanmak istiyorsanız tekil örnek oluşturma düzenine uymanız gerekir. Singleton kalıbı, tüm testler aynı singleton örneğini paylaştığı için test etmeyi zorlaştırır.

Bağımlılıkları kapsayıcıyla yönetme

Nesneleri yeniden kullanma sorununu çözmek için bağımlılıkları almak üzere kullandığınız kendi dependencies container sınıfınızı oluşturabilirsiniz. Bu kapsayıcı tarafından sağlanan tüm örnekler herkese açık olabilir. Örnekte, yalnızca UserRepository örneğine ihtiyacınız olduğundan, bağımlılıklarını gizli yapabilirsiniz. İleride sağlanmaları gerekirse bunları herkese açık hale getirebilirsiniz:

// Container of objects shared across the whole app
class AppContainer {

    // apiService and userRepository aren't private and will be exposed
    val apiService = ApiService()
    val userRepository = UserRepository(apiService)
}

Bu bağımlılıklar uygulamanın tamamında kullanıldığından tüm etkinliklerin kullanabileceği ortak bir yere, yani Application sınıfına yerleştirilmesi gerekir. AppContainer örneği içeren özel bir Application sınıfı oluşturun.

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}

Oluşturma özelliğiyle aynı AppContainer, Application alt sınıfında oluşturulmaya devam eder. Bu işleme, setContent çağrılmadan önce etkinlikte veya LocalContext kullanılarak bir composable'ın içinden erişebilirsiniz:

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

        val appContainer = (application as MyApplication).appContainer

        setContent {
            LoginScreen(appContainer.userRepository)
        }
    }
}

// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
    val context = LocalContext.current
    val appContainer = (context.applicationContext as MyApplication).appContainer
    val viewModel: LoginViewModel = viewModel(
        factory = LoginViewModelFactory(appContainer.userRepository)
    )
    // ...
}

Bağımlılıkları, ağacın derinliklerinden LocalContext'a ulaşmak yerine composable parametreler olarak iletmenizi öneririz. Bu, composable işlevlerin test edilebilir olmasını ve girişlerinin açık olmasını sağlar. Kapsayıcıyı ekran kökünde bir kez çözümleyin ve gerekenleri aşağıya doğru iletin.

Bu şekilde, tekil UserRepository olmaz. Bunun yerine, grafikteki nesneleri içeren ve diğer sınıfların kullanabileceği bu nesnelerin örneklerini oluşturan, tüm etkinlikler arasında AppContainer paylaşılan bir alanınız vardır.

Uygulamada LoginViewModel öğesinin daha fazla yerde kullanılması gerekiyorsa LoginViewModel örneklerini oluşturacağınız merkezi bir yer olması mantıklıdır. LoginViewModel oluşturma işlemini kapsayıcıya taşıyabilir ve bu türden yeni nesneleri bir fabrika ile sağlayabilirsiniz. LoginViewModelFactory için kod şu şekilde görünür:

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
    fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory<LoginViewModel> {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

Compose ile AppContainer güncellemesi, fabrikayı yine de açığa çıkarır. Fabrika daha sonra viewModel composable'ı tarafından tüketilir. Böylece ViewModel, en yakın ViewModelStoreOwner (genellikle ana etkinlik veya Navigation Compose ile bir gezinme girişi) kapsamına alınır:

// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
    // ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)
    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val appContainer = (application as MyApplication).appContainer
        setContent {
            LoginScreen(appContainer.loginViewModelFactory)
        }
    }
}

@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
    val viewModel: LoginViewModel = viewModel(factory = factory)
    // ...
}

Bu yaklaşım, önceki yaklaşımdan daha iyidir ancak yine de göz önünde bulundurulması gereken bazı zorluklar vardır:

  1. Tüm bağımlılıklar için örnekleri manuel olarak oluşturarak AppContainer'ı kendiniz yönetmeniz gerekir.

  2. Hâlâ çok fazla ortak metin kodu var. Bir nesneyi yeniden kullanmak isteyip istemediğinize bağlı olarak fabrikaları veya parametreleri manuel olarak oluşturmanız gerekir.

Uygulama akışlarında bağımlılıkları yönetme

AppContainer projeye daha fazla işlev eklemek istediğinizde karmaşık hale gelir. Uygulamanız büyüdüğünde ve farklı özellik akışları kullanmaya başladığınızda daha da fazla sorun ortaya çıkar:

  1. Farklı akışlarınız olduğunda nesnelerin yalnızca söz konusu akışın kapsamında yer almasını isteyebilirsiniz. Örneğin, LoginUserData oluştururken (yalnızca giriş akışında kullanılan kullanıcı adı ve paroladan oluşabilir) farklı bir kullanıcının eski giriş akışındaki verileri kalıcı hale getirmek istemezsiniz. Her yeni akış için yeni bir örnek istiyorsanız. Bunu, bir sonraki kod örneğinde gösterildiği gibi AppContainer içinde FlowContainer nesneleri oluşturarak yapabilirsiniz.

  2. Uygulama grafiğini ve akış kapsayıcılarını optimize etmek de zor olabilir. Bulunduğunuz akışa bağlı olarak, ihtiyacınız olmayan örnekleri silmeyi unutmayın.

Örnek koda LoginContainer ekleyelim. Uygulamada LoginContainer öğesinin birden fazla örneğini oluşturmak istiyorsunuz. Bu nedenle, tekil bir öğe oluşturmak yerine, giriş akışının AppContainer öğesinden ihtiyaç duyduğu bağımlılıkları içeren bir sınıf oluşturun.

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// AppContainer contains LoginContainer now
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // LoginContainer will be null when the user is NOT in the login flow
    var loginContainer: LoginContainer? = null
}

Compose'da akış kapsayıcısının kullanım süresi, ana makine yerine kompozisyonla bağlantılıdır Activity. Composable'lar bağımlılıklarını parametre olarak aldığından veya bunları yükseltilmiş bir ViewModel'den okuduğundan paylaşılan bir AppContainer.loginContainer'yı değiştirmeniz gerekmez. İki seçeneğiniz vardır:

  1. Navigation Compose iç içe yerleştirilmiş grafik (çok ekranlı akışlar için tercih edilir). Giriş akışındaki tüm ekranları iç içe yerleştirilmiş bir gezinme grafiğinin altına yerleştirin ve kapsayıcıyı bu grafiğin NavBackStackEntry kapsamına alın. Kullanıcı akışa girdiğinde kapsayıcı oluşturulur ve geri yığın girişi çıkarıldığında temizlenir. Manuel yaşam döngüsü çağrıları gerekmez. Daha fazla bilgi için Gezinme grafiğinizi tasarlama başlıklı makaleyi inceleyin.
  2. remember ekran kökünde (tek ekranlı akış için veya Navigation Compose'u kullanmadığınızda). Kapsayıcıyı remember içinde oluşturun. Böylece, kompozisyona her girişte bir kez oluşturulur ve composable ayrıldığında çöp toplama işlemi yapılır:
@Composable
fun LoginFlow(appContainer: AppContainer) {
    val loginContainer = remember(appContainer) {
        LoginContainer(appContainer.userRepository)
    }
    val viewModel: LoginViewModel = viewModel(
        factory = loginContainer.loginViewModelFactory
    )
    // Render the login flow using loginContainer.loginData and viewModel.
}

Sonuç

Bağımlılık ekleme, ölçeklenebilir ve test edilebilir Android uygulamaları oluşturmak için iyi bir tekniktir. Uygulamanızın farklı bölümlerinde sınıf örneklerini paylaşmak ve fabrikaları kullanarak sınıf örnekleri oluşturmak için merkezi bir yer olarak kapsayıcıları kullanın.

Uygulamanız büyüdükçe çok fazla ortak metin kodu (ör. fabrikalar) yazdığınızı görmeye başlarsınız. Bu kodlar hataya açık olabilir. Ayrıca, bellek alanını boşaltmak için artık ihtiyaç duyulmayan kapsayıcıları optimize edip atarak kapsayıcıların kapsamını ve yaşam döngüsünü kendiniz yönetmeniz gerekir. Bu işlemi yanlış yapmak, uygulamanızda küçük hatalara ve bellek sızıntılarına yol açabilir.

Dagger bölümünde, bu süreci otomatikleştirmek ve aksi takdirde elle yazacağınız kodu oluşturmak için Dagger'ı nasıl kullanabileceğinizi öğreneceksiniz.

Ek kaynaklar

İçeriği görüntüleme