چقدر خوب می شد اگر هر وقت نیازمندی داشتیم فقط آن را مطرح می کردیم و به سادگی برایمان فراهم می شد؟ مفهوم Dependency Injection یا به اختصار DI همین است، شما فقط می گویید من آن object را می خواهم و شخص دیگری آن را با تمام پیچیدگی هایش برایتان فراهم می کند! در ضمن تمام امور مربوط به تولید، نگهداری، چرخه حیات و مسائل مربوط آن شئی را برایمان کنترل می کند و باعث می شود ارتباطات بین کلاس ها ساده تر شود و در نتیجه نگهداری و توسعه ساده تر گردد. قرار است این کار را توسط Dagger2 در اندروید پیاده سازی نماییم.
مقدمه:
Dependency Injection is a set of software design principles and patterns that enable us to develop loosely coupled code.
Loose coupling makes code extensible, and extensibility makes it maintainable
Mark Seemann
یکی از اصول SOLID اصل Dependency Inversion Principle است که یکی از دیزاین پترن هایی که آن را پیاده سازی می کند Dependency Injection است که هدف اصلی آن کم کردن وابستگی ها برای نگهداری و توسعه راحت تر می باشد. در صورتی که علاقه مند به آشنایی بیشتر با این دیزان پترن هستید می توانید کتاب Mark seemann را از اینجا دانلود کنید. در ضمن دوست و همکار عزیزم مثالی خوب و ملموسی با نام “Dependency Injection in the Coffeeshop” دارد که دیدن آن خالی از لطف نیست. Dagger
Dagger2 یک Dependency Injection FrameWork است که Dependency Injection را با استفاده از تولید خودکار کد بر پایه annotations بهره می گیرد (در مورد انوتیشن می توانید به این آموزش مراجعه کنید) یعنی برای استفاده از امکانات آن باید از annotation های خاصی استفاده کنیم که به توضیح آنها خواهم پرداخت. بخاطر اینکه کار با Dagger2 کمی ابهام دارد و فهم آن برای خودم هم کمی طول کشید، مثال ساده ای آماده کردم که نحوه کار با Dagger2 را خیلی واضح و راحت باهم برسی کنیم. فقط توجه داشته باشید که هدف اصلی این مقاله نحوه کارکرد با Dagger2 است و نه پیاده سازی صددرصد صحیح Dependency Injection و تلاش می کنیم در مقاله دوم به این امر نیز پرداخته و پروژه را از هر نظر باهم تکمیل کنیم.
به نظرم یک از دلایل پیچیدگی های کار با Dagger2 ، روش های گوناگون پیاده سازی آن است که شاید بعضاً کار کند ولی از نظر DI درست نباشد و می توان با اطلاع از این روش ها تسلط خوبی برآن پیدا کرد. بنابر این سعی می کنم به مرور کد را تمیز تر و به DI اصولی نزدیک تر کنم. پس تا آخر این مقاله با من باشید و درصورت وجود هرگونه ابهام یا ایراد یا سوال آن را با من در میان بگذارید تا در اسرع وقت پاسخ دهم.
قدم اول: اضافه کردن Dagger2 به پروژه
اول از همه اطلاعات زیر را به Gradle سطح پروژه اضافه کنید:
1 2 3 4 |
dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } |
سپس خط زیر را در آخر Gradle سطح برنامه اضافه کنید.
1 |
apply plugin: 'com.neenbedankt.android-apt' |
و وابستگی های زیر را نیز به همان فایل اضافه نمایید
1 2 3 4 5 |
dependencies { compile 'com.google.dagger:dagger:2.7' apt 'com.google.dagger:dagger-compiler:2.7' provided 'javax.annotation:jsr250-api:1.0' } |
سپس Gradle خود را sync کنید.
به روز رسانی: اخیراً به علت کاربرد زیاد Dagger گوگل امکان اضافه کردن این کتابخانه را بصورت مستقیم فراهم کرده و می توان بدین صورت نیز عمل کرد:
1 2 |
compile 'com.google.dagger:dagger:2.7' annotationProcessor 'com.google.dagger:dagger-compiler:2.7' |
قدم دوم: آماده سازی کلاس هایی که می خواهیم برایمان تزریق شوند و ارتباط بین آنها (Wiring up)
مفاهیم اصلی مطرح در Dagger2 به شرح زیر می باشند که همانطور که اشاره کردم برای استفاده از این مفاهیم باید از انوتیشن های هم نام آنها استفاده کرد.
Provides@نحوه تولید وابستگی ها را مشخص می کند. هر جا که این انوتیشن بروی متدی بود یعنی خروجی این متد قرار است در جایی از برنامه تزریق شده و مورد استفاده قرار گیرد.
Module@کلاسی هست که Provider ها در آن قرار می گیرند.
Component@اینترفیسی که ارتباط وابستگی ها و استفاده کننده آن را مطرح می کند. ما فقط اینترفیس را می نویسیم و پیاده سازی آن با خود Dagger است.
Inject@هرکجا که نیاز به تزریق وابستگی داشتیم از این انوتیشن استفاده می کنیم.
قصد ندارم که مانند خیلی از مقاله ها سرتان رو با تعاریف اولیه درد بیارم، پس صاف می رویم سر مثال که کاربرد این انوتیشن ها را ببینیم.
برنامه ای داریم که قرار است موتور خودرو تولید کند و جهت تست، آن را روشن کرده و استارت بزند. موتور ماشین وابسته به کامپیوتر داخلی و رادیاتور می باشد. ( این بخش ها را می توان بیشتر کرد ولی جهت سادگی فرض کنید موتور فقط نیازمند این دوبخش است.) و می خواهیم به کمک Dagger2 هرجا نیازی به کلاس خودرو شد بدون اینکه درگیر وابستگی های آن یعنی کامپوتر و رادیاتور شویم، فقط بگوییم یک موتور آماده تحویل ما بده. کلاس ها رو بصورت زیر تعریف می کنیم:
1 2 3 4 5 6 7 8 9 10 11 |
public class Computer { private int voltage; private String model; public Computer(String model ,int voltage){ this.model=model; this.voltage = voltage; } } |
1 2 3 4 5 6 7 |
public class Radiator { private String name; public Radiator(String name){ this.name = name; } } |
همانطور که دیدیم دو کلاس رادیاتور و کامپیوتر را داریم که وابستگی خاصی ندارند. حال نوبت به تعریف کلاس موتور می رسد. برای ایجاد و بیان وابستگی از انوتیشن Inject@ استفاده می کنیم و Dagger می فهمد که باید یک وابستگی را تزریق کند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class Motor { @Inject public Computer computer; @Inject Radiator radiator; String name; public Motor(String name) { this.name = name; } public boolean startEngin() { if (computer != null && radiator != null) { return true; } else { return false; } } public Computer getComputer() { return computer; } public String getName() { return name; } public Radiator getRadiator(){ return radiator; } } |
پس ما تا اینجا نحوه وابستگی کلاس ها را با انوتیشن Inject@ مشخص کردیم. یعنی به Dagger گفتیم که هنگام ساخت موتور رادیاتور و کامپیوتر را در اختیارم قرار بده! ( حالا نمی دانم چطوری، هرطور خودت صلاح می دونی!) ولی قبل آن لازم است تا یکبار نحوه ساخته شدن وابستگی ها را به Dagger را معرفی کنیم. برای اینکار کلاسی با نام ماژول می سازیم که از انوتیشن Module@ استفاده می کند و بخش های آن که نحوه ساختن وابستگی را شرح می دهند با انوتیشن Provides@ مشخص می کنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@Module public class MotorModule { String motoName; String computerName; String radiatorName; int voltage; public MotorModule(String computerName, int voltage , String radiatorName) { this.radiatorName = radiatorName; this.computerName = computerName; this.voltage = voltage; } @Provides @Singleton Motor provideMotor() { return new Motor(motoName); } @Provides @Singleton Computer provideComputer() { return new Computer(computerName, voltage); } @Provides @Singleton Radiator provideRadiator(){ return new Radiator(radiatorName); } } |
اگر قرار است وابستگی های ما ورودی داشته باشند آن را در Constructor مربوط به Module پاس می دهیم. همانطور که می بینید اسم رادیاتور، اسم کامپیوتر و ولتاژ آن ( که جزو ورودی های ما بودند) به همراه Constructor پاس داده شد. ما در Module به Dagger می فهمانیم که وقتی آبجکتی خواستیم آن را چگونه تولید کند. در ضمن با استفاده ازاسکوپ Singletone@ به Dagger می فهمانیم که همیشه یک نمونه از آن داشته باشد و هر دفعه یک نمونه جدید تولید نکند. برای مشاهده نحوه کار Scope به این مقاله مراجعه کنید.
کلاسی بعدی که به آن نیاز داریم interface ای هست که Dagger توسط آن می فهمد که کدام ماژول ها را در کجا باید تزریق کند و برای تولید کد خودکار Dagger مورد نیاز است، ولی اجازه بدهید تا این کلاس را بعد از نحوه استفاده از Dagger شرح دهم. فقط همین اندازه بدانیم که دو متد با نام دلخواه ( من نام هردو را void inject گذاشتم.) یکی با ورودی MainActivity و دیگری Motor ساختم.
1 2 3 4 5 6 7 8 |
@Singleton @Component(modules = {MotorModule.class}) public interface MotorComponent { void inject(MainActivity activty); void inject(Motor moto); } |
خیلی خوب. حال همه چیز آماده است و نوبت به این می رسد که object مورد نظرمان یعنی موتور را در کلاس تزریق و استفاده کنیم. فقط یک نکته باقی مانده است. لازم است تا Dagger کد خودکار را تولید کند و ورودی ماژول را به آن بدهیم تا کل ارتباطات کامل و تمامی آبجکت ها قابل تزریق باشد. اصطلاحاً سیم کشی یا Wiring up را تمام کنیم. فقط توجه نمایید که برای اینکه dagger بتواند کدهای مربوطه خود را auto generate کند بعد از این بخش ( یعنی ساخت ماژول ها و کامپوننت) باید کل پروژه را Rebuild کنیم. به متد generateDaggerCode توجه نمایید:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public class MainActivity extends AppCompatActivity { @Inject public Motor motor; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); generateDaggerCode(); createCar(motor); } private void createCar(Motor motor) { if (motor != null) { Toast.makeText(this, "car is created", Toast.LENGTH_SHORT).show(); } startCar(motor); } private void startCar(Motor motor) { if (motor.startEngin()) { Toast.makeText(this, "it is started", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "motor name is:" + motor.getName(), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "motor is not provided", Toast.LENGTH_SHORT).show(); } } private void generateDaggerCode() { MotorComponent component = DaggerMotorComponent.builder().motorModule(new MotorModule (this, "Computer name",12, "radiator name")).build(); component.inject(this); component.inject(motor); } } |
حال به تجزیه تحلیل کد دقت نمایید
اول از همه کد Dagger را تولید می کنیم که در ورودی آن ماژول را با ورودی آن پاس می دهیم. ( نام رادیاتور، نام کامپیوتر، ولتاژ) سپس دو متد موجود در Component را صدا می زنیم. حال ما هرجای این کد که یک موتور خواستیم کافی است آن را با Inject@ بیاوریم و خود Dagger آن را مدیریت می کند.
چند نکته و قانون وجود دارد که فهم آنها باعث می شود این پیچیدگی کم شود. اول اینکه اگر هرجایی Inject را در فیلد های یک کلاس انجام دادیم ( مثل همین جا که در MainActivity کلاس Motor را در اول کلاس تزریق کرده و سپس از آن استفاده کردیم) Dagger متوجه آن نمی شود مگر اینکه یک متدی ( با نام دلخواه) در Component بنویسم که ورودی آن، همان کلاس استفاده کننده باشد تا Dagger بفهمد چه کلاسی در فیلد خود Inject انجام داده و بتواند کد خود را بصورت صحیح تولید کند. برای همین امر بود که در Component یک متد به نام inject ساختم که ورودی آن MainActivity بود. همانطور که ملاحظه می کنید چون خود Motor هم در داخل خودش field Injection داشت ( هم Computer و هم Radiator در خط ۴ و ۷ کلاس موتور) پس ناچاراً متدی هم برای آن در Component نوشتم تا ارتباط آنها را نیز متوجه بشود و به اصطلاح فیلداینجکشن ها را اسکن کند.
اگر برنامه را اجرا کنید Motor به درستی Inject شده و پیغام مناسب دریافت خواهید کرد و ما توانسته ایم با استفاده از Dagger2 وابستگی را به کد تزریق کنیم.
حال توجه شما را به یک پیاده سازی دیگر جلب می نمایم. در این روش برای ساخت موتور، اجزای لازم را بدون استفاده از انوتیشن Inject@ و با Constructor پاس می دهم. همانطور که در بالا گفتیم چون تزریق در فیلد انجام ندادیم پس در کامپوننت هم متد آن ( void inject(Motor moto)) را حذف می کنیم. بدین صورت:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class Motor { private Computer computer; private Radiator radiator; private String name; public Motor(String name, Computer computer ,Radiator radiator) { this.name = name; this.computer = computer; this.radiator = radiator; } public boolean startEngin() { if (computer != null && radiator != null) { return true; } else { return false; } } public Computer getComputer() { return computer; } public String getName() { return name; } public Radiator getRadiator() { return radiator; } } |
پس Dagger می فهمد که برای ساخت موتور باید دو ورودی رادیاتور و کامپیوتر به موتور پاس دهد، از طرفی نحوه ساخت کامپیوتر و رادیاتور را می داند. پس همین کافی است که بتواند موتور را برایمان تولید کند و Component هم بصورت زیر می شود:
1 2 3 4 5 6 7 |
@Singleton @Component(modules = {MotorModule.class}) public interface MotorComponent { void inject(MainActivity activty); } |
حال اگر از شما بپرسند چرا متدی قرار دادیم با نام inject که ورودی آن از جنس MainActivity است جواب دهید: بخاطر اینکه در MainActivity در فیلد Motor را تزریق کرده ایم! بله درست گفتید!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class MainActivity extends AppCompatActivity { @Inject public Motor motor; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); generateDaggerCode(); createCar(motor); } private void createCar(Motor motor) { if (motor != null) { Toast.makeText(this, "car is created", Toast.LENGTH_SHORT).show(); } startCar(motor); } private void startCar(Motor motor) { if (motor.startEngin()) { Toast.makeText(this, "it is started", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "motor name is:" + motor.getName(), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "motor is not provided", Toast.LENGTH_SHORT).show(); } } private void generateDaggerCode() { DaggerMotorComponent.builder().motorModule(new MotorModule (this, "Computer name",12, "radiator name")).build().inject(this); } } |
همانطور که ملاحظه کردید هرجایی که بخواهیم وابستگی را تزریق کنیم باید از Inject@ استفاده کنیم و بنابراین باید کلاسی که در آن تزریق کرده ایم را توسط متدی در Component به Dagger معرفی کنیم. به نظر می رسد که بهتر است در هنگام ایجاد و تعریف وابستگی ها (Register) تا حد امکان از Inject@ استفاده نکنیم و در صورت امکان بصورت ورودی به Constructor بدهیم. دلیل من هم این است که هم خوانایی کد بالا می رود و هم تعداد متدهای Component کمتر می شود و در هنگام Resolve از Inject@ استفاده کنیم. ( Register, Resolve, Release پترنی است که در DI Container مورد استفاده قرار می گیرد که در مقاله بعد بیشتر راجع به آن صحبت خواهم کرد. )
بسیار عالی. تا اینجا با نحوه استفاده از Dagger2 آشنا شدید و دو نمونه از پیاده سازی های آن را دیدید. Dagger2 امکانات بسیار زیاد دیگری نیز دارد که انشاء الله در آموزش بعدی به همراه برسی چند مورد نکته دیگر، به آنها پرداخته و پرونده را به کل می بندیم. در ضمن می توانید پروژه تست را از گیت دانلود کنید:
منبع: بایت کد
مرسی خیلی خوب بود.اما میتونست مثال بهتری واسه اسم کلاسها بزنه
مرسی قشنگ توضیح دادین
و کاش یه لول دیگه پیش می رفتید – یعنی مثلا کامپیوتر نیاز به cpu داشت که منطقش کامل دستمون بیاد.
منم مثل دوستان سودی از مقالات و ویدپوهای دیگه نبردم تا رسیدم اینجا و خودم الان دارم تغییراتی می دم و نتایج خوبی بدست آوردم.
سپاسگذارم.
با سلام ممنون از اموزش کاربردیتون من قسمت دوم ک گفتین به روش دیگر پیاده سازی میکنید. رو قسمت Modual رو نفهمیدم چطوری پیاده سازی کردین؟الان ورودی های کلاس Motor رو که شامل Computer و Raditor میشه رو چطوریه در قسمت Modual به کلاس Motor دادین؟
خیلی خوب بود ممنون . من آموزش های زیاد داخلی و خارجی رو دیدم این از همه بهتر بود
خواهش می کنم. نظر لطفتون هست. قراره کلا آموزش ها به سایت بایت کد بره. لطفاً بازدید و حمایت کمک کنید رنکش مثل سایت خودم بشه.
تو بین منابع انگلیسی و فارسیای که خوندم یکی از بهترین آموزشها بود. دم شما گرم.
سلام
تشکر از آموزش خوبتون
خیلی خوب و روان توضیح میدید
چون هیچ آشنایی با بحث DI نداشتم و برام مبهم بود دنبال یه آموزش بودم که هم ساده باشه و هم اینکه جزئیات رو داشته باشه . به نظرم خیلی خوب تونستید این موارد را لحاظ کنید
سلام. خوشحالم که براتون مفید بوده.
سلام با تشکر از آموزش خوبتون.
ولی سوال من این که این فریم ورک در برنامه نویسی اندروید کجا به درد میخوره اگر ممکنه چند مثال بزنید.
سلام. به نظرم اول باید لزوم استفاده از Dependency Injection را برسی کنیم. همانطور که در مقاله مشاهده کردید دیزاین پترن Dependency Injection از اصل IoC که از اصول SOLID در مهندسی نرم افزار می باشد نشات گرفته است. این اصل (IOC) می گوید کنترل برنامه را از سطح کد ما به منابع خارجی مانند فریم ورک ها داده شود که بطور خلاصه شاید بتوان گفت: “نحوه پیاده سازی را به استفاده کننده بسپار”. پس به نظرم بهتر است راجع به این صحبت کنیم که چرا SOLID؟ رعایت اصول SOLID باعث می شود نگهداری و توسعه نرم افزار بسیار ساده تر باشد. پس Dependency Injection و از طرفی یک فریم ورک مانند Dagger2 نیز باعث همین امر می شود.
حالا اگر بخواهم یک مثال عملی در اندروید بزنم، فرض کن یک کلاس پیچیده برای نتورکینگ ساخته ایم به نام NetworkService که هم داخلش Authentication را باید چک کند که آیا کاربر لاگین کرده یا نه؟ از طرفی یک کلاس CacheManager دارد و خودش از چندین کلاس تشکیل شده که آنها از کلی کلاس دیگر و همگی نیاز به Context هم دارند، تشکیل شدند و کلی وابستگی دارند. حالا هرجای کد به این NetworkService نیاز پیدا کردیم باید چیکار کنیم؟ به روش قدیمی و کلاسیک باید new کنیم و وردی هایش را هم با new کردن بهش پاس بدیم. مثل Authentication و CacheManager و غیره و فرض کن هرکدوم از این کلاس ها هم برای ساخته شدن چند تا کلاس دیگه می خوان و و و . پس می بینی هر جا NetworkService رو بخواهیم حدود ۱۰ الی ۲۰ خط کد تکراری بنویسیم. در عووض توسط Dagger یکبار می گویم هرکی این کلاس و خواست این کلاس چطوری درست میشه! سپس هرکی این کلاس رو خواست فقط با یک @Inject اون خواهد داشت و به راحتی استفاده می کنه. البته این یکی از کاربرد های Dagger هست و کلی محاسن دیگه هم داره که یکی از مهمترینش هندل کردن LifeCycle اون کلاس هست. یعنی اون کلاس کی تولید بشه و کی از بین بره؟ تاکی نگهش داره و و و.
فکر می کنم اگر توضیحات و متوجه شدین آموزش دوم را هم ببینید و کاملاً روشن خواهید شد.
عموما چیزهایی که میخوام یاد بگیرم رو از وبسایت های اصلی و Stackoverflow استفاده میکنم.
منتهی این Dagger حقیقتاً چیز عجیب غریب و گیج کننده ایه.
تا بالاخره سراغ وبسایتهای فارسی اومدم ولی بازم نتیجه ناامید کننده بود. چون حس کردم نویسنده های اون مقالات هم خودشونم دقیقا درک درستی از Dagger ندارند و فقط مقاله ی دیگری رو ترجمه کردند.
و اما، مقاله شما بالاخره یه چیزایی رو روشن کرد برام. گرچه هنوز درست هضم نشده و نیاز به تمرین عملی دارم.
خیلی خوب توضیح دادید. آفرین
و اما یک سوال، آیا وقتی میخوایم با استفاده از Constructor اینجکت کنیم، لازم نیست انوتیشن @Inject قبل از Constructor استفاده بشه؟!
سلام. خوشحالم که براتون مفید بوده. بخش دوم آموزش رو بخونید فکر می کنم بخش های مبهم دیگر روشن بشن. سعی کردم همه چیز رو بصورت دسته بندی شده توضیح بدم اما پیشنهادم حتماً دیدن کدهای نمونه انتهای مقاله و تمرین خودتون هست.
و اما سوالتون: بله اگر قرار باشد باید بالای Constructor انوتیشن Inject را قرار بدین، فقط هواستون باشه ورودی های Constructor در Component مربوطه Dispose شده باشند. در آموزش بعدی کلاس Database رو ببینید. خودش constructor Inject شده و وردی آن Context در کامپوننت اصلی export(expose) شده تا هر کلاسی که وابستگی داشت بتونه از Context استفاده کنه.
بازهم ابهامی بود حتماً مطرح کنید.
private void generateDaggerCode() {
DaggerMotorComponent.builder().motorModule(new MotorModule
(this, “Computer name”,12, “radiator name”)).build().inject(this);
}
ممنون از مقاله بسیار مفیدتون
فقط ی سوال اینکه من اینجا دگرموتورکامپوننت رو نتونستم بفهمم از کجا میاد چون برا من خطا داره این قسمت…کجا باید ساخت و محتواش چیه؟
خواهش می کنم. ببینید خود dagger در کد generate شده اش کامپوننت رو بر می گردونه که ما می تونیم یا بگیرمش و در یک متغیر بگذاریمش یلا اینکه inline از component استقاده کنیم و متدهاشو صدا بزنیم. توجه کنید DaggerMotorComponent کد تولید شده خود dagger است که با پاس دادن module هایش کامپوننت مورد نظر ما رو تولید می کنه. اگر دقت کنی می بینی که در تعریف کلاس کامپوننت در بالای کلاس modules = {MotorModule.class} را به عنوان ماژول پاس دادیم. پس وقتی می خواهیم در کد generate شده dagger (یعنی DaggerMotorComponent.builder() ) استفاده کنیم باید ماژول هایی راکه برایش تعریف کرده بودیم و پاس بدیم و در نهایت متد موجود در component رو صدا بزنیم که یکیش void inject(MainActivity activty) و دیگریvoid inject(Motor moto); بوده که در خط بعدی صدا زده شده.
سلام
ممنونم از وقتی که میزارین
متاسفانه دوباره متوجه نشدم
اگر امکانش هست کد این برنامه تون رو بدید من ببینم اون خط چطوری باید پیاده بشه
ممنونم
telegram : smo2020
قهمیدم ممنونم
فقط کاش قبلا از سیمکشی میگفتید پروژه رو کامپایل کنید تا دگر قابل استفاده مارو بسازه
ممنونم در کل…ببخشید اولین بارمه دارم از دگر استفاده میکنم زیاد آشنا نبودم وتازه کارم
خواهش می کنم، این چه حرفیه خوشحالم که برات مفید بوده. فکر می کنم الآن وقتشه که بری سراغ بخش پیشرفته 😉
مدت ها بود میخواستم یاد بگیرمش، از چند روز پیش که شروع کردم به یادگیریش هر چی مقاله و ویدئو دیدم موفق به درک روند کار نشدم، تا اینکه گفتم شاید آموزش فارسی موجود باشه که گوگل منو به اینجا رسوند 😌 عالی بود، لطفا ادامه بدید.
خواهش می کنم. اتفاقا راجع به این موضوع هرچی بیشتر صحبت و بحث بشه بهتره و بهتر جا میافته. خوشحال می شم اگر سوالی داشتی مطرح کنی.
مرسی از مقالتون
خواهش می کنم
مطالبتون رو دنبال میکنم عموما از اینکه کاملا کاربردی توضیح میدید بسیار لذت میبرم و نهایت تشکر رو دارم.
ممنون از پیغام شما. خیلی خوشحالم که مورد توجه قرار گرفته.
بسیار عالی .سپاس فراوان
خواهش می کنم