نکاتِ خیلی مهمِ کدنویسیِ تمیز در اندروید

در برنامه نویسی اندروید یکی از نکات مهمی که نمیتونیم ازش غافل بشیم کدنویسی تمیزه. و بدون کد تمیز امکان نگهداری و توسعه اپلیکیشن سخت و حتی میتونه غیر ممکن باشه! برای همین یک سری نکات برای رعایت این اصول وجود داره که توی این مقاله بهش اشاره میکنیم (اینجا نمیخوام درباره کلین کد و اینا حرف بزنم. قضیه ساده تر و مهم تر از این حرفاست که اغلب رعایت نمیشه):1.Project guidelines1.1 Project structure پروژه ها باید از ساختار دستورالعمل های پروژه پیروی کنند که در این لینک (Android Gradle plugin user guide) تعریف شده. پروژه ribot Boilerplate یه منبع خوب برای یادگیری این مورده.1.2 Project structure 1.2.1 Class filesکلاس ها باید با قوانین UpperCamelCase نام گذاری بشوند.کلاس هایی که از کلاس کامپوننت اندروید ارث بری شدن باید به اسم همون کامپوننت ختم بشن. به عنوان مثال:SignInActivity, SignInFragment,ImageUploaderService,ChangePasswordDialog.1.2.2 Resources filesریسورس ها باید از قوانین lowercase_underscore پیروی کنند.1.2.2.1 Drawable filesقرارداد اسم گذاری برای برای drawable ها:قرارداد اسم گذاری برای برای icon ها(Android iconography guidelines):قرارداد اسم گذاری برای برای selector state ها:1.2.2.2 Layout filesلایوت ها باید با کامپوننت هاشون مچ بشن. مانند تصویر زیر:1.2.2.3 Menu filesمشابه بالا نام منو ها باید با نام کامپوننتش مطابقت داشته باشه. به عنوان مثال اگه ما یک menu تعریف کنیم که توی UserActivity استفاده میشه پس اسمش باید activity_user.xml باشه.اینکه کلمه menu بخشی از اسم منو باشه روش درستی نیست چون که خودش داخل menu directory قرار داره پس مشخص شده که این فایلِ xml یک منو هست.1.2.2.4 Values filesریسورس فایل هایی که باید توی پوشه values قرار داشته باشنم. مانند:strings.xml, styles.xml, colors.xml, dimens.xml, attrs.xml2.Code guidelines2.1 Java language rules2.1.1 Don't ignore exceptionsهرگز استثناها رو به حال خودشون رها نکنید:void setServerPort(String value) { try { serverPort = Integer.parseInt(value); } catch (NumberFormatException e) { } }همونطور که می بینید کد بالا به دلیل این که پیش بینی میکنیم هیچوقت وارد قسمت catch نمیشه اون رو خالی گذاشتیم! بهتره هیچوقت اینکار رو نکینم! راه درست و اصولی اینه که همه exception ها رو هندل کنیم و اونا رو به حال خودشون رها نکنیم.2.1.2 Don't catch generic exceptionکاری که تو کد پایین انجام شده اشتباهه:try { someComplicatedIOFunction(); // may throw IOException someComplicatedParsingFunction(); // may throw ParsingException someComplicatedSecurityFunction(); // may throw SecurityException // phew, made it all the way } catch (Exception e) { // I'll just catch all exceptions handleError(); // with one generic handler! }این کار اصولی نیست که استثناها رو به شکل عمومی هندل کنیم. چون اینکار باعث میشه که exception هایی که هرگز انتظارش رو نداریم(شامل استثناهای زمان اجرا مثل: ClassCastException) در سطح اپلیکیشن اتفاق بیوفته. این به این معنیه که اگه کسی استثنایی اضافه کنه چون ما بهش گفتیم که در صورت بروز هر نوع خطایی به exception عمومی بره پس کامپایلر اون استثنارو هندل نمیکنه!برای فهم بهتره اینکه چرا نباید اینکار رو بکنیم میتونید به این لینک برید.2.1.3 Don't use finalizersدر اینجا finalizer هنگام اجرای زباله روبی (یا همون garbage collected خودمون) اجرای میشه در حالی که اینکار میتونه برای پاک کردن منابع اللخصوص منابع خارجی مفید باشه اما مشخص نیست اون چه زمانی صدا زده میشه(یا حتی اصلا صدا زده میشه یا نه؟!)اندرویید از finalizer ها استفاده نمیکنه. پس بجاش میتونیم با هندل کردن درست exception ها اینکار رو انجام بدیم. اما اگه واقعا نیاز به استفاده ازش داشتیم میتونیم یک تابع مانند ()close بنویسیم و با نوشتن یک دستورشرطی هر موقع که نیاز بود ازش استفاده کنیم.2.1.4 Fully qualify importsزمانی که میخوایم از کلاسی مثل Bar در پکیج foo استفاده کنیم دو راه داریم.1- راه بد: import foo.*;2-راه خوب:import foo.Bar;اگه به همه کلاس های foo نیاز نداریم پس بهتره فقط اونی که نیاز داریم رو import کنیم.2.2 Java style rules2.2.1 Fields definition and namingفیلد ها باید بالای فایل تعریف بشن و از قوانین اسم گذاری زیر پیروی کنند:اسم فیلد های پرایویت و غیر استاتیک باید با حرف m شروع بشن.اسم فیلد های پرایویت و استاتیک باید با حرف s شروع بشن.اسم بقیه فیلدها باید با حرف کوچیک شروع بشن.همه حروف فیلد های فاینال و استاتیک باید با حروف بزرگ نوشته بشه و با آندرلاین جدا بشه.مثال:public class MyClass { public static final int SOME_CONSTANT = 42; public int publicField; private static MyClass sSingleton; int mPackagePrivate; private int mPrivate; protected int mProtected; }2.2.3 Treat acronyms as wordsبا کلمات اختصاری مثل حروف رفتار کنید. مثلا آیدی رو باید به شکل Id بنویسیم نه ID. مانند عکس زیر:2.2.4 Use spaces for indentationاز فضاها برای تو رفتگی ها استفاده کنید.4 فاصله برای بلاک هاif (x == 1) { x++; }8 فاصله برای line wrap ها(فارسیش نمیدونم چی میشه:( )Instrument i = someLongthat, wouldNotFit, on, one, line);2.2.5 Use standard brace styleبراکت ها رو درست توی همون خط بزارید نه یک بعدش.class MyClass { int func() { if (something) { // ... } else if (somethingElse) { // ... } else { // ... } } }استفاده از براکت ها ضروریه درصورتی که کد ما بیشتر از یک خط باشه.اگه کد از ماکسیمم طول خط کمتره استفاده از براکت ها ضروری نیست.کد خوب:if (condition) body();کد بد:if (condition) body(); // bad!2.2.6 Annotations2.2.6.1 Annotations practicesبرخی از annotation ها با توجه به استاندارد اندروید عبارت اند از:- هر زمان که میخوایم متدی در کلاس والد رو override کنیم باید از Override@ در بالای متدمون استفاده کنیم.- ممکنه گاهی متدما یک warning داشته باشه اما ما اطمینان داریم که هشدار برطرفه میشه در این مواقع میتونیم از SuppressWarnings@ استفاده میکنیم. این کار باعث میشه کامپایلر هشدار مربوط به اون متد رو نادیده بگیره.برای مطالعه بیشتر میتونید به این لینک مراجعه کنید.2.2.6.2 Annotations styleکلاس ها، متدها، سازنده ها:زمانی که annotation ها به یک کلاس، متد یا سازنده اضافه میشوند باید هر کدام در یک خط جداگانه نوشته بشوند./* This is the documentation block about the class */ @AnnotationA @AnnotationB public class MyAnnotatedClass { }فیلدها:زمانی که annotation ها به یک فیلد اضافه میشن باید در یک خط نوشته بشن مگر اینکه طول اونها از ماکسیمم طول خط بیشتر باشه.@Nullable @Mock DataManager mDataManager;2.2.7 Limit variable scopeدامنه متغیرهای محلی باید حداقل باشه. با انجام اینکار ما خوانایی و قابلیت نگهداری کدمون رو افزایش و احتمال بروز خطا رو کاهش میدیم. هر متغیر باید در محدوده بلاکی تعریف بشه که اون متغیر فقط در اون محدوده تاثیر گذاره.متغیر های محلی باید زمانی تعریف بشن که برای اولین بار استفاده میشن. همه متغیر های محلی زمانی که تعریف میشن باید دارای مقدار اولیه باشن. اگه اطلاع کافی برای مقداردهی اولیه یک متغیر محلی نداریم باید تا زمان تعریف کردن اون دست نگهداریم.2.2.8 Order import statementsترتیب import ها: ترتیب import ها رو خود اندروید استدیو رعایت میکنه پس لازم نیست نگران این مورد باشید اما دونستنش خالی از لطف نیست.ترتیب به این صورته:1 - import های اندروید2 - import های third parties(com, junit, net, org)3 - import های java و javax4 - import های همون پروژه2.2.9 Logging guidelinesاستفاده از متد های لاگ برای شناسایی مشکلات در اختیار برنامه نویس ها قرار میگیره.Log.v(String tag, String msg) (verbose)Log.d(String tag, String msg) (debug)Log.i(String tag, String msg) (information)Log.w(String tag, String msg) (warning)Log.e(String tag, String msg) (error)یک قانون کلی وجود داره که ما از اسم کلاس ها به عنوان tag استفاده میکنیم و اون رو بالای یک کلاس به شکل static final تعریف میکنیم.public class MyClass { private static final String TAG = MyClass.class.getSimpleName(); public myMethod() { Log.e(TAG, &quotMy error message&quot); } }لاگ های VERBOSE و DEBUG باید هنگام ساخت نسخه نهایی غیر فعال بشن. حتی خوبه که بقیه لاگ ها هم غیرفعال بشن اما ممکنه که تشخیص بدید که استفاده از لاگ ها ممکنه برای کشف مشکلات مفید باشه که در اون صورت باید مطمئن بیشید که اطلاعاتی مثل آدرس ایمیل، آیدی و... نشت نکنه!برای اینکه لاگ هارو در نسخه دیباگ نشون بدیم:if (BuildConfig.DEBUG) Log.d(TAG, &quotThe value of x is &quot + x);2.2.10 Class member orderingهیچ راه حل درستی برای ترتیب اعضای کلاس وجود نداره اما میتونیم از یک روش منطقی و ثابت برای خوانایی کدمون استفاده کنیم.ترتیبی که توصیه میشه به شکل زیره:ConstantsFieldsConstructorsOverride methods and callbacks (public or private)Public methodsPrivate methodsInner classes or interfacesبه عنوان مثال:public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private String mTitle; private TextView mTextViewTitle; @Override public void onCreate() { ... } public void setTitle(String title) { mTitle = title; } private void setUpView() { ... } static class AnInnerClass { } }اگه کلاسمون از کامپوننت های اندروید مثل اکتیویتی و فرگمنت ارث بری میکنه و متد های لایف سایکل رو اورراید میکنیم ترتیب درست به شکل زیره:public class MainActivity extends Activity { //Order matches Activity lifecycle @Override public void onCreate() {} @Override public void () {} @Override public void () {} @Override public void onDestroy() {} }2.2.11 Parameter ordering in methodsتوی اندروید ممکنه ما متدی بنویسیم که از Context استفاده میکنه. در این صورت Context باید اولین پارامتر ورودی متدمون باشه.اگه متد ما نیاز به یک اینترفیس callback داره برعکسِ Context اون باید آخرین پارامتر ورودی متدمون باشه.مثال:// Context always goes first public User loadUser(Context context, int userId); // Callbacks always go last public void loadUserAsync(Context context, int userId, UserCallback callback);2.2.12 String constants, naming, and valuesالمان های زیادی در اندروید وجود داره که برای استفاده از اونها ممکنه مجبور بشیم که ثابت های رشته ای تعریف بکنیم. حتی در برنامه های کوچک. مانند SharedPreferences، Bundle یا Intent باید اونها رو به شکل final static تعریف کنیم. و نام اونها رو باید به شکل پیشوند بنویسیم. این پیشوند ها باید به شکل زیر باشند.مثال:// Note the value of the field is the same as the name to avoid duplication issues static final String PREF_EMAIL = &quotPREF_EMAIL&quot static final String BUNDLE_AGE = &quotBUNDLE_AGE&quot static final String ARGUMENT_USER_ID = &quotARGUMENT_USER_ID&quot // Intent-related items use full package name as value static final String EXTRA_SURNAME = &quotcom.myapp.extras.EXTRA_SURNAME&quot static final String ACTION_OPEN_USER = &quotcom.myapp.action.ACTION_OPEN_USER&quot2.2.13 Arguments in Fragments and Activitiesزمانی که میخوایم دیتایی رو ازطریق intent یا bundle به اکتیویتی یا فرگمنت منتقل کنیم باید کلیدها برای دو مقدار متفاوت از قوانین بالا پیروی کنه.زمانی که یک اکتیویتی یا فرگمنت انتظار یک آرگومان رو داره، در این صورت باید یک متد public static برای تسهیل ایجاد intent یا فرگمنت مناسب بنویسیم.در مورد اکتیویتی ها این متد با اسم ()getStartIntent نوشته میشه. مانند کد زیر:public static Intent getStartIntent(Context context, User user) { Intent intent = new Intent(context, ThisActivity.class); intent.putParcelableExtra(EXTRA_USER, user); return intent; }در مورد فرگمنت ها این متد ()newInstance نام داره و مثل کد زیر نوشته میشه:public static UserFragment newInstance(User user) { UserFragment fragment = new UserFragment(); Bundle args = new Bundle(); args.putParcelable(ARGUMENT_USER, user); fragment.setArguments(args) return fragment; }نکته اول: این متدها باید بالای کلاس و قبل از متد ()onCreate قرار بگیرند.نکته دوم: اگه از روش های بالا استفاده کردیم کلید ها باید به صورت private تعریف بشند چون دیگه بیرون از کلاس به اونها نیازی نیست.2.2.14 Line length limitطول خطوط کد نباید از 100 کاراکتر بیشتر بشه. اگه کد ما بیشتر از اینه پس باید با دو روش زیر تقسیم بشه.استفاده از یک متغیر محلی یا یک متد.(ترجیحا)تقیسم کردن یک خط به چند خط زیر هم.در این مورد دو تا استثنا وجود داره که ممکنه از 100 کارکتر بیشتر بشه: خطوطی که امکان تقسیم شدن رو ندارند. مانند URL ها طولانی در کامنت هاتعریف ها مربوط به import ها و package ها.2.2.14.1 Line-wrapping strategiesبرای توضیح دادن line_wrap یک فرمول دقیق و مشخصی وجود نداره. اما تعداد کمی قانون متداول وجود داره.Break at operatorsبرای شکستن خطوط طولانی میتونیم اون رو قبل از عملگر ها بشکنیم مثل کد زیر:int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;Assignment Operator Exceptionیک قانون دیگه وجود داره که در اون میتونیم این شکستن رو بعد از عملگر مساوی(=) انجام بدیم. مثل کد زیر:int longName = anotherVeryLongVariable + anEvenLongerOne - thisRidiculousLongOne + theFinalOne;Method chain caseزمانی که چند متد به شکل زنجیر وار صدا زده میشن(در واقع وقتی داریم از builder ها استفاده میکنیم) میتونیم بعد از صدا زدن متد و قبل از کاراکتر (.) این شکستن رو انجام بدیم. مثل کد زیر:Picasso.with(context).load(&quothttp://ribot.co.uk/images/sexyjoe.jpg&quot).into(imageView);Picasso.with(context) .load(&quothttp://ribot.co.uk/images/sexyjoe.jpg&quot) .into(imageView);Long parameters caseوقتی یک متد پارامتر های زیادی داره و باعث طولانی تر شدن خط ما میشه ما باید در هر پارامتر و بعد از کاما(,) این شکستن رو انجام بدیم. مثل کد زیر:loadPicture(context, &quothttp://ribot.co.uk/images/sexyjoe.jpg&quot, mImageViewProfilePicture); loadPicture(context, &quothttp://ribot.co.uk/images/sexyjoe.jpg&quot, mImageViewProfilePicture, clickListener);2.2.15 RxJava chains stylingدر استفاده از متدهای زنجیره ای rxjava باید قبل از نقطه(.) شکستن اتفاق بیوفته. مثل کد زیر:public Observable<Location> syncLocations() { return mDatabaseHelper.getAllLocations() .concatMap(new Func1<Location, Observable<? extends Location>>() { @Override public Observable<? extends Location> call(Location location) { return mRetrofitService.getLocation(location.id); } }) .retry(new Func2<Integer, Throwable, Boolean>() { @Override public Boolean call(Integer numRetries, Throwable throwable) { return throwable instanceof RetrofitError; } }); }2.3 XML style rules2.3.1 Use self closing tagsاگه یک المان در XML هیچ محتوایی در داخلش نداره پس باید اون رو در خودش ببندیم مانند کد زیر:کد خوب:<TextView android:id=&quot@+id/text_view_profile&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot />کد بد:<!-- Don\'t do this! --> <TextView android:id=&quot@+id/text_view_profile&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot > </TextView>2.3.2 Resources namingآیدی ریسورس ها باید به شکل lowercase_underscore انجام بشه.2.3.2.1 ID namingآیدی باید به شکل پیشوند و با نام المان شروع بشه:مثال:<ImageView android:id=&quot@+id/image_profile&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot /> <menu> <item android:id=&quot@+id/menu_done&quot android:title=&quotDone&quot /> </menu>2.3.2.2 Stringsنام رشته باید با یک پیشوند از بخشی که به اون تعلق داره شروع بشه. برای مثال: registration_email_hintیاregistration_name_hintاگه یک رشته به هیچ بخش مشخصی تعلق نداره پس باید از قوانین زیر پیروی کنه:2.3.2.3 Styles and Themesبرخلاف بقیه ریسورس ها استایل ها باید با قانون UpperCamelCase.نام گذاری بشن.2.3.3 Attributes orderingبه عنوان یک قاعده کلی باید سعی کنید attribute های مشابه رو با هم گروه بندی کنیم. یک راه خوب برای مرتب سازی اونها به شکل زیره:View IdStyleLayout width and layout heightOther layout attributes, sorted alphabeticallyRemaining attributes, sorted alphabetically2.4 Tests style rules2.4.1 Unit testsکلاسهای آزمون باید با نام کلاس مورد نظر مطابقت داشته باشند. مثلا اگه ما کلاس تستی داشته باشیم که مربوط به DataBaseHelper پس باید اسم کلاس تستمون DataBaseHelperTest باشه.متد تست با Test@ حاشیه نویسی میشه و باید با نام متدی که تست میشه شروع بشه و بعد پیشبینی و/یا رفتار مورد انتظار نوشته بشه.به مثال زیر نوجه کنید:Template: @Test void methodNamePreconditionExpectedBehaviour()Example: @Test void signInWithEmptyEmailFails()اگه اسم متد تست به اندازه کافی واضحه پس نیازی نیست به اضافه کردن پیشبینی و/یا رفتار مورد انتظار نیست.گاهی ممکنه کلاسی دارای متدهای بزرگ زیادی باشه، و اغلب نیازه که برای هر کدوم چندین تست نوشته بشه. در این صورت خوبه که کلاس های تست رو به چند کلاس مختلف تقسیم کنیم. برای مثال فرض کنید کلاسی به اسم DataManager وجود داره که دارای متدهای بزرگ زیادیه، پس ما میتونیم اونو به کلاس های تست DataManagerSignInTest ،DataManagerLoadUsersTest و ... تقسیم کنیم.2.4.2 Espresso testsمعمولا هر کلاس تست Espresso برای یک اکتیویتی نوشته شده پس نام اون باید با نام اکتیویتی تطبیق داشته باشه مثل: SignInActivityTestباید هنگام استفاده از Espressso هر متد رو در خط جداگانه ای نوشت.onView(withId(R.id.view)) .perform(scrollTo()) .check(matches(isDisplayed()))خب این مقاله تموم شد.امیدوارم که خوب ترجمه کرده باشم. لینک اصلی مقاله اینجاست و اینجاست.اگه جایی رو بد توضیح دادم یا اشتباه نوشتم ممنون میشم بهم بگین.