اگر جدیدا قصد برنامه نویسی اندروید را کردهاید، یا هنوز روشهای متدوالی را برای
کار با این زبان انتخاب نکردهاید؛ به نظرم این مقاله میتواند کمک خوبی
برای شما باشد. مسائلی که بیان میکنم در واقع از تجربیات شخصی و راه حل
هایی است که برای خودم تعیین کردهام و تعدادی از آنها را در طول مدتی که
در این زمینه فعالیت کردهام، از جاهای مختلف دیده و در یک جا گردآوری
کردهام. برای نامگذاری اشیاء و متغیرها و دیگر موارد، من از این
قاعده پیروی میکنم که به نظرم بسیار ایده آل میباشد. الگوی معماری هم که جدیدا مورد استفاده قرار دادهام، الگوی
MVP است که نمونهای از آن، در
گیت هاب قرار گرفته است. البته این
مثال ساده تر نیز وجود دارد. تشریح کامل این معماری را به همراه آزمون واحد آن، میتوانید در این
مقاله سه قسمتی ببینید.
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله
استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به
عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید
استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره
میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید
مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
/**
*
* <p>
* check network is available or not <br/>
* internet connection is not matter,for check internet connection refer to IsInternetConnected() Method in this class
* </p>
* <p>
* Required Permission : <b>android.permission.ACCESS_NETWORK_STATE</b>
* </p>
* @param context
* @return returns true if a network is available
*/
public boolean isNetworkAvailable(Context context) {
ConnectivityManager connectivityManager
= (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
همچنین اگر، مورد خاص دیگری مثل بالا بود، حتما آن را ذکر کنید. میتوانید از
تگ گذاری در کامنت ها
نیز استفاده کنید. از ویژگیهای کامنت todo در اندروید استادیو این است
که میتوانید در حین کار با سیستم گیت نیز از آن بهره ببرید و قبل از کامیت
کردن کد، کدهای todo به شما یادآوری شوند و هر پیکربندی را که لازم دارید، روی آن انجام دهید.
دو.
از یک کلاس واحد جهت استفاده از اطلاعات عمومی و یا ثابتها استفاده
نمایید. این اطلاعات میتوانند شامل: مسیرها، آدرسهای وب سرویس، شماره
اختصاصی هر نوتیفیکیشن و .... باشند. برای اینکار میتوان هر کدام از اطلاعات را
داخل یک کلاس قرار داد و همه این کلاسها را به صورت استاتیک تعریف کنید تا
بدین شکل در دسترس قرار بگیرند (از الگوی singleton هم میتوان استفاده
کرد).
public class ProjectSettings
{
public static NotificationsId=new NotificationsId();
public static UrlAddresss=new UrlAddresss();
public static SdPath=new SdPath();
......
}
نحوه صدا زدن هم به همین شکل میشود:
ProjectSettings.NotificationsId.UpdateNotificationId
بدین شکل هم به طور ساده و مفهومی صدا زده میشود و هم اینکه در همه جای
برنامه این ثابتها و مقادیر قابل استفاده هستند. به عنوان مثال به شماره
هر نوتیفیکیشن از همه جا دسترسی دارید و هم اینکه شمارهای تکراری اشتباها
انتخاب نمیشود.
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
یا اینکه برای یک دیالوگ مستقیما و در جا همانجا به کدنویسی مشغول میشوند.
این روشها هیچ مشکلی ندارند ولی در آینده نگهداری کد را مشکل میکنند.
مثلا تصور کنید شما بسیاری از جاهای برنامه، Toast زدید و حالا قصد دارید در
نسخه بعدی برنامه، toastهای دلخواه و یا custom ایی را ایجاد کنید. در این صورت
مجبورید کل برنامه را رصد کرده و هر جا toast هست آن را تغییر دهید. در
اینجا هم اصول DRY را نادیده گرفتهاید و هم زحمت شما زیاد شدهاست و حتی
ممکن است یک یا چندتایی از قلم بیفتند. برای دیالوگها هم بدین صورت خواهد
بود و خیلی از مسائل دیگر. به همین جهت استفاده از اینترفیسها توصیه
میشود و فردا نیز اگر باز یک کلاس دیگر را نوشتید، خیلی راحت آن را با کلاس
فعلی تعویض میکنید.
public interface IMessageUI
{
void ShowToast(Context context,String message);
}
public class MessageUI impelement IMessageUI
{
public void ShowToast(Context context,string message)
{
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}
چهار. اگر برای اولین بار است وارد اندروید میشوید،
خوب چرخههای یک شیء، چون اکتیویتی یا فراگمنت را یاد بگیرید تا در آینده با
مشکلات خاصی روبرو نشوید.
به عنوان مثال درست است که اولین رویداد فراخوانی در onCreate رخ میدهد ولی
همیشه محل مناسبی برای دریافت دیتاها در زمان اولیه نیست. به عنوان مثال
تصور کنید که لیستی در اکتیویتی A دارید و به اکتیویتی B میروید و یک آیتم
به اطلاعات اضافه میشود و موقعی که به اکتیویتی A بر میگردید، زیاد تعجب نکنید که لیست دقیقا به
همان شکل قبلی است و خبری از آیتم جدید نیست.
چون
اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی
که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی
دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است
که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور
تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
به طور خلاصه نحوه اجرای رویدادها بدین شکل است که ابتدای رویداد OnCreate اجرا میشود که هنوز هیچ UI ئی در آن پیاده سازی نشدهاست و شما در اینجا موظفید Layout خود را معرفی کنید. رویداد OnStart بعد از آن موقعی که UI آماده شده است، اجرا میگردد. سپس رویداد OnResume اجرا میشود.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
پنج.
سرویس را با تردهای UI ترکیب نکنید. بعضا دیده میشود که کاربران
AsyncTask را داخل سرویس استفاده میکنند ولی این را بدانید که سرویس یک
ترد پردازشی جداگانه است و تضمینی برای ارتباط با UI به شما نمیدهند. هر
چند گوگل جدیدا تمهیداتی را برای آن اندیشیده است که به شما اجازه اینکار را
نمیدهد. ولی اگر باز هم اندروید استادیو به شما خوردهای نگرفت، خودتان این
قانون را اجرا کنید. قرار نیست یک AsyncTask با سرویس ترکیب شود.
شش.
اگر برنامه شما قرار است در چندین حالت مختلفی که اتفاق میافتد، یک کار
خاصی را انجام دهد، برای برنامهتان یک Receiver بنویسید و در آن کدهای
تکراری را نوشته و در محلهای مختلف وقوع آن رویدادها، رسیور را صدا بزنید.
برای نمونه برنامه تلگرام یک سرویس پیام رسان پشت صحنه دارد که در دو
رویداد قرار است اجرا شوند. یکی موقعی که گوشی بوت خود را تکمیل کرده است و
در حال آغاز فرایندهای سیستم عامل است و دیگر زمانی است که برنامه اجرا
میشود. در اینجا تلگرام از یک رسیور سیستمی برای آگاهی از بوت شدن و یک
رسیور داخل برنامه جهت آگاهی از اجرای برنامه استفاده میکند و هر دو به یک
کلاس از جنس BroadcastReceiver متصلند:
<receiver android:name=".AppStartReceiver" android:enabled="true">
<intent-filter>
<action android:name="org.telegram.start" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
public class AppStartReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
ApplicationLoader.startPushService();
}
});
}
}
برای نام رسیورهای داخلی هم میتوانید مورد شماره 2 را اجرا کنید.
برنامه تلگرام حتی برای حالتهای پخش هم رسیورها استفاده کرده است که در همین رسیور وضعیت تغییر پلیر مشخص میشود:
<receiver android:name=".MusicPlayerReceiver" >
<intent-filter>
<action android:name="org.telegram.android.musicplayer.close" />
<action android:name="org.telegram.android.musicplayer.pause" />
<action android:name="org.telegram.android.musicplayer.next" />
<action android:name="org.telegram.android.musicplayer.play" />
<action android:name="org.telegram.android.musicplayer.previous" />
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
اینگونه تلگرام میتواند از همه جا سرویس را کنترل کند. مثلا موقعی که
دانلود یک موزیک تمام شده، سریعا پخش آن موزیک دانلود شده را آغاز کند.
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد
ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم
ActiveAndroid و
CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه
میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و
در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک
inner join باید به شکل زیر بنویسید:
From query= new Select()
.from(Poem.class)
.innerJoin(BankPoemsGroups.class)
.on("poems.id=bank_poems_groups.poem")
.where("BankGroup=?", String.valueOf(groupId));
return query.execute();
همانطور که میبینید بخشهایی از آن مثل جوینها و شرطها را باید خودتان
تکمیل کنید. از آنجا که ممکن است در آینده نام فیلد تغییر کند یا اینکه در
حین انبوهی از کدها، عبارت رشتهای را اشتباه وارد کنید، بهتر است به این
فرم کار کنید:
@Table(name="poems")
public class Poem extends Model {
public static String tableName="poems";
public static String codeColumn="code";
public static String titleColumn="title";
public static String bookColumn="book";
......
@Column(name="code",index = true)
public int Code;
@Column(name="title")
public String Title;
@Column(name="book")
public Book Book;
.....}
در مدل بالا، نام فیلدها و جداول به صورت استاتیک تعریف شدهاند. حالا میتوانیم از این اسامی به راحتی در لایه سرویس استفاده کنیم:
From query= new Select()
.from(Poem.class)
.innerJoin(BankPoemsGroups.class)
.on(Poem.TableName+"."+ Poem.IdColumn+"="+ BankPoemsGroups.TableName+"."+ BankPoemsGroups.PoemColumn)
.where(Poem.BankGroupColumn+"=?", String.valueOf(groupId));
return query.execute();
حالا کمی بهتر شد. هم برای تغییر آینده بهتر شد و هم اینکه احتمال خطای
تایپی کاهش یافت. ولی باز هم ایجاد کوئری هنوز سخت است و نوشتن مرتب یک
رابطه جوین و شرطی و چسباندن مداوم رشتهها کار خسته کنندهای است و احتمال
خطای سهوی و انسانی هم در آن بالاست. برای رفع این مشکل بهتر است یک کلاس
جدید برای ساخت این کوئریها داشته باشیم که یک نمونه از آن را در این
پایین میبینید:
public class QueryConcater {
public String GetInnerJoinQuery(String table1,String field1,String table2,String field2)
{
String query=table1 +"." +field1+"="+table2+"."+field2;
return query;
}
......
}
در ادامه برای مرتب سازی و شرط و ... هم مینویسیم:
return new Select()
.from(Color.class)
.innerJoin(ProductItem.class)
.on(queryConcater.GetInnerJoinQuery(ProductItem.TableName,
ProductItem.ColorColumn, Color.TableName))
.where(queryConcater.WhereConditionQuery
(ProductItem.TableName, ProductItem.ProductColumn), productId)
.execute();
در دستورات بالا از این کلاس دو متد برای کوئری جوین و یکی هم برای ساخت
شرط ایجاد شده است و مقادیر به صورت پارامتر داده شدهاند. این الگو کمک
میکند که اگر هم این تکه کد اشتباه باشد، با تغییر یکجا بقیه کدها هم تغییر
میکنند و اگر در آینده هم ORM تغییر یافت، نحوه کوئری نویسیها در این کلاس
تغییر کنند، نه اینکه در طول لایه سرویس پراکنده باشند.
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا
Acra است که میتوانید
backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت
Tracepot
امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم
آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر
به شما گزارش داده خواهد شد. این گزارشها شامل:
- وضعیت گوشی در حین باز شدن برنامه و در حین خطا چگونه بوده است.
- مشخصات گوشی
- این خطا به چه تعداد رخ داده است و برای چه تعداد کاربر
- گزارش گیری بر اساس اولین تاریخ رخداد خطا و آخرین تاریخ، نسخه سیستم عامل اندروید، ورژن برنامه شما و...
و امکانات دیگر.
نه. آکرا همانند
Elmah نمیتواند خطاهای catch شده را دریافت کند. برای حل این مشکل عبارت زیر را در catchها بنویسید:
ACRA.getErrorReporter().handleException(caughtException);
ده. بر خلاف سیستم دات نت که شما اجباری به استفاده از Try Catchها ندارید. در
جاوا اینگونه نیست و هر متدی که Throw روی آن انجام شده باشد مستلزم استفاده از catch است. به همین دلیل در شماره نه
گفتیم که چگونه باید این مشکل را حل کنیم. ولی در بسیاری از اوقات پیش
میآید که ما داریم از ماژولهای متفاوتی استفاده میکنیم که جدا از ماژول
اصلی برنامه هستند و این مورد باعث میشود که بعضی افراد یا Acra را در همه
ماژولها صدا بزنند یا اینکه بی خیال آن شوند. ولی کار راحتتر این است که
شما هم همانند برنامه نویسان جاوا متد خود را به Throw مزین کنید تا در
هنگام استفاده از آن در برنامه اصلی نیاز به catch شدن باشد. در واقع شما
نباید catchها را داخل یک کتابخانه جدا و مستقل قرار دهید و روش صحیح هم
همین است حالا چه استفاده از آکرا نیاز باشد و چه نباشد.
نمونه اشتباه: public void CopyFile(String source,String destination,CopyFileListener copyFileListener) {
try {
InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(destination);
long fileLength=new File(source).length();
// Transfer bytes from in to out
byte[] buf = new byte[64*1024];
int len;
long total=0;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
total+=len;
copyFileListener.PublishProgress(fileLength,total);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
نمونه صحیح: public void CopyFile(String source,String destination,CopyFileListener copyFileListener) throws IOException {
InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(destination);
long fileLength=new File(source).length();
// Transfer bytes from in to out
byte[] buf = new byte[64*1024];
int len;
long total=0;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
total+=len;
copyFileListener.PublishProgress(fileLength,total);
}
in.close();
out.close();
}
ادامه دارد...