vouria
vouria
خواندن ۱۴ دقیقه·۱ سال پیش

کاتلین با ChatGPT (پروژه محور)

https://poe.com/e.com
https://poe.com/e.com



من:
بهترین استراتژی در پروژه های کاتلین با معماری کلین و mvvm برای هندل کردن ریسپانس های سرور و دیتابیس و ... چیست
یه مثال خوب و جامع که بتونه خطا ها و پاسخ های سرور رو به خوبی هندل کنه
که با رتروفیت انجام میشه و دیتابیس که با room هست
که بنابر معماری mvvm دارای ریپازیتوری و ویومدل و ویو هست و لایودیتا و کوروتین هم هست
و در هنگام پردازش و انتظار از لودینگ استفاده میشه


sage:

برای پروژه‌های کاتلین با معماری Clean و MVVM که از Retrofit و Room استفاده می‌کنند، بهترین استراتژی برای هندل کردن ریسپانس‌های سرور و دیتابیس، استفاده از ریپوزیتوری‌ها می‌باشد. در این روش، وظیفه‌ی رفتاری به ریپوزیتوری‌ها داده می‌شود و وظیفه‌ی اصلی ویومدل‌ها، ارائه‌ی داده‌ها به ویو است.

در این روش، ابتدا یک ریپوزیتوری برای هر منبع داده (مثلاً یک ریپوزیتوری برای سرویس و یک ریپوزیتوری برای دیتابیس) ایجاد می‌شود. سپس در ویومدل‌ها، بهکمک از ریپوزیتوری‌ها، داده‌های مورد نیاز برای نمایش به ویو گرفته می‌شود. با استفاده از لایودیتا و کوروتین، امکان برقراری ارتباط با منابع داده‌ای را داریم و می‌توانیم به صورت غیر همزمان داده‌ها را از منابع مختلف گرفته و به ویو ارائه دهیم.

همچنین، برای هندل کردن خطاها و پاسخ‌های سرور، بهتر است از مدل دیتایی استفاده کرده و خطاها و پاسخ‌های سرور را در آن مدل تعریف کنید. سپس در ریپوزیتوری، با استفاده از try-catch، خطاها را هندل کرده و پاسخ‌هایسرور را بازگردانده و در ویومدل‌ها به صورت مناسب استفاده کنید.

یک مثال خوب و جامع برای هندل کردن ریسپانس‌های سرور و دیتابیس در پروژه‌های کاتلین با معماری Clean و MVVM، به صورت زیر می‌تواند باشد:

  • در پکیج repository، برای هر منبع داده یک ریپوزیتوری ایجاد می‌کنیم. به عنوان مثال، یک ریپوزیتوری برای سرویس و یک ریپوزیتوری برای دیتابیس.
  • در هر ریپوزیتوری، مدل دیتایی برای پاسخ‌های سرور و دیتابیس تعریف می‌کنیم. به عنوان مثال، برای پاسخ‌های سرور مدلی با نام ServerResponseرا تعریف می‌کنیم که شامل فیلدهایی مثل status و message و data است.
  • در هر ریپوزیتوری، یک متد برای دریافت داده‌ها از منبع مربوطه تعریف می‌کنیم. به عنوان مثال، برای ریپوزیتوری سرویس، یک متد با نام getUsers تعریف می‌کنیم که با استفاده از Retrofit و کوروتین، اطلاعات کاربران را از سرور دریافت کرده و در یک شی از نوع ServerResponse<List<User>> قرار می‌دهد. سپس این شی را بازگردانده و در ویومدل استفاده می‌کنیم.
  • در ویومدل، با استفاده از لایودیتا، داده‌های مورد نیاز را از ریپوزیتوری‌ها گرفته و به ویو ارائه می‌دهیم. همچنین، برای هندل کردن خطاها و پاسخ‌های سرور، از مدل دیتایی استفاده می‌کنیم. به عنوان مثال، یک مدل دیتایی برای پاسخ‌های سرور با نام ServerResponse را تعریف کرده و در ویومدل از آن استفاده می‌کنیم. در صورتی که دریافت پاسخی از سرور با خطا مواجه شد، در ریپوزیتوری با استفاده از try-catch، خطا را هندل کرده و پاسخ‌های سرور را بازگردانده و در ویومدل به صورت مناسب استفاده می‌کنیم.

در کل، استفاده از ریپوزیتوری‌ها و مدل دیتایی بهر منبع داده، از جمله سرور و دیتابیس، به همراه استفاده از لایودیتا و کوروتین و هندل کردن خطاها به صورت جداگانه و در ریپوزیتوری‌ها و ویومدل‌ها، بهبود قابل توجهی در خوانایی کد و سهولت در مدیریت منابع داده‌ای و پاسخ‌های سرور و دیتابیس ایجاد می‌کند. به عنوان مثال، می‌توانید در ریپوزیتوری سرویس، با استفاده از Retrofit و کوروتین، داده‌های سرور را دریافت کنید و در مدل دیتایی ServerResponse آنها را ذخیره کنید. سپس در ویومدل، با استفاده از لایودیتا، داده‌های مورد نیاز را از ریپوزیتوری‌ها گرفته و به ویو ارائه دهید. در صورتی که دریافت پاسخی از سرور با خطا مواجه شد، در ریپوزیتوری با استفاده از try-catch، خطا را هندل کرده و پاسخ‌های سرور را بازگردانده و در مدل دیتایی ServerResponse ذخیره کنید. در ویومدل نیز، با استفاده از مدل دیتایی ServerResponse، خطاها را هندل کرده و پاسخ‌های سرور را به صورت مناسب به ویو ارائه دهید. همچنین، در هنگام پردازش و انتظار از لودینگ نیز می‌ت
...



من:

حالا همین مثال رو با کد توضیح بده و از سیلد کلاس هم استفاده کن
کد ها بر اساس یه سناریو ساده و به هم مرتبط باشند که قابل اجرا باشند
یه جوری پیاده سازی کن که پروژه های بزرگ هم بتونه ساپورت کنه و جامع باشه و قوانین سالید هم تا اندازه ای اجرا بشه


sage:

من می‌توانم یک مثال ساده از چگونگی استفاده از معماری Clean و MVVM با Retrofit و Room برای هندل کردن داده‌ها و خطاها ارائه دهم. در این مثال، ما یک لیست کاربران را از یک سرویس web دریافت کرده و آنها را در یک دیتابیس Room ذخیره می‌کنیم. سپس با استفاده از ViewModel و LiveData، داده‌های کاربران را به یک RecyclerView نمایش می‌دهیم. همچنین، در صورتی که با خطا مواجه شدیم، آن را هندل می‌کنیم و پیام خطا را به کاربر نمایش می‌دهیم.


در اینجا از سیلد کلاس هم استفاده می‌کنیم تا قوانین SOLID را رعایکد های زیر، یک مثال ساده از چگونگی استفاده از معماری Clean و MVVM با Retrofit و Room برای هندل کردن داده‌ها و خطاها را نشان می‌دهد:


در سیلد کلاس، از Dependency Inversion Principle (DIP) و Dependency Injection (DI) استفاده می‌کنیم. به عنوان مثال، کلاس UserRepository از Data Source هایی مثل RemoteDataSource و LocalDataSource وابسته است، اما به جای تزریق این Data Source ها به UserRepository، ما از یک Interface با نام UserRepositoryContract استفاده می‌کنیم و سپس این Interface را توسط کلاس UserRepositoryImpl پیاده‌سازی می‌کنیم. به این ترتیب، ما می‌توانیم به راحتی Data Source های مختلف را به UserRepository اضافه کنیم.


در این مثال، چندین پکیج وجود ددر اینجا، من یک مثال ساده از چگونگی استفاده از معماری Clean و MVVM با Retrofit و Room برای هندل کردن داده‌ها و خطاها ارائه می‌دهم. این مثال بر اساس یک سناریو ساده و به هم مرتبط باشد و قابلیت اجرا را داشته باشد. همچنین، من از سیلد کلاس استفاده می‌کنم تا قوانین SOLID را رعایت کنم و یک کد جامع و قابل ساپورت برای پروژه‌های بزرگ ایجاد کنم.


در این مثال، ما از Retrofit برای ارتباط با سرویس web استفاده می‌کنیم و از Room برای ذخیره کردن داده‌ها در دیتابیس استفاده می‌کنیم. هما یک لایه Data داریم که شامل سه بخش است: Remote Data Source، Local Data Source و Repository.


Remote Data Source با استفاده از Retrofit اطلاعات را از سرویس web دریافت می‌کند و آنها را به Repository ارسال می‌کند.


Local Data Source از Room برای ذخیره کردن اطلاعات در دیتابیس استفاده می‌کند. این لایه به Repository اجازه می‌دهد که داده‌هایی را که قبلاً دریافت کرده‌ایم، به صورت محلی در دسترس داشته باشیم.


Repository لایه ای است که داده‌های دریافتی از Remote Data Source و Local Data Source را با هم ترکیب کرده و به ViewModel ارسال می‌کند. همچنین، در صورتی که با خطا مواجه شدیم، این لایه آن را هندل می‌کند و پیام خطا را به ViewModel ارسال می‌کند.


در لایه ViewModel، ما از LiveData برای نمایش داده‌های دریافت شده استفاده می‌کنیم. این لایه همچنین مسئول هندل کردن خطاها و نمایش پیام خطا به کاربر است.


در ادامه، کد هر یک از این لایه‌ها را بررسی می‌کنیم:


1. Remote Data Source:

این لایه برای دریافت اطلاعات از سرویس web از Retrofit استفاده می‌کند. ابتدا، یک Interface با نام UserService ایجاد می‌کنیم:

interface UserService {
@GET(&quotusers&quot)
suspend fun getUsers(): List<UserResponse>
}

سپس، یک کلاس با نام RemoteDataSource ایجاد می‌کنیم که UserService را تزریق می‌کنیم و با استفاده از این Interface، اطلاعات را از سرویس web دریافت می‌کنیم:

class RemoteDataSource(private val userService: UserService) {
suspend fun getUsers(): Result<List<UserResponse>> {
return try {
val response = userService.getUsers()
Result.Success(response)
} catch (e: Exception) {
Result.Error(e)
}
}
}

در اینجا، از یک sealed class به نام Result استفاده می‌کنیم که دو حالت Success و Error را دارد. ما از این کلاس برای برگرداندن اطلاعات به Repository و همچنین هندل کردن خطاها استفاده می‌کنیم.


2. Local Data Source:

در این لایه، از Room برای ذخیره کردن داده‌ها در دیتابیس استفاده می‌کنیم. ابتدا، یک Entity با نام UserEntity ایجاد می‌کنیم:

@Entity(tableName = &quotusers&quot)
data class UserEntity(
@PrimaryKey valid: Int,
val name: String,
val email: String,
val phone: String
)

سپس، یک DAO با نام UserDao ایجاد می‌کنیم که از این Entity برای ایجاد جدول در دیتابیس استفاده می‌کنیم. در اینجا، ما یک درخواست SELECT به نام getUsers ایجاد می‌کنیم تا اطلاعات کاربران را از دیتابیس دریافت کنیم:

@Dao
interface UserDao {
@Query(&quotSELECT * FROM users&quot)
suspend fun getUsers(): List<UserEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveUsers(users: List<UserEntity>)
}


سپس، یک کلاس با نام LocalDataSource ایجاد می‌کنیم که UserDao را تزریق می‌کنیم

از این DAO، اطلاعات کاربران را از دیتابیس دریافت می‌کنیم و همچنین آنها را به دیتابیس ذخیره می‌کنیم:


class LocalDataSource(private val userDao: UserDao) {
suspend fun getUsers(): Result<List<UserEntity>> {
return try {
val users = userDao.getUsers()
Result.Success(users)
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun saveUsers(users: List<UserEntity>) {
userDao.saveUsers(users)
}
}


3. Repository:

در این لایه، داده‌های دریافتی از Remote Data Source و Local Data Source را با هم ترکیب کرده و به ViewModel ارسال می‌کنیم. همچنین، در صورتی که با خطا مواجه شدیم، آن را هندل می‌کنیم و پیام خطا را به ViewModel ارسال می‌کنیم. ابتدا، یک Interface با نام UserRepositoryContract ایجاد مکنیم:


interface UserRepositoryContract {
suspend fun getUsers(): Result<List<User>>
}


سپس، یک کلاس با نام UserRepositoryImpl ایجاد می‌کنیم که این Interface را پیاده‌سازی می‌کند. در اینجا، Remote Data Source و Local Data Source را تزریق می‌کنیم و با استفاده از آنها، اطلاعات را از سرویس web دریافت کرده و در دیتابیس ذخیره می‌کنیم. سپس، اطلاعات را از دیتابیس و سرویس web دریافت کرده و با هم ترکیب می‌کنیم:


class UserRepositoryImpl(
private val remoteDataSource: RemoteDataSource,
private val localDataSource: LocalDataSource
) : UserRepositoryContract {
override suspend fun getUsers(): Result<List<User>> {
val localUsers = localDataSource.getUsers()
return if (localUsers is Result.Success && localUsers.data.isNotEmpty()) {
localUsers
} else {
when (val remoteUsers = remoteDataSource.getUsers()) {
is Result.Success -> {
val users = remoteUsers.data.map { it.toUser() }
localDataSource.saveUsers(users.map { it.toUserEntity() })
Result.Success(users)
}
is Result.Error -> {
if (localUsers is Result.Success) {
localUsers
} else {
remoteUsers
}
}
}
}
}
}

در اینجا، از دو Extension Function برای تبدیل اطلاعات استفاده می‌کنیم. تابع toUser برای تبدیل اطلاعات از UserResponse به User و تابع toUserEntity برای تبدیل اطلاعات از User به UserEntity استفاده می‌شود.


4. ViewModel:

در این لایه، از ViewModel و LiveData برای نمایش داده‌های دریافت شده استفاده می‌کنیم. همچنین، مسئول هندل کردن خطاها و نمایش پیام خطا به کاربر است. ابتدا، یک ViewModel با نام MainViewModel ایجاد می‌کنیم:


class MainViewModel(
private val userRepository: UserRepositoryContract
) : ViewModel() {
private val _usersLiveData = MutableLiveData<Result<List<User>>>()
val usersLiveData: LiveData<Result<List<User>>>
get() = _usersLiveData
fun getUsers() {
viewModelScope.launch {
_usersLiveData.value = userRepository.getUsers()
}
}
}

در اینجا، ابتدا یک LiveData با نام usersLiveData ایجاد می‌کنیم که اطلاعات کاربران را به RecyclerView منتقل می‌کند. همچنین، یک ViewModelScope برای اجرای کدهای suspend استفاده می‌کنیم. در تابع getUsers، داده‌های کاربران را از Repository دریافت می‌کنیم و آنها را در LiveData قرار می‌دهیم.


در ادامه، در Activity یا Fragment خود، باید این ViewModel را ایجاد کرده و آن را به RecyclerView وصل کنید. به عنوان مثال، در یک Fragment، می‌توانیم به شکل زیر این کار راانجام دهیم:

class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private lateinit var viewModel: MainViewModel
private lateinit var adapter: UserListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, MainViewModelFactory()).get(MainViewModel::class.java)
adapter = UserListAdapter()
binding.recyclerView.adapter = adapter
viewModel.usersLiveData.observe(viewLifecycleOwner, { result ->
when (result) {
is Result.Success -> {
adapter.submitList(result.data)
}
is Result.Error -> {
Toast.makeText(requireContext(), result.exception.message, Toast.LENGTH_SHORT).show()
}
}
})
viewModel.getUsers()
}
}

در اینجا، ابتدا یک ViewModel و یک Adapter برای RecyclerView ایجاد می‌کنیم. سپس، در onViewCreated، ViewModel را ایجاد کرده و آن را به RecyclerView منتقل می‌کنیم. در نهایت، با استفاده ازobserve، LiveData را برای دریافت داده‌های کاربران بررسی می‌کنیم. در صورتی که اطلاعات با موفقیت دریافت شده باشد، آن را به Adapter منتقل کرده و در RecyclerView نمایش می‌دهیم. در صورتی که با خطا مواجه شدیم، پیام خطا را در قالب یک Toast به کاربر نمایش می‌دهیم.


در نهایت، باید یک MainViewModelFactory ایجاد کرده و آن را به ViewModelProvider ارسال کنیم. این کار به صورت زیر انجام می‌شود:

class MainViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
val remoteDataSource = RemoteDataSource(ApiService.create())
val localDataSource = LocalDataSource(AppDatabase.getInstance().userDao())
val repository = UserRepositoryImpl(remoteDataSource, localDataSource)
return MainViewModel(repository) as T
}
throwIllegalArgumentException(&quotUnknown ViewModel class&quot)
}
}

در اینجا، ابتدا Remote Data Source و Local Data Source را ایجاد کرده و سپس این دو را به Repository ارسال می‌کنیم. در نهایت، با استفاده از Repository، ViewModel را ایجاد کرده و آن را به عنوان خروجی تابع برمی‌گردانیم.



من:
خیلی خوب توضیح دادی اگه امکانش هست بازم ادامه بده و کامل کن



sage:


5. Adapter:


در این لایه، از RecyclerViewAdapter برای نمایش داده‌های دریافت شده به کاربر استفاده می‌کنیم. ابتدا، یک Adapter با نام UserListAdapter ایجاد می‌کنیم:


class UserListAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.user_item, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position)
holder.bind(user)
}
}


در اینجا، برای ایجاد ViewHolder از Layout فایل user_item استفاده می‌کنیم. همچنین در تابع onBindViewHolder، اطلاعات هر کاربر را به ViewHolder منتقل می‌کنیم.


6. ViewHolder:


در این لایه، از ViewHolder برای نمایش داده‌های هر کاربر استفاده می‌کنیم. ابتدا، یک ViewHolder بانام UserViewHolder ایجاد می‌کنیم:


class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = UserItemBinding.bind(itemView)
fun bind(user: User) {
binding.nameTextView.text = user.name
binding.emailTextView.text = user.email
}
}

در اینجا، با استفاده از ViewBinding، اطلاعات هر کاربر را به View های مربوطه منتقل می‌کنیم.


7. DiffCallback:


در این لایه، از DiffUtil برای پیدا کردن تفاوت‌ها بین داده‌ها استفاده می‌کنیم. برای این منظور، یک DiffCallback با نام UserDiffCallback ایجاد می‌کنیم:


class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}


در اینجا، تابع areItemsTheSame برای این است که بررسی کند که آیا دو آیتم با هم یکسان هستند یا نه. تابع areContentsTheSame برای بررسی تفاوت‌های محتوایی بین دو آیتم استفاده می‌شود.


با اتمام این مراحل، ما یک برنامه کامل و قابل استفاده برای دریافت اطلاعات کاربران از یک سرویس web و نمایش آنها در یک RecyclerView داریم. در این برنامه، از معماری MVVM و Clean Architecture استفاده شده است که اجزای مختلف برنامه را به صورت جداگانه پیاده سازی کرده و از هم جدا کرده است. این کار، باعث افزایش قابلیت اطمینان، قابلیت تست و قابلیت گسترشمی‌شود. همچنین، با استفاده از این معماری، ما قابلیت ایجاد و توسعه برنامه‌های پایدار و قابل نگهداری را داریم.


من:
چیزی جا نمونه همه رو بگو اگه بازم هست ادامه بده


sage:

بله، در ادامه می‌توانیم یک نمونه از سرویس web و پاسخ آن را برای دریافت اطلاعات کاربران ایجاد کنیم. برای این منظور، ابتدا یک Interface با نام ApiService ایجاد می‌کنیم:


interface ApiService {
@GET(&quotusers&quot)
suspend fun getUsers(): List<UserResponse>
companion object {
private const val BASE_URL = &quothttps://jsonplaceholder.typicode.com/&quot
fun create(): ApiService {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
}
}


در اینجا، از Retrofit برای ارتباط با سرویس web استفاده شده است. با استفاده از این کلاس، اطلاعات کاربران را از سرویس web دریافت کرده و به صورت یک لیست از UserResponse ارسال می‌کنیم.

سپس، یک data class با نام UserResponse ایجاد می‌کنیم:


data class UserResponse(
val id: Int,
val name: String,
val email: String
)


در اینجا، اطلاعات کاربران را با نام، آدرس ایمیل و شناسه کاربری نگهداری می‌کنیم.


حال می‌توانیم از ApiService برای دریافت اطلاعات کاربران استفاده کنیم. برای این منظور، ابتدا در ViewModel یک تابع با نام getUsers را ایجاد می‌کنیم:


class UserViewModel(private val apiService: ApiService) : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
fun getUsers() {
viewModelScope.launch {
val userResponses = apiService.getUsers()
val users = userResponses.map { User(it.id, it.name, it.email) }
_users.value = users
}
}
}


در اینجا، با استفاده از متد getUsers در ApiService، اطلاعات کاربران را از سرویس web دریافت کرده و سپس با استفاده از map، اطلاعات را به شیء User تبدیل می‌کنیم و در LiveData قرار می‌دهیم. همچنین، این تابع در CoroutineScope ViewModel اجرا می‌شود تا درخواست به صورت asynchronous انجام شود و برنامه را مسدود نکند.


سپس، در Activity یا Fragment مربوطه، ViewModel را ایجاد کرده و تابع getUsers را فراخوانی می‌کنیم. همچنین، به RecyclerViewAdapter اطلاعات کاربران را منتقل می‌کنیم تا در RecyclerView نمایش داده شود.

class UserActivity : AppCompatActivity() {
private lateinit var binding: ActivityUserBinding
private lateinit var viewModel: UserViewModel
private lateinit var adapter: UserListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUserBinding.inflate(layoutInflater)
setContentView(binding.root)
val apiService = ApiService.create()
viewModel = ViewModelProvider(this, UserViewModelFactory(apiService)).get(UserViewModel::class.java)
adapter = UserListAdapter()
binding.recyclerView.adapter = adapter
viewModel.users.observe(this, { users ->
adapter.submitList(users)
})
viewModel.getUsers()
}
}


در اینجا، با استفاده از ViewModelProvider، ViewModel را ایجاد کرده و به RecyclerViewAdapter اطلاعات کاربران را منتقل می‌کنیم. همچنین، با observe کردن LiveData، در صورت تغییر در اطلاعات، آن را به adapter می‌دهیم تا در RecyclerView نمایش داده شود. در انتها، تابع getUsers در ViewModel فراخوانی می‌شود تا اطلاعات کاربران از سرویس web دریافت شود.


با این کار، ما یک برنامه کامل و قابل استفاده برای دریافت اطلاعات کاربران از یک سرویس web و نمایش آنها در یک RecyclerView ایجاد کرده‌ای. این برنامه از معماری MVVM و Clean Architecture استفاده می‌کند که اجزای مختلف برنامه را به صورت جداگانه پیاده سازی کرده و از هم جدا کرده است. این کار، باعث افزایش قابلیت اطمینان، قابلیت تست و قابلیت گسترش می‌شود. همچنین، با استفاده از این معماری، ما قابلیت ایجاد و توسعه برنامه‌های پایدار و قابل نگهداری را داریم.



















kotlingptpoedata sourceclean architecture
برنامه نویس اندروید
شاید از این پست‌ها خوشتان بیاید