پیاده سازی معماری clean  و mvvm در کاتلین
پیاده سازی معماری clean و mvvm در کاتلین

در این مقاله به جزئیات نحوه اجرای این الگو در یک برنامه نمونه می پردازیم. برنامه نمونهRestaurantDeliveryنام دارد.که ازAPI DoorDashبرای داده ها و نمایش لیستی از رستوران های اطراف استفاده می کند.

 

معماری clean  چه چیزی به ما میدهد

  • نگهداری آسان کدها و خوانایی بالاتر کدها
  • درک مشترک برنامه نویسان در مورد چگونگی ساختن فیچر ها ، ساده تر کردن بررسی کد
  • کد های قابل تست. تزریق وابستگی به ما این امکان را میدهد که به راحتی باmock دیتا ها کدها را تست کنیم وunit test را بنویسیم

 

برای پیاده سازی معماریclean ما باید 3 ماژول اصلی را بسازیمdata وdomain 

 وapplication البته این ها ماژول های اصلی هستندو شمابر اساس نیاز میتوانید ماژول های دیگری را هم به پروژه اضافه کنید

 

 یک بررسی کلی از پروژه داشته باشیم

لایهData :در این لایهapiهاDatabaseوcache را پیاده سازی میکنیم.

لایهDomain : منطق پروژه در این لایه پیاده سازی میشود این لایهUseCaseها وModelهار را نگه داری میکند.

لایهApplication: تمام اجزای اندروید در این لایه پیاده سازی میشوندComponentها پترن ها (MVVM)  و...

 

 

پیاده سازی

هنگام پیاده سازی یک فیچر جدید بهتراست که برای شروع از لایهDataشروع کنیم بعد از آن لایهDomainودر آخر لایهApplicationدر این آموزش به همین شکل پیاده سازی را انجام میدهیم

 

لایهData

در این لایهapi هاReposotoryها کلاس هایdata(dataclassobject)و

mapperها را پیاده سازی کرده ایم

 

InterfaceApiرا پیاده سازی میکنیم

interface RestaurantEndpoint {

    @GET("restaurant/")
    fun getRestaurantList(@Query("lat") lat: Double, @Query("lng") lng: Double, @Query("offset") offset: Int, @Query("limit") limit: Int): Single<Response<List<RestaurantResponse>>>

    @GET("restaurant/{id}")
    fun getRestaurant(@Path("id") id: Int): Single<Response<RestaurantResponse>>

}

کلاسApi را به صورت زیر مینویسیم

class RestaurantApi @Inject constructor(private val restaurantEndpoint: RestaurantEndpoint) {

    fun getRestaurantList(lat: Double, lng: Double, offset: Int, limit: Int): Single<List<RestaurantResponse>> {
        return restaurantEndpoint.getRestaurantList(lat, lng, offset, limit)
            .mapResponse()
    }

    fun getRestaurant(id: Int): Single<RestaurantResponse> {
        return restaurantEndpoint.getRestaurant(id)
            .mapResponse()
    }

}

کلاسRestaurantApiفقط مسئول برقراری ارتباط با اینترفیسRestaurantEndpointو هندل کردن خطاهایresponseاست.mapResponse() یکKotlin extensionاست که خطا را هندل می کند یاresponse bodyرا برمی گرداند.

در گام بعدی به یکinterface  نیاز داریم که این داده هارا بازیابی کند پس ما لایهDomain را به لایهData ارتباط میدهیم.

ما با ایجاد یکinterface در لایهDomain وimplementآن در لایهDataاین کار را انجام میدهیم.

ابتدا در لایهDomainیکinterface را با نامRestaurantRepository میسازیم.

نکته: نامگذاری در اینجا لزوماً با کلاس هایapiوendpointیک به یک نیست.Repositoryمعمولاً بیشترgenericاست زیرا می تواند با چندینAPIارتباط برقرار کند. سعی کنید توابع موجود در هرRepositoryرا به طور مناسب برای برنامه خود گروه بندی کنید. مثال:AccountRepositoryمی تواند نام یکRepositoryما برای به دست آوردن داده های مربوط به حساب کاربری باشد.

interface RestaurantRepository {

    fun getRestaurantList(lat: Double, lng: Double, offset: Int, limit: Int): Single<List<Restaurant>>

    fun getRestaurant(id: Int): Single<Restaurant>

}

به لایهData برمیگردیم و اینRepository را پیاده سازی میکنیم

class RestaurantRepositoryImpl(
    private val restaurantApi: RestaurantApi,
    private val restaurantMapper: RestaurantMapper
) : RestaurantRepository {

    override fun getRestaurantList(lat: Double, lng: Double, offset: Int, limit: Int): Single<List<Restaurant>> {
        return restaurantApi.getRestaurantList(lat, lng, offset, limit)
            .map { restaurantMapper.map(it) }
    }

    override fun getRestaurant(id: Int): Single<Restaurant> {
        return restaurantApi.getRestaurant(id)
            .map { restaurantMapper.map(it) }
    }
}

این یک مثال ساده است اما ما در اینجا درحال بازیابی داده ها و سپسmapp کردن آن قبل از ارسال بهDomainهستیم. اگر ما بخواهیم که یک سری از داده ها به صورتlocalذخیره شوند اینRepository دارای کلاس هایDao یا هرآنچه که برای ارتباط باDBخود استفاده میکنید میباشد. در اینجا ما قبل از انتقال داده ها به یک شئdbمیپردازیم.

restaurantMapperیک کلاس ساده

با دو متد به نامmapاست که یکی از متد ها اطالاعات یک رستوران را برمیگرداند و دیگری لیستی از رستوران ها را برمگیرداند.

 

ادامه پیاده سازی لایهDoamin

این لایه منحصر به فرد است چون که یک کتابخانه جاوا خالص است و از وجود لایه های دیگر بی خبر است.این امر ذاتا قابل آزمایش است.

در این لایهModelهاUseCaseوRepository interface را پیاده سازی میکنیم.

در بالا ترRestaurantRepositoryرا در این لایه نوشتیم

حالا ما باید یکUseCaseایجاد کنیم که با آن صحبت کند.UseCaseیک کلاس تک منظوره است که یک متدpublic دارد این کلاس لیست رستوران ها را به ما برمیگرداند.

class GetRestaurantListUseCase @Inject constructor(private val restaurantRepository: RestaurantRepository) {

    sealed class Result {
        object Loading : Result()
        data class Success(val restaurants: List<Restaurant>) : Result()
        data class Failure(val throwable: Throwable) : Result()
    }

    fun execute(lat: Double, lng: Double, offset: Int, limit: Int): Observable<Result> {
        return restaurantRepository.getRestaurantList(lat, lng, offset, limit)
            .compose(validate())
            .compose(sort())
            .toObservable()
            .map { Success(it) as Result }
            .onErrorReturn { Failure(it) }
            .startWith(Loading)
    }
}

اینجا میبینیم که یک لیست میگیریم برخی اعتبار سنجی هارا انجام میدهیم و منطقی را بر اساس آن مرتب میکنیم وسپس آن را در یکresult برمیگردانیم.

اکنون داده هایمان همه قلاب شده است وUseCase های ما برای بازیابی این داده هادر دسترس هستند خب این داده ها را وارد لایهapplicationمیکنیم.

 

لایهApplication

برای این لایه فقطActivityوViewModelرا نشان میدهم نمیخواهم خیلی وارد جزئیات بشویم.

اینجاMainActivityما است

 

class MainActivity : BaseActivity() {

    @Inject lateinit var viewModel: MainViewModel
    private val disposables = CompositeDisposable()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, layout.activity_main)

        screenComponent.inject(this)

        binding.viewModel = viewModel
        viewModel.bound()
    }

    // onResume we need to subscribe to our viewModel actions
    override fun onResume() {
        super.onResume()
        viewModel.showErrorGettingRestaurants.observe()
            .subscribe {
                AlertDialog.Builder(this)
                    .setTitle(getString(string.error_title))
                    .setMessage(getString(string.restaurant_list_error_message))
                    .setNeutralButton(getString(string.ok)) { dialog, _ -> dialog.dismiss() }
            }.addTo(disposables)
    }

    // onPause we need to unsubscribe from viewModel actions since the view is now backgrounded
    override fun onPause() {
        disposables.clear()
        super.onPause()
    }

    override fun onDestroy() {
        viewModel.unbound()
        super.onDestroy()
    }
}

@BindingAdapter("adapter")
fun RecyclerView.setAdapter(viewModel: MainViewModel) {
    val adapter = RestaurantListAdapter(viewModel.restaurantList)
    adapter.onItemClickListener = { viewModel.onRestaurantClicked(it) }
    adapter.onImageClickedListener = { viewModel.onRestaurantImageClicked(it) }
    this.addItemDecoration(DividerItemDecoration(this.context, DividerItemDecoration.VERTICAL))
    this.layoutManager = LinearLayoutManager(this.context)
    this.adapter = adapter
}

چند نکته قابل ذکر است .. ماViewModelرا با استفاده ازDaggerتزریق می کنیم و به هرStickyActionsازViewModelگوش می دهیم تا نمایش را به روز کند.

StickyActionیکRxJava BehaviorSubject wrapperاست

این بهobserveاجازه میدهد تغییرات درViewModel را مشاهده کند.

StickyAction مقادیرnullرا هندل میکند و در صورتobserveمقادیر از آن ها استفاده میکند.

یکBehavaiorSubjectمعمولی برای مشاهده هر observerجدید آخرین مقدار را در خود حفظ میکند.

مورد دیگری که می خواهم ذکر کنم آداپتور اتصال دهنده است. من آداپتورهای اتصال خود را سبک و خاص نگه میدارم. این وظیفه برای تنظیم اولیه آداپتور و تنظیمobservable listاست. این آداپتور با تغییر لیست ، خود را به روز می کند. این باعث می شود هرگونه تماس به آداپتور ذخیره شود یا اصلاً نیاز به نگه داشتنreferenceداشته باشید. فقط لیست را به روز کنید وRecyclerViewنیز به روز می شود.
 

 

 بهViewModelمیرویم

class MainViewModel @Inject constructor(
    private val getRestaurantListUseCase: GetRestaurantListUseCase,
    private val mainRouter: MainRouter
) {

    val disposables = CompositeDisposable()
    val progressVisible = ObservableBoolean()
    val restaurantList = ObservableArrayList<Restaurant>()
    val showErrorGettingRestaurants = StickyAction<Boolean>()

    companion object {
        // Hardcoded LatLng for simplicity here
        const val LAT = 37.00
        const val LNG = -122.00
    }

    // Called onCreate. Retrieves the list of restaurants
    fun bound() {
        //offset and limit can be used for pagination in the future. This is static for now.
        getRestaurantListUseCase.execute(LAT, LNG, 0, 50)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { handleGetRestaurantListResult(it) }
            .addTo(disposables)
    }

    // Called onDestroy. Clean up method.
    fun unbound() {
        disposables.clear()
    }

    // Handles Result from getRestaurantListUseCase
    private fun handleGetRestaurantListResult(result: Result) {
        progressVisible.set(result == Result.Loading)
        when (result) {
            is Result.Success -> {
                restaurantList.addAll(result.restaurants)
            }
            is Result.Failure -> {
                showErrorGettingRestaurants.trigger(true)
            }
        }
    }

    // Shows restaurant detail screen based on restaurant clicked
    fun onRestaurantClicked(restaurant: Any) {
        mainRouter.navigate(MainRouter.Route.RESTAURANT_DETAIL, Bundle().apply {
            putInt(RestaurantDetailActivity.EXTRA_RESTAURANT_ID, (restaurant as Restaurant).id)
        })
    }

    // Shows image detail screen based on restaurant clicked
    fun onRestaurantImageClicked(restaurant: Any) {
        mainRouter.navigate(MainRouter.Route.IMAGE_DETAIL, Bundle().apply {
            putString(ImageDetailActivity.EXTRA_URL, (restaurant as Restaurant).coverImgUrl)
        })
    }
}

همانطور که میبینید ما داده هارا در تابعbound()میگیریم این تابع درoncreate()فراخوانی میشود.

وقتی لیست داده ها وارد می شوند ، ما با اضافه کردن آن به لیست ، آن را مدیریت خواهیم کرد. در اینجا ما همچنین برخی از رویدادهایonClickداریم که کاربر را به صفحه دیگری می برد.

اینجاست که روتر ما در دسترس است. روتر تمام منطقnavigationرا در خود جای داده است. این امر به سبک شدنViewوViewModelکمک می کند ، و همچنین باعث می شود منطقnavigationقابلtestباشد.

 

موارد دیگری هستند که قبلاً مورد بحث قرار ندادم ، مانند تنظیم داگر ، نوشتنunit testباMockito،Databindingو ... درمورد بعضی از این موارد آموزش هایی ارائه خواهیم داد.

 

سوال یا مشکلی داشتید در قسمت کامنت ها بنویسید تا به آن ها پاسخ بدهم.

 

جدیدترین ویدئوهای آموزشی

در بخش TV باگتو، آموزش های کوتاه و جدید را مشاهده نمایید

0 نظرات

برای ارسال نظر باید وارد حساب کاربری خود شوید
ورود به حساب کاربری ثبت نام

بیش از 50% تخفیف به مناسبت جمعه سیاه
فقط تا پایان امروز