آموزش Data Binding در اندروید

Data Binding راهی برای اتصال رابط کاربری (UI) با داده ها فراهم می کند و اجازه می دهد که رابط کاربر بطور خودکار بروز شود. همچنین کار ما را راحت کرده و به ما اجازه میدهد که متغیرها و توابع را مستقیما در لایوت مقدار دهی کنیم و مقادیر به view های مربوطه متصل شوند ودیگه نیازی به استفاده از کد تکراری      ()findViewById  درسمت جاوا نیست.که اینکار حجم کد را کمتر و سرعت را بالاتر میبرد. با آموزش Data Binding در اندروید همراه ما باشید.

Data Bindingیکی از اجزای معماری اندروید است که توسط اندروید پیشنهاد شده است.

فهرست مطالب آموزش Data Binding در اندروید

۱-فعال کردن Data Binding

۲-مثال اصلی آموزش Data Binding در اندروید

۳- کلاسهای Data Binding ایجاد نشده اند

Data Binding -4 در Layout

۵-اتصال Event Handling/ Click Listeners

۶-بروزرسانی رابط کاربر با استفاده از observables

۷-بروزرسانی رابط کاربر با استفاده از observablefield

۸-بارگذاری تصاویر از URL

۹-متصل کردن توابع جاوا

  1- -فعال کردن Data Binding

برای شروع کار ابتدا باید این ویژگی را در پروژه فعال کرد.فایل build.gradle را باز کنید وبا این چند خط کد Data Binding را فعال کنید.پس از فعالسازی پروژه را sync (همگامسازی) کنید.

android {
    dataBinding {
        enabled = true
    }
 
    compileSdkVersion 27
 
    defaultConfig {
        applicationId "info.androidhive.databinding"
        minSdkVersion 16
        // ..
    }
}
 

ما میخواهیم اطلاعات کاربر را از یک کلاس POJO   User نمایش دهیم . در زیر کلاس POJO یک شی ء User با نام و ایمیل را ایجاد میکند.  به منظور اتصال داده  UI به کلاس  model می توان از هر آبجکت ساده ی جاوا استفاده کرد که در اصطلاح POJO میگویند  .

آموزش Data Binding در اندروید

public class User {
    String name;
    String email;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getEmail() {
        return email;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
}

برای فعال کردن Data Binding در یک Layout باید با تگ <layout> به عنوان عنصر ریشه(root)شروع کرد .همراه با آن تگ های <data> و <variable> استفاده میشود.

در زیر ساختاری از لایوت data-binding وجود دارد.

<layout ...>
 
    <data>
         
        <variable
            name="..."
            type="..." />
    </data>
 
    <LinearLayout ...>
       <!-- YOUR LAYOUT HERE -->
    </LinearLayout>
</layout>
  • داخل تگ <layout> کدهای معمول لایوت قرار میگیرد.
  • تگ <data> در زیر تگ<layout> قرار دارد.همه متدها و متغیرهای اتصال باید در تگ <data > وارد شوند.
  • داخل تگ <data> , یک متغیر با تگ <variable> تعریف میشود. تگ <variable> دو خصوصیت name و type دارد.خصوصیت name نام مستعار هست و خصوصیت type باید از کلاس مدل باشد.در اینجا مسیر ما کلاس User می باشد.
  • برای اتصال یک مقدار , از دستور نگارشی{ }@ استفاده میشود. در لایوت زیر name و email کاربربا استفاده از {user.name} @و  {user.email}@به TextView  متصل شده اند.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
    <data>
         
        <variable
            name="user"
            type="info.androidhive.databinding.User" />
    </data>
 
    <LinearLayout 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"
        android:orientation="vertical"
        android:padding="@dimen/fab_margin"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".MainActivity"
        tools:showIn="@layout/activity_main">
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.email}" />
 
    </LinearLayout>
</layout>
  • وقتی که اتصال داده ها در فایل لایوت انجام شد به منوی Build رفته و پروژه را   Clean Project وRebuild Project میکنیم. اینکار کلاسهای ضروری اتصال را ایجاد میکند.
  • نامگذاری کلاسهای اتصال تولید شده براساس نام فایل لایوت اتصال فعال شده, میباشد .برای لایوت activity_main.xml نام کلاس اتصال تولید شده ActivityMainBinding خواهد بود(پسوند Binding در آخر اضافه میشود)
  • برای متصل کردن داده در رابط کاربر (UI) باید ابتدا لایوت اتصال را با استفاده از کلاسهای اتصال تولید شده  پر کنید. در زیر ابتدا لایوت ActivityMainBinding   را پر کرده و بعد ()binding.setUser لایوت را به شیء User متصل میکند.

میتوانید ملاحظه کنید که از ()findViewByIdدر جایی استفاده نکردیم.

import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
 
import info.androidhive.databinding.databinding.ActivityMainBinding;
 
public class MainActivity extends AppCompatActivity {
 
    private User user;
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        // setContentView(R.layout.activity_main);
 
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
 
        user = new User();
        user.setName("Ravi Tamada");
        user.setEmail("ravi@androidhive.info");
 
        binding.setUser(user);
    }
}

اگر این برنامه را اجرا کنید میتوانید جزيیات نمایش داده شده کاربر را در TextViews ببینید.

Data Binding در اندروید

۳-کلاسهای DataBinding تولید نشده اند

نسخه رایج اندروید استودیو در اکثر اوقات موفق به تولید کلاسهای اتصال نمیشود. معمولا این مساله را میتوان با , Cleaning & Rebuilding کردن پروژه حل کرد.اگر مشکل همچنان ادامه داشت , به مسیر File Invalidate Caches & Restart برویدو پروژه را دوباره راه اندازی کنید.اگر فایلهای لایوت شما خطایی نداشته باشند احتمالا این مشکل حل شود.

۴- DataBinding در لایوتهای *<include>

* چیدمانی که قرار است چند جا تکرار شود را در یک فایل می نویسند و در جاهای مختلف آن را include می کنند.

 ما  CoordinatorLayout و  AppBarLayout  و سایر عناصر را در مثال بالا نداریم . معمولا  لایوت اصلی(main)  و  محتوا (content)را در دو لایوت مختلف activity_main.xml  و content_main.xml جدا میکنیم .content_main  در لایوت اصلی (main) با استفاده از تگ <include> وجود خواهد داشت . حالا خواهیم دید که چطور وقتی که لایوتها را include میکنیم امکان اتصال داده را فراهم کنیم .

در زیر activity_main.xml با CoordinatorLayout, AppBarLayout و FAB را داریم.

  • تگ <layout> در لایوت activity_main.xml برای فعال کردن اتصال داده استفاده میشود.همچنین تگهای <data> <variable برای اتصال شی ء User استفاده میشوند.
  • برای انتقال user به لایوت content_main در تگ <include> خط کد bind:user=”@{user}” را مینویسیم.بدون این خط کد شیء user در لایوت content_main در دسترس نخواهد بود.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:bind="http://schemas.android.com/apk/res/android">
 
    <data>
 
        <variable
            name="user"
            type="info.androidhive.databinding.User" />
    </data>
 
    <android.support.design.widget.CoordinatorLayout 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=".MainActivity">
 
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">
 
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />
 
        </android.support.design.widget.AppBarLayout>
 
        <include
            android:id="@+id/content"
            layout="@layout/content_main"
            bind:user="@{user}" />
 
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="@dimen/fab_margin"
            app:srcCompat="@android:drawable/ic_dialog_email" />
 
    </android.support.design.widget.CoordinatorLayout>
</layout>
  • content_main.xml تگ <layout> را برای فعال کردن اتصال داده دوباره include میکند. تگهای <layout>, <data> , <variable> برای هردو parent و لایوتهای include شده ضروری هستند.
  • ویژگیهای android:text=”@{user.name}” , android:text=”@{user.email}” برای نمایش داده ها در TextViewها استفاده میشوند.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
    <data>
 
        <variable
            name="user"
            type="info.androidhive.databinding.User" />
    </data>
 
    <LinearLayout 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"
        android:orientation="vertical"
        android:padding="@dimen/fab_margin"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".MainActivity"
        tools:showIn="@layout/activity_main">
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.email}" />
 
    </LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
 
        setSupportActionBar(binding.toolbar);
 
        User user = new User();
        user.setName("Ravi Tamada");
        user.setEmail("ravi@androidhive.info");
 
        binding.setUser(user);
    }
}

اگر برنامه را اجرا کنید میتوانید داده های نمایش داده شده  را ببینید.

Data Binding

۵اتصال Event Handling/ Click Listeners

نه تنها داده ها ما میتوانیم click  و سایر رویدادها را روی عناصر UI متصل کنیم.

برای اتصال رویداد کلیک ما احتیاج داریم که یک کلاس با متدهای مورد نیاز callback ایجاد کنیم.

در زیر ما یک کلاس داریم که رویداد کلیک FAB* را مدیریت میکند.

* دکمه شناور FAB یا همان Floating Action Button کار این دکمه این هست ، که همیشه دم دسته یعنی حتی اگه یک لیست طولانی در لایوت داشته باشیم و اسکرول کنیم دکمه همیشه ثابت سرجای خودش هست.

public class MyClickHandlers {
 
        public void onFabClicked(View view) {
            Toast.makeText(getApplicationContext(), "FAB clicked!", Toast.LENGTH_SHORT).show();
        }
}

برای اتصال به این رویداد ما دوباره از تگ  <variable>استفاده میکنیم با مسیری برای هندلر کردن کلاس.

در پایین این خط کد android:onClick=”@{handlers::onFabClicked}” کلیک FAB را به متد ()onFabClickedمتصل میکند.

<layout xmlns:bind="http://schemas.android.com/apk/res/android">
 
    <data>
 
        <variable
            name="handlers"
            type="info.androidhive.databinding.MainActivity.MyClickHandlers" />
    </data>
 
    <android.support.design.widget.CoordinatorLayout ...>
 
        <android.support.design.widget.FloatingActionButton
            ...
            android:onClick="@{handlers::onFabClicked}" />
 
    </android.support.design.widget.CoordinatorLayout>
</layout>
  • برای اختصاص دادن رویداد فشار طولانی (long press) ,متد باید نوع Boolean را بجای فضای خالی (void) برگرداند .

()public boolean onButtonLongPressed

  • همچنین میتوانید پارامترهارا در زمان اتصال انتقال بدهید.

public void onButtonClickWithParam(View view, User user) .اتصال شیء, user رااز لایوت UI دریافت می کند.در لایوت پارامترها میتوانند منتقل شوند با استفاده از :

android:onClick=”@{(v) -> handlers.onButtonClickWithParam(v, user)}”

  • برای اتصال رویدادها ,binding.setHandlers(handlers) از اکتیویتی فراخوانده میشود.
package info.androidhive.databinding;
 
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
 
import info.androidhive.databinding.databinding.ActivityMainBinding;
 
public class MainActivity extends AppCompatActivity {
 
    private User user;
    private MyClickHandlers handlers;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
 
        setSupportActionBar(binding.toolbar);
 
        user = new User();
        user.setName("Ravi Tamada");
        user.setEmail("ravi@androidhive.info");
 
        binding.setUser(user);
 
        handlers = new MyClickHandlers(this);
        binding.content.setHandlers(handlers);
    }
 
    public class MyClickHandlers {
 
        Context context;
 
        public MyClickHandlers(Context context) {
            this.context = context;
        }
 
        public void onFabClicked(View view) {
            Toast.makeText(getApplicationContext(), "FAB clicked!", Toast.LENGTH_SHORT).show();
        }
 
        public void onButtonClick(View view) {
            Toast.makeText(getApplicationContext(), "Button clicked!", Toast.LENGTH_SHORT).show();
        }
 
        public void onButtonClickWithParam(View view, User user) {
            Toast.makeText(getApplicationContext(), "Button clicked! Name: " + user.name, Toast.LENGTH_SHORT).show();
        }
 
        public boolean onButtonLongPressed(View view) {
            Toast.makeText(getApplicationContext(), "Button long pressed!", Toast.LENGTH_SHORT).show();
            return false;
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:bind="http://schemas.android.com/apk/res/android">
 
    <data>
 
        <variable
            name="user"
            type="info.androidhive.databinding.User" />
 
        <variable
            name="handlers"
            type="info.androidhive.databinding.MainActivity.MyClickHandlers" />
    </data>
 
    <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
        ...>
 
        <android.support.design.widget.AppBarLayout
            ...>
 
        </android.support.design.widget.AppBarLayout>
 
        <include
            android:id="@+id/content"
            layout="@layout/content_main"
            bind:user="@{user}" />
 
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="@dimen/fab_margin"
            android:onClick="@{handlers::onFabClicked}"
            app:srcCompat="@android:drawable/ic_dialog_email" />
 
    </android.support.design.widget.CoordinatorLayout>
</layout>
 
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
    <data>
 
        <variable
            name="user"
            type="info.androidhive.databinding.User" />
 
        <variable
            name="handlers"
            type="info.androidhive.databinding.MainActivity.MyClickHandlers" />
    </data>
 
    <LinearLayout 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"
        android:orientation="vertical"
        android:padding="@dimen/fab_margin"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".MainActivity"
        tools:showIn="@layout/activity_main">
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.email}" />
 
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handlers::onButtonClick}"
            android:text="CLICK" />
 
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{(v) -> handlers.onButtonClickWithParam(v, user)}"
            android:text="CLICK WITH PARAM" />
 
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="LONG PRESS"
            android:onLongClick="@{handlers::onButtonLongPressed}" />
 
    </LinearLayout>
</layout>
 

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

۶-بروز رسانی UI با استفاده از Observables

بدون آنکه متد setter() بطور واضح فراخوانی شود Observables  راهی را برای همگامسازی خودکار UI با داده فراهم میکند .وقتی مقداری از یک ویژگی (property) در یک شیء تغییر کند ,  UIبروز خواهد شد.برای ساختن شیء observable کلاس را گسترش میدهیم (extend) از BaseObservable.

  • برای ساختن یک ویژگی observable از علامت Bindable @روی متد() getter استفاده میکنیم.
  • وقتی که داده ها تغییر میکنند در متد() setter برای بروزکردن رابط کاربر, notifyPropertyChanged(BR.property) فراخوانی میشود
  • وقتی که Data Binding فعال باشد کلاس BR  بطور خودکار تولید خواهد شد.

در پایین کلاس اصلاح شده User گسترش یافته (extend ) از BaseObservable.شما میتوانید متوجه شوید که در اینجاnotifyPropertyChanged بعد از نسبت دادن مقادیر جدید فراخوانی شده.

package info.androidhive.databinding;
 
import android.databinding.BaseObservable;
import android.databinding.Bindable;
 
public class User extends BaseObservable {
    String name;
    String email;
 
    @Bindable
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }
 
    @Bindable
    public String getEmail() {
        return email;
    }
 
    public void setEmail(String email) {
        this.email = email;
        notifyPropertyChanged(BR.email);
    }
}

برای تست این , ما اطلاعات کاربر را درon FAB click تغییر دادیم .شما میتوانید ببینید که UI  در on FAB click به درستی بروز شد.

public class MyClickHandlers {
 
    Context context;
 
    public MyClickHandlers(Context context) {
        this.context = context;
    }
 
    public void onFabClicked(View view) {
        user.setName("Ravi");
        user.setEmail("ravi8x@gmail.com");
    }
}

۷-بروزرسانی UI با استفاده از ObservableFields

اگر کلاس شیء شما دارای ویژگیهای کمتری برای بروزرسانی باشد یا اگر شما نمیخواهید که هر فیلد را در شیء مشاهده کنید.میتوانید از ObservableFields برای بروزرسانی رابط کاربر استفاده کنید .شما میتوانید متغیر(variable) را به عنوان ObservableFields اعلام کنید و زمانیکه داده های جدید تنظیم(set) شود رابط کاربر بروز رسانی خواهد شد.

همان کلاس User را میتوان بصورت زیر با استفاده از ObservableFields اصلاح کرد.

package info.androidhive.databinding;
 
import android.databinding.ObservableField;
 
public class User {
    public static ObservableField<String> name = new ObservableField<>();
    public static ObservableField<String> email = new ObservableField<>();
 
    public ObservableField<String> getName() {
        return name;
    }
 
    public ObservableField<String> getEmail() {
        return email;
    }
}

برای بروزرسانی مقادیر باید بجای استفاده از متد() setter مقدار جدید را مستقیما به ویژگی تخصیص داد.

public class MyClickHandlers {
 
    Context context;
 
    public MyClickHandlers(Context context) {
        this.context = context;
    }
 
    public void onFabClicked(View view) {
        user.name.set("Ravi");
        user.email.set("ravi8x@gmail.com");
    }
}

۸-بارگیری تصاویر از (Glide or Picasso)URL

میتوانید یک ImageView را برای بارگیری تصویر به یکURL  متصل کنید.برای اتصال شما میتوانید از علامتBindingAdapter @برای ویژگی(property) شیء استفاده کنید.

در زیر متغیر profileImage به مشخصه android:profileImage متصل شده است .تصویر با استفاده از کتابخانه Glide یا Picasso بارگذاری خواهد شد .

package info.androidhive.databinding;
 
import android.databinding.BaseObservable;
import android.databinding.Bindable;
import android.databinding.BindingAdapter;
import android.widget.ImageView;
 
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
 
public class User {
    //..
    String profileImage;
 
    public String getProfileImage() {
        return profileImage;
    }
 
    public void setProfileImage(String profileImage) {
        this.profileImage = profileImage;
    }
 
    @BindingAdapter({"android:profileImage"})
    public static void loadImage(ImageView view, String imageUrl) {
        Glide.with(view.getContext())
                .load(imageUrl)
                .into(view);
 
        // If you consider Picasso, follow the below
        // Picasso.with(view.getContext()).load(imageUrl).placeholder(R.drawable.placeholder).into(view);
    }
}

برای بارگذاری تصویر داخل ImageView مشخصه android:profileImage=”@{user.profileImage}” را اضافه کنید.

<ImageView
     android:layout_width="100dp"
     android:layout_height="100dp"
     android:layout_marginTop="@dimen/fab_margin"
     android:profileImage="@{user.profileImage}" />

دقت کنید که اجازه اینترنت را در فایل manifest اضافه کرده باشید.

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

اندروید

۹-اتصال توابع جاوا

میتوانید توابع جاوا را به عناصر UI متصل کنید .حالا شما میخواهید قبل از نمایش UI عملیاتی را روی مقادیر انجام دهید .براحتی شما میتوانید اینکار را با استفاده از تگ <import>انجام دهید.

در اینجا متدی داریم که رشته را به تمام حروف بزرگ تبدیل میکند.

public class BindingUtils {
    public static String capitalize(String text) {
        return text.toUpperCase();
    }
}

برای صدازدن این تابع در لایوت خود ابتدا کلاس را با استفاده از تگ <import> وارد کنید و تابع مربوط به ویژگی را فراخوانی کنید.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
 
    <data>
        <import type="info.androidhive.databinding.BindingUtils" />
    </data>
 
    <LinearLayout ...>
 
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{BindingUtils.capitalize(user.name)}" />
 
    </LinearLayout>
</layout>

با انجام موارد بالا در آموزش Data Binding در اندروید حالا ما یکسری اطلاعات پایه ای در مورد Data Binding داریم.

ورکشاپ رایگان دوره های تخصصی برنامه نویسی

شما این فرصت را دارید، با تکمیل فرم زیر، قبل از انتخاب دوره آموزشی مناسب خود، در ورکشاپ رایگان دوره های تخصصی برنامه نویسی شرکت کنید

درباره‌ی دولت آبادی

همچنین ببینید

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

گزارش دوره آموزش اندروید – جلسه هفتم

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

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *