آموزش ایجاد فایل با کاتلین از اندروید 5 تا 13

آموزش ایجاد فایل با کاتلین از اندروید 5 تا 13
در این پست می‌خوانید:

به نام خدا، قصد دارم در این مقاله به آموزش ایجاد فایل در اندروید بپردازم. همونطور که می دونی ایجاد فایل در اندروید بسیار می تونه با توجه به تغییر پیاپی سیاست های گوگل در نسخه های مخلف اندروید، چالش برانگیز باشه. از این رو برآن شدم تا در مقاله کنونی، چگونگی مدیریت مجوز ایجاد فایل از نسخه 5 تا 13 اندروید؛ چگونگی ایجاد فایل؛ باید ها و نباید ها در انتخاب مسیر ذخیرۀ فایل و خیلی چیزای دیگه رو پوشش بدم.

این آموزش به صورت کاربردی درنظر گرفته شده است و از خواننده انتظار می ره پس از مطالعۀ آن، در زمینۀ ایجاد فایل در حافظۀ گوشی مشکلی نداشته باشه.

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

مجوز ایجاد فایل در نسخه های متخلف اندروید

همونطور که می دونی از اندروید شش (نامیده شده به Marshmello) و به بالا، برای ایجاد فایل یا خیلی از کارهای حائز اهمیت دیگه، باید به صورت مستقیم و در حین اجرای برنامه از کاربر مجوز بگیری وگرنه سیستم عامل اندروید بهت اجازۀ ایجاد فایل رو نخواهد داد. منتها از اندروید مارشملو (API 23) و به بالا نحوۀ دریافت مجوز متفاوته؛ بعد از آنکه مطمئن شدی مجوز ایجاد فایل در گوشی کاربر رو داری، می تونی فایلت رو بدون مشکل بسازی.

جهت جامعیت بیشتر مقاله، مجوز خواندن از فایل را هم در کنار مجوز ایجاد فایل پوشش می دم.

در گام اول درخواست مجوز ها را در فایل Manifest به صورت زیر می نویسیم:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <!--max version is 29 and after that no need-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29" />

مجوز READ_EXTERNAL_STORAGE برای خواندن فایل و مجوز WRITE_EXTERNAL_STORAGE برای ایجاد فایل می باشه، در باب android:maxSdkVersion="29" وقتی به اندروید 10 (که API 29 باشد) رسیدیم، صحبت خواهیم کرد.

همچنین کد زیر رو به برچسب application درون فایل Manifest اضافه کن، دربارۀ این کد هم بعدا صحبت می کنیم:

<application
        android:requestLegacyExternalStorage="true"

فعلا فایل Manifest همینطوری بمونه

در ادامۀ مقاله گام به گام پروژۀ خودمون رو با تحت پوشش قرار دادن نسخه ها مختلف اندروید غنی خواهیم کرد، پس با ما همراه باشید.

اگر برای اولین بار است به مفهوم مجوز (Permission) در اندروید بر می خوری این مقاله رو بخون

دریافت مجوز ایجاد فایل در اندرویدهای قبل از 6 (API 23)

اگر نسخۀ اندرویدی کاربر کمتر از API 23 یا همان اندروید 6 باشد، نیازی به درخواست مجوز از کاربر نیست، برای آنکه مطمئن یشی نسخۀ اندروید گوشی کاربر کمتر از اندروید 6 هست یا نه، از شرط زیر استفاده کن، در صورت درست بودن شرط؛ تو از مجوز برای ایجاد یا خواندن فایل برخوردار خواهی بود.

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
       //مجوز برای ایجاد یا خواندن فایل وجود دارد
}

دریافت مجوز ایجاد فایل برای اندروید 13 و به بالا (API 33)

ایجاد فایل در اندروید 13

مژده! برای اندروید 13 (نامی به Tiramisu) و به بالا، نیازی نیست درگیر دردسر های مجوز گرفتن از کاربر بشی

فقط کافیه درون دستور شرطی مطمئن شوی که نسخۀ اندروید کاربر، 13 و به بالاست؛ آنگاه می تونی فایل خودت رو بسازی:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){
       //مجوز برای ایجاد یا خواندن فایل وجود دارد
}

دریافت مجوز ایجاد فایل برای اندروید های 11 و 12 (API 30 ,31 ,32)

ایجاد فایل در اندروید 11 و 12

در اندروید های 11 و 12 دیگر نیازی به مجوز WRITE_EXTERNAL_STORAGE نیست و برای همین اتریبیوت android:maxSdkVersion="29" رو در uses-permission   WRITE_EXTERNAL_STORAGE داخل مانیفست قید کردیم، از اندروید 13 و به بالا هم نیازی به این مجوز نبود. در اندروید های 11 و 12 کفایت می کند READ_EXTERNAL_STORAGE را بررسی کنیم و از اندروید 13 و بالاتر نیازی به همین هم نیست. شاید این نشون می ده گوگل با انجام تمهیداتی در اندروید های بالاتر سعیِ بر کاهش دردسر برنامه نویسان اندروید رو در پیش گرفته.

else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
                    ContextCompat.checkSelfPermission(
                        activity,
                        Manifest.permission.READ_EXTERNAL_STORAGE
                    ) == PackageManager.PERMISSION_GRANTED){
        //فقط کافیست وجود داشتن مجوز خواندن را بررسی کنیم
}

ما از else if استفاده کردیم زیرا در شرط های بالاتر، اندروید 13 و به بالا را در نظر گرفته بودیم و حتما نسخۀ اندروید کاربر کمتر از 13 هست که تا به این شرط رسیده.

اگر می خواهید به جدید ترین روش های برنامه نویسی اندروید با استفاده از کاتلین مسلط بشید و همۀ معماری های موجود در اندروید را یاد بگیرید در دورۀ نخبگان اندروید محمد نوری با بیش از ده سال تجربه شرکت کنید

دریافت مجوز ایجاد فایل از اندروید 6 تا 10 (API 23 تا API 29)

ایجاد فایل در اندروید 10

توجه داشته باشید اندروید 10را به این علت قاطی اندروید های 6 تا 9 کردیم که در Manifest داخل برچسب application، آن یک خط کد یاد شده رو قرار دادیم، یعنی:

<application
        android:requestLegacyExternalStorage="true"

در واقع گوگل هنگامی که اندروید 10 عرضه شده بود یک فرصت آخر به برنامه نویسان داد تا با افزودن این خط کد، همچنان همان کارهایی که برای دریافت مجوز ایجاد فایل و خواندن فایل که در اندروید های 6 تا 9 انجام می دادند، برای بار آخر در پیش بگیرند. اندروید 10 همان API 29 هست.

از API 29 به بعد دیگر نیازی به مجوز WRITE_EXTERNAL_STORAGE نبود و ما در maxSdk آن مجوز قید کردیم که فقط تا API 29 رو پشتیبانی کنه؛

اگر اتریبیوت requestLegencyStorage را برابر با true قرار نمی دادیم آنگاه باید maxSdk رو در مجوز WRITE_EXTERNAL_STORAGE برابر با 28 بجای 29 قرار می دادیم، زیرا فقط با وجود این خط کد ما می توانیم اندروید 10 رو قاطی اندروید 6 تا 9 کنیم و به همون صورتی که برای اندروید 6 تا 9 مجوز خواندن و ایجاد فایل رو از کاربر می گرفتیم، همچنان از کاربر بگیریم.

جهت آشنایی با انواع نسخه های اندرویدی به اینجا سر بزن.

پس برای بررسی وجود داشتن مجوز خواندن و ایجاد فایل در اندروید های 6 تا 10، کفایت می کنه شرط زیر را بررسی کنیم:

else if (ContextCompat.checkSelfPermission(
                activity,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
                activity,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED
        ){
       //مجوز وجود دارد
}

اما اگر مجوز وجود نداشته باشد و بخواهیم درخواست مجوز از کاربر بگیریم باید چکار کنیم؟

کافیه == ها رو برداریم و != رو بجاشون جایگذاری کنیم و درون { } هم کد درخواست مجوز را قرار بدهیم. کد زیر رو ببین:

if (ContextCompat.checkSelfPermission(
                activity,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
                activity,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            activity.requestPermissions(
                arrayOf(
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                ),
                STORAGE_PERMISSIONS_RC_2
            )
        }

همونطور که می بینی، با استفاده از تابع requestPermission که درون اکتیویتی هست، می تونی مجوز های خواندن و ایجاد فایل رو از کاربر بخوای، STORAGE_PERMISSIONS_RC_2 هم یک ثابت هست که درون خودش یک عدد ثابت رو داره(مثلا 1122) که بهش requestCode می گن و بعد از پذیرش یا رد کردن مجوز توسط کاربر، می تونیم با استفاده از این عدد، درخواستمون رو در یک تابع دیگه که بعدا دربارش شرح خواهیم داد، پیگیری کنیم.

نوشتن یک تابع جامع برای بررسی مجوز ایجاد فایل در اندروید:

می خواهیم در قالب یک تابع جامع همۀ موارد بالا رو با هم قرار بدیم تا به صورت جامع بتوانیم صحت مجوز ایجاد فایل رو تا API 33 تضمین کنیم: اگر مجوز بود که true رو در یک متغییری ذخیره کن، اگر نبود، مجوز رو از کاربر درخواست کن(اگر مجوز توسط کاربر به برنامه اعطا شد، آنگاه true رو توی همون متغییری که گفتیم حالا بیا ذخیره کن[آن متغییر مقدار پیشفرضش false هست یعنی به طور پیش فرض ما درنظر می گیریم که مجوز نداریم])

اول دوتا ثابت برای requestCode ها تعریف (یعنی زمانی که می خوایم درخواست مجوز برای کاربر به صورت runtime در قالب یک دیالوگ بفرستیم) می کنیم:

//API >= 30
const val STORAGE_PERMISSIONS_RC_1 = 1023
//API < 30 & API > M
const val STORAGE_PERMISSIONS_RC_2 = 1024

ثابت اولی برای اندروید یازده و به بالا، ثابت دومی برای اندروید های 6 تا 10 هست.

isStoragePermissionGranted = false
.
.
.
fun myPermissionManager() {
        //if permission is ok
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
            || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
            || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
                    ContextCompat.checkSelfPermission(
                        this,
                        Manifest.permission.READ_EXTERNAL_STORAGE
                    ) == PackageManager.PERMISSION_GRANTED)
            || (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) == PackageManager.PERMISSION_GRANTED)
        ) {
            isStoragePermissionGranted = true
        }
        //if it is >= api 30 and need requestPermission
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
            ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(
                arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                STORAGE_PERMISSIONS_RC_1
            )
        }
        //if api is in 23 and 29 and need request
        else if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(
                arrayOf(
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                ),
                STORAGE_PERMISSIONS_RC_2
            )
        }
    }

همونطور که می بینی تمام نیاز های ما برای بررسی وجود یا عدم وجود مجوز ایجاد فایل یا خواندن فایل در گوشی کاربر در تابع بالا پوشش داده می شه، اگر مجوز وجود نداشته باشه برای کاربر درخواست اعطای مجوز فرستاده می شه

در شرط اول بررسی می کنیم آیا مجوز ایجاد فایل و خواندن فایل رو داریم یا نه؟ اگر یکی از مجوزهای READ_EXTERNAL_STORAGE یا WRITE_EXTERNAL_STORAGE وجود نداشته باشه، وارد شرط دوم می شیم.

در شرط دوم می گه، اگر اندرویدت بزرگتر مساوی اندروید 11 بود(یعنی یا اندرویدت 12 بود یا اندرویدت 11 بود، چون اگر اندرویدت 13 باشه به صورت پیش فرض مجوزهای READ_EXTERNAL_STORAGE و WRITE_EXTERNAL_STORAGE وجود دارند) آنگاه به صورت پیش فرض مجوز WRITE_EXTERNAL_STORAGE رو داری، فقط می مونه بررسی کنی آیا مجوز READ_EXTERNAL_STORAGE توسط کاربر بهت داده شده یا نه، اگر مجوزش رو نداری از کاربر بخواه که بهت مجوزش رو بده.

کلا پروندۀ WRITE_EXTERNAL_STORAGE برای ایجاد فایل، در اندروید 11 و به بالا بسته می شه، نه تنها نیازی نیست اونو از کاربر مطالبه کنی، بلکه حتی نیازی نیست اونو توی Manifest قرار بدی، برای همینه maxSdk رو برابر با api 29 که همون اندروید 10 باشه قرار دادیم دیگه!

اگر این شرط هم برقرار نبود پس حتما اندروید گوشی کاربر باید بین 6 تا 10 باشه، چون اگر کمتر از 6 باشه یا بزرگتر مساوی 11 باشه باید توی دوتا شرط اولی پوشش داده شده باشه؛ پس توی شرط سوم میایم اندروید های 6 تا 10 رو پوشش می دیم و می گیم اگر مجوز های READ_EXTERNAL_STORAGE و WRITE_EXTERNAL_STORAGE هیچ کدوم نبودند، اونها رو از کاربر بخواه

حالا یک سوال! از کجا بفهمیم کاربر در واکنش به پیام درخواست مجوز از سوی ما برای ایجاد فایل، مجوز رو اعطا کرده یا مجوز رو رد کرده؟

override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>, grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        //check requests permissions
        when (requestCode) {
            //check external storage permissions
            STORAGE_PERMISSIONS_RC_1 -> {
                isStoragePermissionGranted =
                    grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
            }
            STORAGE_PERMISSIONS_RC_2 -> {
                isStoragePermissionGranted =
                    grantResults.size > 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED
                            && grantResults[1] == PackageManager.PERMISSION_GRANTED
            }
        }
    }

جواب تابع onRequestPermissionsResult هست، این تابع در دَمی که کاربر نسبت به درخواست ما برای دریافت مجوز واکنش نشون می ده، صدا زده می شه و نتیجه رو برای ما بر میگردونه.

علت آنکه ما از دو requestCode مختلف برای فرستادن پیام درخواست مجوز به کاربر استفاده کردیم آن بود که برای اندروید 11 و به بالا (یعنی تا اندروید 12) فقط کافیه ببینیم کاربر به READ_EXTERNAL_STORAGE مجوز داده یا نه؟ درواقع grantResults فقط یک آیتم داره ولی در اندروید های 6 تا 10 با توجه به آنکه دوتا درخواست می فرستیم، آرایۀ ما دوتا آیتم داره.

در واقع هدف ما از استفاده از دوتا requestCode صرفا پرهیز از بررسی عناصر آرایه و راحتی کارمان بود.

لازم به ذکر هست برای اندروید های 6 تا 10 باید ببینیم آیا هر دو مجوز درخواست شده از کاربر تایید شدن یا نه، مثلا من توی برنامم هم به مجوز ایجاد فایل و هم به مجوز خواندن فایل باهم نیاز دارم و اگر یکیشون نباشه ترجیح می دم isStoragePermissionGranted برابر با همون false باشه، یعنی هردوتاشو برای کارم میخوام و اگر یکی از اونها بهم داده بشه بدردم نمی خوره.

این سناریوی برنامۀ من بود ممکنه در برنامۀ تو سناریو فرق بکنه. نمیدونم توجه کردی یا نه؟ در تابع myPermissionManager من بررسی نکردم اگر در اندروید های 6 تا 10 یکی از مجوز های READ_EXTERNAL_STORAGE و WRITE_EXTERNAL_STORAGE چکار باید بکنه؟ من فقط گفتم اگر هیچ یک از این دو مجوز نبود درخواست مجوز بده!

تو برای تمرین می تونی این رو درنظر بگیری که اگر یکی از مجوز ها اعطا نشده بود، همون مجوزی که از سوی کاربر اعطا نشده دوباره از کاربر درخواست بشه!

خب تا اینجا یاد گرفتیم که آیا اکنون مجوز های لازم رو در اختیار داریم یا نه چطور مجوز ایجاد فایل و خواندن آن رو از کاربر بگیریم. میریم سراغ آنکه چطوریک پوشه با محتوای یک فایل txt در حافظۀ گوشی ایجاد کنیم.

آموزش ایجاد فایل و پوشه در حافظۀ داخلی گوشی اندروید کاربر

به این نکته توجه داشته باش! در اندروید های بالا، گوگل بنابر سیاست های خودش، بهت اجازه نمی ده به صورت مستقیم توی حافظۀ اصلی فایل یا پوشه ایجاد کنی، لازمه که درون پوشه های اصلی خود اندروید (Download ،Documents ،Music ،Pictures ،Movies و غیره) پوشه یا فایل مورد نظرت رو ایجاد کنی.

ایجاد فایل و پوشه در حافظۀ داخلی گوشی

همونطور که می بینی پوشه های اصلی(سه تا پوشۀ اولی و دوتا پوشۀ آخری، مثلا Music و Download)، دارای آیکن های مرتبط با موضوع خودشون در گوشۀ پایین سمت راستشون هم هستند. مثلا پوشۀ Music آیکن () رو پایین سمت راستش داره و پوشۀ Download علامت () رو به عنوان آیکن در پایین سمت راست خودش داره. اما پوشه های معمولی (📁) این آیکن ها رو ندارند.

اکنون ما می خواهیم یک فایل txt بسازیم که داخل یک پوشه ای با نام NouriAcademy هست. پس مناسب ترین پوشه، پوشۀ Documents هست چون موضوع آن مرتبط با فایلی که می خواهیم درست بکنیم می باشه. البته اجباری نیست حتما درون پوشۀ Documents فایلمون رو بسازیم، تو حتی می تونی توی پوشۀ Pictures فایلت رو بسازی؛ فقط مهم آنه که جزو پوشه اصلی باشه.

اول باید پوشۀ NouriAcademy رو بسازیم که بعدا فایل txtمون رو داخلش ایجاد کنیم؛ البته لازمم نیست حتما پوشه بسازیم ما می تونیم به صورت مستقیم فایلمون رو درون پوشۀ Documents ایجاد بکنیم!

val nouriAcademyPath = Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_DOCUMENTS + "/" + "NouriAcademy"
        )
    )

خب همونطور که در کد بالا مشاهده می کنی، کافیه به تابع Environment.getExternalStoragePublicDirectory به عنوان ورودی، اول Environment.DIRECTORY_DOCUMENTS رو بدیم. Environment.DIRECTORY_DOCUMENTS همون پوشۀ Documents ما رو اظهار می کنه. در ادامه “/” و نام پوشۀ NouriAcademy رو قرار می دیم. تابع مورد نظر یکدونه شئ از کلاس File برای ما می سازه.

try {
            if (!nouriAcademyPath.exists()) {
                //وجود نداره، پس یک پوشه بساز
                nouriAcademyPath.mkdirs()
            }
            
             val noteTitle = "MyNote"
             val noteTxt = File.createTempFile(noteTitle, ".txt", nouriAcademyPath)
             noteTxt.writeText("https://nouri.academy/?p=6301 | maziar lambda expression edu")
            
             val directory = nouriAcademyPath.toString()

            //show "$directory✅"
        }catch (e : Exception){
            //show e.message.toString()
        }

وقتی یک شئ از نوع کلاس Flie داشته باشیم، می تونیم با استفاده از تابع exists() بررسی بکنیم که آیا توی گوشی کاربر وجود داره یا نه، اگر وجود نداشت می گیم با استفاده از تابع mkdirs() اونو بساز!

با استفاده از تابع File.createTempFile() که سه تا پارامتر ورودی داره، می تونیم یک فایل txt بسازیم که مطمئنا نام آن منحصر به فرد خواهد بود. منحصر به فرد بودن نام فایل خیلی اهمیت داره و این تابع بهمون کمک می کنه تا نام فایلمون حتما منحصر به فرد باشه.

پارامتر اول این تابع، نام فایلمون هست که قبل از اعداد تصادفی ای که نام فایلمون رو منحصر به فرد می کنن قرار می گیره، به این پارامتر prefix یا پیشوند می گن. پارامتر دوم که بهش suffix یا پسوند می گن، همون پسوند فایل ما هست که برابر با “.txt” قرارش دادیم چون قصد ما ایجاد یک فایل متنی به صورت یادداشت بود.

پارامتر آخر هم، همون شئ کلاس Flie هست که آدرس پوشه ای که می خوایم فایل txt درونش ساخته بشه، توشه.

تمام! با کمک همین سه پارامتر یک فایل txt درون پوشۀ NouriAcademy درون پوشۀ Documents در حافظۀ گوشی کاربر ایجاد می شه!

حالا می مونه آنکه  درون فایل txtمون یک متنی رو وارد بکنیم. این کار به کمک Extension function ای با نام writeText() قابل انجامه، به عنوان ورودی یک متن بهش می دیم که قصد داریم درون فایل txt نوشته بشه. مثلا من نشانی یکی از مقالات سایت با موضوع آموزش لامبدا فانکشن در برنامه نویسی کاتلین با ۹ گام! رو قرار دادم و واژه هایی رو پس از اون نوشتم.

بلافاصله متن دلخواه ما در فایل txt درج می شه

 

خب ما ایجاد فایل رو یاد گرفتیم، حالا اگر بخوایم فایل و پوشه ای که خودمون ایجاد کردیم رو حذف کنیم باید چکار کنیم؟ مثلا الان می خوایم NouriAcademy/MyNote رو حذف کنیم (هم خود فایله رو و هم پوشۀ NouriAcademy رو)، یعنی هم پوشه ای که ساختیم و هم فایل داخلشو که باز خودمون ساختیمش. کافیه از خط کد زیر استفاده کنیم:

if (nouriAcademyPath.exists()){
                //delete it with all of its children files.
                nouriAcademyPath.deleteRecursively()
            }

هدف از نوشتن این مقاله روش مشکل ایجاد فایل در اندروید های مختلف بود که خود به خاطر یک پروژه ای باهاش روبرو شده بود.

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

جعبه دانلود فایل۱ فایل
سورس کد
گزارش خرابی لینک دانلود
فرم گزارش خرابی لینک دانلود
دوره‌های مرتبط
تکمیل شده
دوره آموزش پرداخت درون برنامه ای بازار و مایکت

دوره آموزش پرداخت درون برنامه ای بازار و مایکت

روز
ساعت
دقیقه
ثانیه
۱۱
۱۱
۴۰
۵۰۰ ۳۰۰ هزار تومان
تکمیل شده
دوره آموزش رایگان برنامه نویسی اندروید از صفر
تکمیل شده
آموزش پروژه محور برنامه نویسی اندروید - پروژه فروشگاه آنلاین

آموزش پروژه محور برنامه نویسی اندروید - پروژه فروشگاه آنلاین

روز
ساعت
دقیقه
ثانیه
۳۰
۱۸
۵۰
۹۹۹ ۴۹۹ هزار تومان
دیدگاه‌ها ۲
ارسال دیدگاه جدید