تخفیفات تابستان تاپایان: یکشنبه ۱۶ بهمن ۱۴۰۱
بزن بریم فروشگاه

ساخت اپلیکیشن اندرویدی با MVVM

ساخت اپلیکیشن اندرویدی با MVVM
مطالعه شده توسط : ۲۴۴۸ نفر
بروزرسانی : 1 سال پیش

زمان آن رسیده که در برنامه نویسی اندروید یک معماری جدیدی استفاده کنیم که با استفاده از DataBinding پیاده سازی میشود. در این مقاله ابتدا توضیحات و مفاهیم اصلی معماری mvvm به شما آموزش داده میشود سپس یک مثال واقعی بصورت قدم به قدم برای ساختن یک پروژه ی ساده با معماری mvvm انجام خواهیم داد پس تا آخر این مقاله ی آموزشی همراه باشید

همیشه در برنامه نویسی شیوه های مختلفی بوجود می آید که هدف اکثر این شیوه ها راحت تر کردن برنامه نویسی ، منظم کردن آن و در نهایت افزایش بهره وری یک برنامه نویس است. هر چند که تعدادی از برنامه نویسان همیشه رویه ی خود را دارند و تن به این سیستم ها نمیدهند اما باید بدانید که برای افزایش سرعت و کارآیی در پروژه های متوسط به بالا بهتر است از یک اصولی استفاده شود. اصولی که اکثرا برنامه نویسان برآن پایبند هستند. مثلا MVC در بحث وب ، به بسیاری از پروژه ها کمک اساسی کرد فریمورک های مختلفی برپایه ی MVC به وجود آمد که کدها را منظم تر و آسان تر کرده بود. برنامه نویسی اندروید نیز جزو آن دسته از حوزه هایی هست که نامنظمی های خود را دارد و وقتی پروژه ای از اندازه ی متوسط خود فراتر میرود بهم ریختگی زیاد میشود و وجود یک سیستم برای ساماندهی کدها ضروری است. در این مقاله به معماری MVVM میپردازیم.

 

معماری MVVM چیست ؟

برای شروع بهتر است بدانیم که MVVM مخفف Model-View-ViewModel است و یک نوع الگوی معماری هست که توسط John Gossman معرفی شده تا در هنگام استفاده از Data Binding جایگزین الگوهای قدیمی مثل MVC و MVP باشد.

مفهوم MVVM کلا این است که در پروژه های برنامه نویسی، قسمت presentation logic از business logic  جدا باشد که این کار با انتقال آن به کلاس خاص انجام میشود.

آموزش معماری mvvm در اندروید



خب پس این سه عبارت در عنوان MVVM چه معنی میدهد ؟‌ در لیست زیر ببینید :‌

 

  • قسمت Model مربوط به پایگاه داده ی اپلیکیشن است.
  • به عبارت دیگر آن ، کلاس POJO , کلاس های پردازش API , یک پایگاه داده و ... است.
  • قسمت View  همان لیوت اپلیکیشن و تمام ویجت هایی هست که روی صفحه نمایش داده میشود. 
  • قسمت ViewModel ، یک آبجکت است که رفتار View را براساس نتایج Model را تعریف میکند ، این قسمت میتواند یک فرمت ساده ی متنی باشد یا کامپوننت یا صفحه ی نمایش وضعیت همانند صفحه ی بارگزاری ، خطا ، صفحه ی خالی و ... باشد.
    همچنین این قسمت رفتار کاربر را نیز تعریف میکند مانند ورودی های متن ، کلیک کردن دکمه ها ، swipe کردن صفحه و ... 

 

معماری MVVM چه قابلیت هایی به ما میدهد ؟‌

  • توسعه ی منعطف تر  
  • تست پذیری 
  • جداسازی 

از آنجا که هیچ چیزی صد در صد کامل نیست MVVM نیز معایبی دارد :‌

  1. معماری MVVM برای پروژه های کوچک مناسب نیست.
  2. اگر قسمت data binding پیچیده باشد دیباگ کردن اپلیکیشن سخت تر خواهد بود.

 

در معماری MVVM کی کجاست ؟

در ابتدا که معماری MVVM را شروع میکنید این الگو نیاز به تغییراتی در ساختار اندروید دارد. در واقع بازنگری قسمت های مختلف و استفاده سنتی آنها ضروری است. برای مثال بیایید Activity ساده ی اندرویدی را در نظر بگیریم.

هر اکتیویتی یک فایل layout دارد که از نوع XML است و یک کلاس متصل به آن که JAVA است. به نظرتون فایل xml همان view ما و فایل Java همان ViewModel ما هست ؟ کاملا اینطور نیست.

اگر بگوییم فایل Java ما هم یک View است چطور ؟ 

به هر حال custom view دارای هم xml است و هم کلاس java  ولی آنها بصورت واحد در نظر گرفته میشوند.البته بدون فایل لیتوت xml هم میتوانید کار کنید ولی باید ویجت های ضروری را با استفاده از کد بسازید.

بنابراین میتونیم نتیجه بگیریم که در این معماری Activity همان View هست ( فایل xml + کلاس java ).

ولی سوال اینجاست که ViewModel چیست و جای آن کجاست ؟‌

در حقیقت ViewModel یک آبجکت کاملا جدا است و آن چیزیست که ما به فایل xml با استفاده از متود binding.setViewModel میفرستیم که دارای فیلد ها و متودهایی هست که ما بتوانیم view ها را با model ها  bind کنیم.

در اینجا Model ها همان تعریف و مفهوم سنتی خود را دارد ولی چیزی که میخام بهش اضافه کنم اینه که در ViewModel مستقیما به دیتابیس یا API ها ، مستقیما اشاره نکنید.در عوض ، برای هر VM یک Repository ایجاد کنید ، اینطوری کد تمیزتر و کم حجم تر خواهد بود.

مثال برای معماری mvvm در برنامه نویسی اندروید

بصورت کلی اگر بخواهیم بدانیم در MVVM کی کجاست ؟ بهتره یه مثال بزنیم . فرض کنید شما بعنوان یک مشتری به یک رستوران میروید گارسون از شما سفارش غذا میگیرد و به آشپز اعلام میکند و آشپز بعد از آماده سازی غذا آنرا به گارسون میدهد تا گارسون به دست شما برساند. آیا شما بعنوان مشتری آشپز را دیدید ؟‌ خیر ، ولی محصول آشپز که همان غذاست به دست شما رسیده است . 

در معماری MVVM مشتری همان View است و گارسون همان ViewModel هست که واسطه ی بین شما و آشپز بود و در نهایت model همان آشپز است و غذایی که دست همه چرخیده و توسط گارسون به دست شما رسیده همان داده ها یا DATA اپلیکیشن است.  آشپز که همان Model ما بود با استفاده از وسایلی که در آشپزخانه است غذا را آماده میکند. در این بخش آشپزخانه را بعنوان ریپوزیتوری در نظر بگیرید.

 

معماری MVVM در برنامه نویسی اندروید چطور پیاده سازی میشود ؟‌

اگر قصد دارید اپلیکیشن اندرویدی خود را با استفاده از معماری mvvm پیاده سازی کنید دو راه دارید یکی استفاده از data binding و دیگری استفاده از Rx.java 

در این مقاله ی آموزشی قصد ما استفاده از data binding هست .

وقتی میخواهیم از MVVM در اپ اندرویدی استفاده کنیم معماری پروژه ی اندرویدی ما مشابه تصویر زیر میباشد :‌

معماری mvvm چیست


در ادامه به نحوه ی کدنویسی و پیاده سازی یک پروژه ی ساده بر مبنای معماری MVVM میپردازیم.  برای تمرین ادامه ی این مقاله ی آموزشی بهتر است ابتدا یک پروژه ی خام و جدید در محیط اندروید استودیو بسازید و سپس طبق کدهای زیر جلو بروید.

ابتدا وابستگی های زیر را به build.gradle پروژه ی خود اضافه کنید :‌

implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.amitshekhar.android:rx2-android-networking:1.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.2.18'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'

سپس روی گزینه ی sync بزنید و حتما اتصال شما به اینترنت با IP ایران نباشد (  چون برای دانلود بعضی از پکیج ها تحریم هستیم ) و سپس منتظر بمانید تا گردل عمل بیلد و بازسازی پروژه را تمام بکنه.

ساختار اولیه و ابتدایی پروژه های MVVM در محیط اندروید استودیو باید به شکل زیر باشد :  

ساختار پروژه های MVVM در اندروید استودیو

بسته ی utils که در تصویر بالا میبینید را به شکل زیر تنظیم کنید : 

package com.mindorks.framework.mvvm.utils

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

خب حالا به یک کلاس نیاز داریم که وضعیت شبکه را به لایه ی UI بفرستد و نام آن کلاس رو Resource میزاریم.

پس یک دیتاکلاس از نوع کاتلین بسازید و اسم اون رو Resource بزارید. این کلاس باید درون پکیج utils قرار بگیره.

package com.mindorks.framework.mvvm.utils

data class Resource<out T>(val status: Status, val data: T?, val message: String?) {

    companion object {

        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T?): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T?): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }

    }

}

در این صورت پکیج utils ما آماده است.

در ادامه قصد داریم لایه ی data را بسازیم.

  1. یک پکیج به اسم data درست کنید
  2. یک پکیج دیگر داخل data به نام model بسازید

حالا پاسخ API ما که بصورت جیسان ارائه شده به شکل زیر خواهد بود : 

[
  {
    "id": "1",
    "name": "Mrs. Nedra Gerhold",
    "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/to_soham/128.jpg",
    "email": "Lonzo6@hotmail.com"
  },
  {
    "id": "2",
    "name": "Spencer McKenzie",
    "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/irae/128.jpg",
    "email": "Josiah.Hane@gmail.com"
  }
]

براساس این خروجی API ما باید کلاس data خودمون را پیاده سازی کنیم.

حالا داخل پکیج model که بالاتر ساختیم یک کلاس کاتلین به اسم User بسازید.

 کلاس User که ساختیم باید به شکل زیر باشه :‌

package com.mindorks.framework.mvvm.data.model

import com.google.gson.annotations.SerializedName

data class User(
    @SerializedName("id")
    val id: Int = 0,
    @SerializedName("name")
    val name: String = "",
    @SerializedName("email")
    val email: String = "",
    @SerializedName("avatar")
    val avatar: String = ""
)

حالا نیازه که ما یک لایه برای Network  درست کنیم .

  1. یک پکیج جدید درون data به اسم api بسازید.
  2. یک interface کاتلین درون پکیج api بسازید به نام ApiService 

کدهای درون ApiService باید به شکل زیر باشد :‌

package com.mindorks.framework.mvvm.data.api

import com.mindorks.framework.mvvm.data.model.User
import io.reactivex.Single

interface ApiService {

    fun getUsers(): Single<List<User>>

}

سپس باید یک کلاس جدیدی بسازیم به اسم ApiServiceImpl که از اینترفیسی که بالا ساختیم ارث بری کند نام اینترفیس ApiService بود پس باید کدهای این کلاس به شکل زیر باشد :‌

package com.mindorks.framework.mvvm.data.api

import com.mindorks.framework.mvvm.data.model.User
import com.rx2androidnetworking.Rx2AndroidNetworking
import io.reactivex.Single

class ApiServiceImpl : ApiService {

    override fun getUsers(): Single<List<User>> {
        return Rx2AndroidNetworking.get("https://5e510330f2c0d300147c034c.mockapi.io/users")
            .build()
            .getObjectListSingle(User::class.java)
    }

}

حالا یک کلاس جدیدی بسازید به اسم ApiHelper که درون پکیج api قرار دارد و محتویات آن به شکل زیر است :‌

package com.mindorks.framework.mvvm.data.api

class ApiHelper(private val apiService: ApiService) {

    fun getUsers() = apiService.getUsers()

}

درون data یک پکیج دیگری بسازید به نام repository و درون این پکیج یک کلاس جدیدی به اسم MainRepository بسازید که محتویاتش را از کد زیر استفاده کنید :‌

package com.mindorks.framework.mvvm.data.repository

import com.mindorks.framework.mvvm.data.api.ApiHelper
import com.mindorks.framework.mvvm.data.model.User
import io.reactivex.Single

class MainRepository(private val apiHelper: ApiHelper) {

    fun getUsers(): Single<List<User>> {
        return apiHelper.getUsers()
    }

}

حالا لایه ی data ما آماده است.

 

ساختن لایه ی UI  و اجرای اپلیکیشن براساس معماری MVVM

حالا به مرحله ی نهایی رسیده ایم و در این بخش باید یک لایه برای UI درست کنیم و سپس پروژه را build و اجرا کنیم. کارهایی که در این بخش باید انجام بدهیم : 

  1. یک پکیج جدید به اسم ui بسازید.
  2. درون پکیج ui یک پکیج دیگر به اسم main بسازید.
  3. پکیج دیگری به اسم view درون پکیج main بسازید.
  4. حالا MainActivity را به درون پکیج view ببرید ( کات کنید).
  5. یک پکیج دیگر درون main به نام viewmodel بسازید.

 

درون viewmodel یک کلاس کاتلین جدید به نام MainViewModel بسازید که دارای کدهای زیر میباشد :

package com.mindorks.framework.mvvm.ui.main.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.mindorks.framework.mvvm.data.model.User
import com.mindorks.framework.mvvm.data.repository.MainRepository
import com.mindorks.framework.mvvm.utils.Resource
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers

class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

    private val users = MutableLiveData<Resource<List<User>>>()
    private val compositeDisposable = CompositeDisposable()

    init {
        fetchUsers()
    }

    private fun fetchUsers() {
        users.postValue(Resource.loading(null))
        compositeDisposable.add(
            mainRepository.getUsers()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ userList ->
                    users.postValue(Resource.success(userList))
                }, { throwable ->
                    users.postValue(Resource.error("Something Went Wrong", null))
                })
        )
    }

    override fun onCleared() {
        super.onCleared()
        compositeDisposable.dispose()
    }

    fun getUsers(): LiveData<Resource<List<User>>> {
        return users
    }

}

ما در اینجا از LiveData استفاده میکنیم.

حالا بیایید لیوت xml خودمون رو بسازیم.

توی فولدر لیوت ها فایل activity_main.xml را باکدهای زیر تغییر بدهید: 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.view.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

حالا در فولدر layout هاا یک فایل جدید item_layout.xml بسازید و کدهای زیر را درونش قرار بدهید : 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="60dp">

    <ImageView
        android:id="@+id/imageViewAvatar"
        android:layout_width="60dp"
        android:layout_height="0dp"
        android:padding="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserName"
        style="@style/TextAppearance.AppCompat.Large"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="MindOrks" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserEmail"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textViewUserName"
        app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
        tools:text="MindOrks" />

</androidx.constraintlayout.widget.ConstraintLayout>

یک پکیج جدید درون main بسازید با نام adapter و درونش یک کلاس جدید به اسم MainAdapter درست کنید که شامل کدهای زیر باشه : 

package com.mindorks.framework.mvvm.ui.main.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mindorks.framework.mvvm.R
import com.mindorks.framework.mvvm.data.model.User
import kotlinx.android.synthetic.main.item_layout.view.*

class MainAdapter(
    private val users: ArrayList<User>
) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {

    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(user: User) {
            itemView.textViewUserName.text = user.name
            itemView.textViewUserEmail.text = user.email
            Glide.with(itemView.imageViewAvatar.context)
                .load(user.avatar)
                .into(itemView.imageViewAvatar)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        DataViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_layout, parent,
                false
            )
        )

    override fun getItemCount(): Int = users.size

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
        holder.bind(users[position])

    fun addData(list: List<User>) {
        users.addAll(list)
    }

}

درون پکیج ui یک پکیج دیگری به اسم base بسازید و سپس درون آن یک فایل کلاس به نام ViewModelFactory بسازید و محتویات زیر را درون آن بنویسید :‌

package com.mindorks.framework.mvvm.ui.base

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.mindorks.framework.mvvm.data.api.ApiHelper
import com.mindorks.framework.mvvm.data.repository.MainRepository
import com.mindorks.framework.mvvm.ui.main.viewmodel.MainViewModel

class ViewModelFactory(private val apiHelper: ApiHelper) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(MainRepository(apiHelper)) as T
        }
        throw IllegalArgumentException("Unknown class name")
    }

}

 

حالا نیازه که ما فایل MainActivity خودمان را تکمیل بکنیم : 

package com.mindorks.framework.mvvm.ui.main.view

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.mindorks.framework.mvvm.R
import com.mindorks.framework.mvvm.data.api.ApiHelper
import com.mindorks.framework.mvvm.data.api.ApiServiceImpl
import com.mindorks.framework.mvvm.data.model.User
import com.mindorks.framework.mvvm.ui.base.ViewModelFactory
import com.mindorks.framework.mvvm.ui.main.adapter.MainAdapter
import com.mindorks.framework.mvvm.ui.main.viewmodel.MainViewModel
import com.mindorks.framework.mvvm.utils.Status
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var mainViewModel: MainViewModel
    private lateinit var adapter: MainAdapter

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

    private fun setupUI() {
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = MainAdapter(arrayListOf())
        recyclerView.addItemDecoration(
            DividerItemDecoration(
                recyclerView.context,
                (recyclerView.layoutManager as LinearLayoutManager).orientation
            )
        )
        recyclerView.adapter = adapter
    }

    private fun setupObserver() {
        mainViewModel.getUsers().observe(this, Observer {
            when (it.status) {
                Status.SUCCESS -> {
                    progressBar.visibility = View.GONE
                    it.data?.let { users -> renderList(users) }
                    recyclerView.visibility = View.VISIBLE
                }
                Status.LOADING -> {
                    progressBar.visibility = View.VISIBLE
                    recyclerView.visibility = View.GONE
                }
                Status.ERROR -> {
                    //Handle Error
                    progressBar.visibility = View.GONE
                    Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
                }
            }
        })
    }

    private fun renderList(users: List<User>) {
        adapter.addData(users)
        adapter.notifyDataSetChanged()
    }

    private fun setupViewModel() {
        mainViewModel = ViewModelProviders.of(
            this,
            ViewModelFactory(ApiHelper(ApiServiceImpl()))
        ).get(MainViewModel::class.java)
    }
}

 

و در آخر چون این اپلیکیشن از اینترنت استفاده میکند مجوز دسترسی به اینترنت را در فایل AndroidManifest اعمال کنید : 

<uses-permission android:name="android.permission.INTERNET"/>

 

تبریک . حالا شما یک اپلیکیشن ساده در معماری MVVM با زبان کاتلین ساخته اید البته در جاوا نیز پیاده سازی به این شکل است که ما برای ساده تر کردن کدها از کاتلین استفاده کردیم. حالا میتوانید اپ را build کنید و نتیجه ی کار خود را ببینید.

 

لینک کوتاه این مقاله : https://avasam.ir/post/358
این سیستم برپایه ی علاقه مندی شما یک دوره ی مناسب به شما پیشنهاد میدهد
مرا بسوی بهترین دوره ی آموزشی که برای من مناسب است هدایت کن 🤖
برای استفاده ی دیگران و حمایت از ما در جامعه های زیر به اشتراک بگذارید

برای نوشتن نظر وارد شوید ورود
یا به عنوان یک میهمان نظر خود را بنویسید :
    1. اگر سوال شما طولانی است و نیاز به پشتیبانی خوبی دارد در پروفایل خود تیکت باز کنید تیم پشتیبان ما پاسخ میدهد
    2. سعی کنید نظر خود را بیش از چند جمله بنویسید
    3. نظرات شامل توهین و تهمت و نامرتبط تائید نخواهد شد
آیا دوست دارید با یک دوره ی آموزشی با کیفیت برنامه نویسی را شروع کنید و تمام مبانی لازم را یاد بگیرید ؟ پس دوره ی دوازده قدم آواسام را از دست ندهید
دوره ی آموزش دوازده قدم برنامه نویسی
دوره ی آموزش پروژه محور ساخت کافه بازار دوره ی آموزش پروژه محور ساخت فروشگاه دیجیکالا آموزش لاراول دوره ی آموزش ویو جی اس