در این مقاله به جزئیات نحوه اجرای این الگو در یک برنامه نمونه می پردازیم. برنامه نمونه 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 و ... درمورد بعضی از این موارد آموزش هایی ارائه خواهیم داد.
سوال یا مشکلی داشتید در قسمت کامنت ها بنویسید تا به آن ها پاسخ بدهم.
برای افزودن دیدگاه خود، نیاز است ابتدا وارد حساب کاربریتان شوید