Android Viper Mimarisi nedir?

Abdullah Özerol
FLO Teknoloji

--

Merhabalar, bu yazımda Android yazılım tasarım desenlerinden olan “VIPER tasarım deseni nedir?”, “Bize ne gibi avantajlar sunar?”, “Dezavantajları var mıdır?” gibi sorulara yanıt bulacağız. VIPER mimarisinin yapısını anlamaya çalışacağız.

VIPER adını View, Interactor, Presenter, Entity ve Router ’ın ilk harflerinden alan bir yazılım mimarisidir.

VIPER mimarisinin avantajları nelerdir?

  • VIPER paterni diğer patenlerden farklı olarak uygulama hayat döngüsüne uygun bütün mimariyi içeren çok iyi tasarlanmış bir paterndir. Geliştiricilere çok temiz bir mimari sunar. Herhangi bir hata bulunduğunda ilgili katmanda düzeltilmesi kolaylıkla yapılabilir. Ayrıca birçok geliştirici farklı katmanlarda birbirinden bağımsız çalışma imkanı bulur.
  • Kodun test edilebilirliğini ve tekrar kullanılabilirliğini sağlar.
  • Rollerine göre uygulamayı komponentlere böler ve ilgili kodların birbirinden bağımsız ayrılmasını sağlar.
  • Yeni özelliklerin eklenmesini kolaylaştırır.
  • UI logic, Business logicden ayrı yazıldığı için otomated testlerin yazımını kolaylaştırır.
  • Modüllerin daha kolay değiştirilebilmesi ve test edilebilirlik kolaylığı açısından ve az conflict çıkmasından dolayı büyük projelerde ekip çalışmasında kullanılması daha kolaydır.
  • Dosyalar daha küçük ve net olduğundan esnekliği açısından ve yeni özelliklerin eklenmesi basit olduğundan geliştiricilere kullanım kolaylığı sağlar.

VIPER mimarisinin dezavantajları nelerdir?

  • Küçük projelerde karmaşıklığı artırır.
  • Tüm ekibin paterni iyi bilmesi ve ona uygun kod yazması gerekir.
  • Projeye yeni başlayanlarda paterne uygun kod yazması için öğrenme süreci gerektirir.

VIPER mimarisini daha iyi anlayabilmek adına katmanlarından söz edelim.

VIPER Katmanları

VIPER ve diğer Clean Architecture modellerinin arkasındaki temel fikir, uygulamanızın bağımlılıklarını izole etmek ve uygulamanız içindeki veri akışını iyileştirmek için daha temiz ve daha modüler bir yapı oluşturmaktır.

VIPER mimarisinde katmanları bir evin birbirinden farklı odaları gibi düşünebiliriz. Her odanın kendine özgü bir işlevi vardır ve diğer odalarla bağlantı içinde olur. Bu odalar birbiri ile olan bağlantılarını giriş ve çıkış işlemleri olarak yaparlar ve hangi odada hangi işlemlerin yapılacağı giriş ve çıkış protokolleri ile belirlenmiş olur.

View, Interactor, Presenter, Entity ve Route katmanları:

  • View Nedir?

Her ikisi de kullanıcı için görüntüleri ve kullanıcı etkileşimini algılar. Kendi başına pek bir şey yapmaz. Sorumluluğu, olayları Presenter modülüne iletmek ve UI öğelerini göstermektir. Presenter, View modülünün iletişim kurduğu tek modüldür.

  • Interactor Nedir?

Business logic işlemlerinin yapıldığı kısımdır ve uygulamanın omurgasını oluşturur. Verileri alır ve depolar. Örneğin uygulamamızda API çağrımı yaparken genellikle bu katmandan yaparız. Bu katmanda yapılan işlemler tamamen UI dan bağımsız olmalıdır.

  • Presenter Nedir?

Presenter esas olarak View ile ilgili logic’i içeren kodu içerir. User işlemlerine göre Interactor’ dan data alır ve bunu gösterilmek üzere View katmanına iletir. View ile Interactor arasında bir köprü görevi görür. Bu katmanda View ile ilgili veya uygulamanın business kurallarıyla ilgili kod bulunmamalıdır.

  • Entity Nedir?

VIPER içerisindeki en küçük elementtir. Interactor tarafından kullanılan model nesnelerini içerir. Entity’lerin sadece Interactor tarafından kullanılması çok önemlidir. Interactor asla presenter layer’a entity modellerini göndermez.

  • Router Nedir?

Hangi ekranların ne zaman gösterileceğini belirlendiği uygulama geçiş akışın bulunduğu katmandır. Ayrıca geçiş animasyonları da bu katmanda bulunur. Router yalnızca Presenter ile iletişime geçer.

Biraz daha detaylara inelim.

Uygulama Modülleri ve Entityler

Her modül bir Contract’tan ve çeşitli VIPER katmanlarını uygulayan birkaç ilişkili sınıftan oluşur. Contract, modülde hangi VIPER katmanlarının uygulanması gerektiğini ve katmanların gerçekleştireceği eylemleri açıklar.

@ActivityScope
@Component(modules = [MainModule::class], dependencies = [AppComponent::class])
interface MainComponent {

fun inject(target: MainActivity)

@Component.Builder
interface Builder {
@BindsInstance
fun activity(activity: MainActivity): Builder

fun appComponent(component: AppComponent): Builder

fun plus(module: MainModule): Builder

fun build(): MainComponent
}
}
@Module
class MainModule {

@Provides
@ActivityScope
fun api(retrofit: Retrofit) = retrofit.create(MainApi::class.java)

@Provides
@ActivityScope
fun repository(api: MainApi) = MainRepo(api)


@Provides
@ActivityScope
fun router(activity: MainActivity): MainContract.Router = MainRouter(activity)

@Provides
@ActivityScope
fun presenter(router: MainContract.Router, interactor: MainInteractor) = MainPresenter(router, interactor)

@Provides
@ActivityScope
fun interactor(repository: MainRepo) = MainInteractor(repository)
}

Main Modül’üne karşılık gelen Contract’a hızlıca göz atalım:

interface MainContract {
interface View {
fun showLoading()
fun hideLoading()
fun publishData(data: List<Joke>)
fun showMessage(msg: String)
}

interface Presenter {

fun bindView(view: MainContract.View)

fun unbindView()

fun onViewCreated()

fun onItemClicked(joke: Joke)

fun onBackClicked()
}

interface Interactor {
fun getJokes(onSuccess: (List<Joke>) -> Unit, onError: (Throwable) -> Unit)
}

interface Router {
fun finish()
fun openFullJoke(joke: Joke)
}

interface Repo {
fun getJokes(): Single<List<Joke>>
}
}

Interface olarak tanımlanan ve uygulanması gereken dört katman olduğunu görebilirsiniz: View, Router, presenter ve interactor. Interface içinde bildirilen işlevler kodun bir noktasında tanımlanacaktır.

View:

Sadece Presenter ile iletişim kuran ve UI öğelerini gösteren View katmanında, MainActivity’de MainContract.View implement edilir. Presenter için uygulama yaşam döngüsü methodları , “showLoading()” ve “hideLoading()” gibi MainContract.View Override methodları eklenir.

class MainActivity : AppCompatActivity(), MainContract.View {

companion object {
fun launch(context: Context) {
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
}
}

@Inject
lateinit var presenter: MainPresenter

val component: MainComponent by lazy {
DaggerMainComponent.builder()
.appComponent((application as App).component)
.activity(this)
.plus(MainModule())
.build()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

initView()
component.inject(this)
presenter.bindView(this)
presenter.onViewCreated()
}

override fun onDestroy() {
super.onDestroy()
presenter.unbindView()
}

override fun showLoading() {
recyclerView.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}

override fun hideLoading() {
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
}

override fun publishData(data: List<Joke>) {
val adapter = MainAdapter(data, object : MainAdapter.JokeListener {
override fun onItemClick(joke: Joke) {
presenter.onItemClicked(joke)
}
})
recyclerView.adapter = adapter
}

override fun showMessage(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

private fun initView() {
val manager = LinearLayoutManager(this).apply { orientation = LinearLayoutManager.VERTICAL }
recyclerView.layoutManager = manager
toolbar.setTitle(R.string.main_title)
toolbar.setTitleTextColor(ContextCompat.getColor(this, android.R.color.white))
toolbar.setNavigationOnClickListener { presenter.onBackClicked() }
}
}

Presenter:

View ile ilgili logici içeren kodu içerisinde barındırır. Kullanıcı işlemlerini baz alarak Interactor’dan data alır ve bunu gösterilmek adına View katmanı ile iletişim kuran tek katmandır. View ile Interactor arasındaki bağlantıyı sağlayan katmandır. Bu katmanda View ile ilgili veya uygulamanın business kurallarıyla ilgili kod bulunmamalıdır.

MainPresenter aşağıdaki gibi tanımlanır ve oluşturulan Presenter package’ine yerleştirilir.

class MainPresenter(private val router: MainContract.Router, private val interactor: MainInteractor) :
MainContract.Presenter {

private var view: MainContract.View? = null

override fun bindView(view: MainContract.View) {
this.view = view
}

override fun unbindView() {
view = null
interactor.dispose()
}

override fun onViewCreated() {
view?.showLoading()
interactor.getJokes({
view?.hideLoading()
view?.publishData(it)
}, this::onError)
}

override fun onItemClicked(joke: Joke) {
router.openFullJoke(joke)
}

override fun onBackClicked() {
router.finish()
}

private fun onError(error: Throwable) {
view?.hideLoading()
error.message?.let { view?.showMessage(it) }
}
}

Interactor:

Uygulamanın Bussines Logic olarak adlandırdığımız kısmıdır. Burada UI işlemleri yapılmaz. Fetch, Update gibi işlemler burada gerçekleşir. MVVM tasarım desenindeki VM görevini görür.

Sunucudaki onViewCreated() için, View yüklendikten sonra uzak veri kaynağından verileri sorgulamak için öncelikle Interactor Package’ine MainInteractor sınıfını oluşturmanız gerekir.

class MainInteractor(private val repo: MainRepo) : MainContract.Interactor {

private val compositeDisposable = CompositeDisposable()

override fun getJokes(onSuccess: (List<Joke>) -> Unit, onError: (Throwable) -> Unit) {
val disposable = repo.getJokes()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(onError)
.doOnSuccess(onSuccess)
.subscribe({ value -> Log.d("TAG", "onNext $value") },
{ e -> Log.d("TAG", "onError ${e.localizedMessage}") })

compositeDisposable.add(disposable)
}

fun dispose() = compositeDisposable.dispose()
}

MainPresenter’da, MainInteractor’da oluşturulan bu method çağırılır.

override fun onViewCreated() {
view?.showLoading()
interactor.getJokes({
view?.hideLoading()
view?.publishData(it)
}, this::onError)
}

Router:

Daha önce belirtildiği gibi Router, Viewler arasında gezinmeden sorumlu VIPER katmanıdır. Başka bir deyişle, Router uygulamada var olan her View’ın farkındadır ve bir View’den diğerine veya tam tersi yönde gezinmek için araçlara ve kaynaklara sahiptir.

Bunu her modül için akılda tutarak, Presenter’ı (modülün beyni) ve ardından diğer varlıkları (View, Interactor ve Entity) oluşturmak mantıklı olacaktır. Son olarak; Router, Viewleri tamamlamalı ve bir şekilde Router’ın navigasyonu yönetmesine izin vermelidir.

Bu katman uygulamanın sayfalarının ne zaman gösterileceğini belirlememizi sağlayan katmandır.

class MainRouter(private val activity: MainActivity) : MainContract.Router {
override fun finish() {
activity.finish()
}

override fun openFullJoke(data: Joke) {
DetailActivity.launch(activity, data)
}
}

Entity:

Uygulamanın Model kısmıdır. Uygulama ile ilgili Data modeller burada bulunur. Bu kısım sadece Interactor ile iş birliği yapar. Diğer kısımlarda bulunmamalıdır.

Interactor tarafından kullanılan model nesnelerini içerdiğini belirtmiştik.

@Parcelize
data class Joke(
@SerializedName("site") var site: String? = null,
@SerializedName("name") var name: String? = null,
@SerializedName("desc") var desc: String? = null,
@SerializedName("link") var link: String? = null,
@SerializedName("elementPureHtml") var elementPureHtml: String? = null
) : Parcelable

Bu yazımda Android için VIPER mimarisi adına ufak bir bilgilendirme yapmak istedim. VIPER mimarisinin avantajları ve dezavantajlarının yanında katman yapısı hakkında ufak örnekler vermeye çalıştım.

Vakit ayırdığınız için teşekkürler.

--

--