اشتباهات رایج در طراحی واسط کاربری برنامه‌های اندرویدی

نویسنده : سید ایوب کوکبی ۱۱ خرداد ۱۳۹۸
اشتباهات رایج در طراحی واسط کاربری برنامه‌های اندرویدی

تا الان به این فکرکردید که فرق بین یک واسط کاربری خوب و بد چیست؟ با اینکه تلقی هر کسی از واژۀ خوب نسبی است ولی با کمی دقت می‌توان ویژگی‌های مشترک دیزاین‌های خوب را پیدا کرد. ویژگی‌هایی همچون:

  • خوانایی: واسط گرافیکی خوب خوانا (Readable) است. نیازی نیست به تب دیزاین اندروید استودیو بروید. همه چیز را از همان xml layout متوجه می‌شوید؛
  • آزمون‌پذیری: واسط گرافیکی خوب، آزمون‌پذیر (Testable) است. یعنی هر جزئی از آن را بدون نگرانی از وابستگی به سایر اجزاء می‌توان تست کرد؛
  • قابل‌استفادۀ مجدد: اجزا یا کامپوننت‌های یک واسط گرافیکیِ خوب بدون تغییر عملکردشان در سایر قسمت‌های برنامه قابل استفاده‌اند.

در این مقاله، اشتباهات رایج ِطراحی UI که تاثیر بدی روی بازمصرفی عناصر گرافیکی (
Reusability) دارد را بررسی می‌کنیم. همچنین خواهید دید که بازمصرفی عناصر UI چه تاثیر خوبی روی افزایش خوانایی و آزمون‌پذیری آن دارد.

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

گاهی برای صرفه‌جویی در زمان و تولید نکردن کدهای تکراری همان مدلی را که در لایه دیتابیس و شبکه استفاده کرده‌ایم داخل ویوها نیز استفاده می‌کنیم. منظور از مدل، کلاس مدل است. با یک مثال توضیح را پی می‌گیریم:

userApiClient.getUser(userId)
          .subscribe(
		{ userModel -> userView.render(userModel) },
		{ error -> Log.e("TAG", "Failed to find user", error) }
          )

کد بالا در لایۀ شبکه قرار گرفته است. معمولاً فراخوانی API و دریافت اطلاعات از سرور در این لایه جای می‌گیرد. در صورت اجرای موفقیت‌آمیز کد، خروجی زیر را خواهیم داشت؛ یعنی اطلاعات دریافت شده از بکند در آبجکتی از نوع UserModel قرار می‌گیرد:

data class UserModel(
  val id: Int, 
  val name: String, 
  val age: Int, 
  val address: String, 
  val createdAt: Date, 
  val updatedAt: Date)

از همین آبجکت برای پر کردن فیلدهای name و address در userView استفاده می‌کنیم. احتمالاً شما هم بارها از همین روش استفاده کرده‌اید. ولی این کار با اصل بازمصرفی عناصر واسط کاربری در تضاد است. این موضوع در دراز مدت باعث بروز مشکلاتی خواهد شد.

  • مهم‌ترین دلیل به کار نگرفتنِ کلاسِ بکند این است که ما نمی‌توانیم از ویوی خود در سایر قسمت‌های برنامه استفاده کنیم. ویو را به بکند متصل کرده‌ایم و اصلِ مشکل همینجاست!
  • کلاس بکند ممکن است اطلاعاتی فراتر از نیاز ویو تأمین کند. اطلاعات اضافه یعنی پیچیدگی بیشتر. از طرفی هرچه اطلاعات بیشتری دریافت شود احتمال اشتباه در به کارگیری فیلدها نیز بیشتر می‌شود. در مثال بالا امکانش بود که از یک مدل ساده مثل این استفاده می‌کردیم:
data class UserUIModel(val name: String, val address: String)

ما معمولاً خوشبین هستیم که قسمت بکند برنامه تغییری نمی‌کند ولی خودمان هم می‌دانیم که حرف کاملاً اشتباهی است. بکند تغییر می‌کند و ما باید برای این تغییرات آماده باشیم. نباید طوری باشد که با تغییر بکند مجبور باشیم قسمت‌هایی از UI را تغییر دهیم تا با تغییرات جدید هماهنگ شود.

این مشکل را می‌توان با اندکی کد اضافه حل کرد. ابتدا باید یک UI Model و متدی برای تبدیل Backend Model به UI Model بسازیم. در مثال ما، کلاس UserModel را به UserUIModel تبدیل می‌کنیم و آن را به عنوان یک پارامتر به UserView ارسال می‌کنیم:

userApiClient.getUser(userId)
          .map{ userModel -> convertToUIModel(userModel) }
          .subscribe(
              { userUIModel -> userView.render(userUIModel) },
              { error -> Log.e("TAG", "Failed to find user", error) }
          )

پیاده‌سازی جدید اندکی کدنویسی اضافه دارد و تا حدودی هم به پیچیدگی برنامه اضافه کرده ولی قطعاً برای جداسازی بکند از UI که مزیت بزرگی محسوب می‌شود هزینۀ ناچیزی است. در طراحی جدید، دیگر اطلاعات غیرضروری به ویوها ارسال نمی‌شود و همچنین استفاده از ویوها در هر جایی از برنامه مقدور خواهد بود.

بخوانید  50 منبع مفید برای حرفه‌ای شدن در برنامه‌نویسی اندروید

Layoutهای بزرگ

بعضی از layoutها را دیده‌اید که چقدر شلوغ و سنگین هستند. طراح همۀ عناصر اکتیویتی را داخل یک لی اوت جای داده است. متأسفانه این مشکل در بسیاری از برنامه وجود دارد و زمینه‌ساز مشکلات فراوانی در اندروید است.

  • در هر سیستم‌عاملی خواندن فایل‌ها همیشه کاری زمان‌بر و پرهزینه‌ای است. اطلاعات زیادی وجود دارد و به همان نسبت کامپوننت‌های فراوانی هم در واسط کاربری برنامه هست؛
  • امکان تست کردن جداگانۀ هر بخش از xml وجود ندارد. ما معمولاً تست UI را در قالب بخشی از اینتیگریشن تست با espresso انجام می‌دهیم. تست یکپارچگی یا Integration Test خوب است ولی در ترکیب منطق تجاری و عناصر گرافیکی، یافتن ریشۀ مشکل را دشوار می‌کند. با شکستن ویوی بزرگ به چندین کاستوم ویوی کوچکتر و همچنین حذف کامل کدهای منطق تجاری از UI قادر هستیم واسط گرافیکی را به صورت ایزوله تست کنیم. این تست اشکالات موجود در UI را بسیار بهتر از آزمون یکپارچگی آشکار می‌کند؛
  • قادر به استفاده مجدد از عناصر xml نیستیم. این موضوع ما را مجبور می‌کند تا کامپوننت‌ها را به صورت جداگانه در صفحات دیگر کپی کنیم. به مرور زمان با افزایش تعداد کپی‌ها اوضاع به هم‌ریخته‌ای به وجود می‌آید که زمینه را برای ناپایداری برنامه فراهم می‌کند.

ساخت کل واسط کاربری در یک فایل XML برابر است با ساخت تمام منطق تجاری برنامه در یک اکتیویتی. واسط کاربری را به همان روشی که پیش‌تر بکند و منطق تجاری را از هم جدا کردیم باید به قطعات کوچکتری بشکنیم. برای این کار از کاستوم ویو و تگ‌های <include> و <merge>استفاده می‌کنیم. با کمک این تگ‌ها می‌توانیم به هر فیچر یک UI Component اختصاص دهیم. واسط کاربری با افزایش تعداد عناصر و درهم‌تنیدگی فرگمنت‌ها و اکتیویتی‌ها باعث بروز مشکلات جدی خواهد شد. این کار عمل ریفکتور را در معرض خطر قرار می‌دهد.

با یک مثال از یک پروژۀ واقعی متن‌باز بهتر متوجه موضوع می‌شوید. جهت حفظ خوانایی، پراپرتی‌ها را حذف و کامنت‌گذاری کرده‌ایم:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >

    <!-- ToolBar -->
    <android.support.v7.widget.Toolbar ... >
      <RelativeLayout ...
        <TextView/>
        <TextView/>
      </RelativeLayout>
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >

        <!-- Header -->
        <LinearLayout ... >
          <TextView ... />
          <TextView ... />
        </LinearLayout>

        <!-- User Message -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- User Information -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- Option selector -->
        <LinearLayout ... >
          <TextView ... />
          <Spinner ... />
        </LinearLayout>
      </LinearLayout>

      <!-- progress overlay -->
      <FrameLayout ... >
        <ProgressBar ... />
      </FrameLayout>
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>

حتی با حذف پراپرتی‌ها و افزودن کامنت‌ها باز هم خوانایی آن پایین است. یکی از عوامل مهم این شلوغی تودرتو بودن عناصر است. در چنین اوضاعی اضافه کردن کامنت دردی دوا نمی‌کند. چارۀ کار شکستن UI به اجزاء کوچکتر است. در کد بالا به راحتی می‌توان ۶ کامپوننت جدا را تشخیص داد (Header, User Message, User Information و … .) این کامپوننت‌ها را می‌توانیم در قالب کاستوم ویوهای مجزا کنار هم بچینیم:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >
    <CompanyToolbar ... />
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >
        
        <InformationHeader ... />

        <UserMessageView ... />

        <UserInformationView ... />
        
        <MultipleChoiceView ... />
      </LinearLayout>

      <ProgressOverlay ... >
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>

با ساخت ویوی سفارشی برای هر یک از این کامپوننت‌ها، کد ساده‌تر و به تبع آن خواناتر می‌شود. و از همه مهم‌تر کامپوننت‌ها در سایر layoutها هم قابل استفاده خواهند بود. این کار تاثیر جانبی ریفکتورهای آینده را هم کمتر می‌کند. فکرش را بکنید همۀ اکتیویتی‌ها دارای یک انیمیشن progressOverlay می‌بودند. کافی است با تغییر این کامپوننت، انیمیشن را در همۀ آن‌ها بروزرسانی کنید. ایدۀ دسته‌بندی عناصر UI برگرفته از کتاب Atomic Design بِراد فِراست است. پیشنهاد می‌کنم حتماً به این کتاب نگاهی بیندازید؛ حاوی نکته‌های فراوانی است. صحبت از کتاب شد، حتماً کتاب‌های مشهور برنامه‌نویسی اندروید را هم مطالعه کنید.

بخوانید  ترفندهای مفید اندروید استودیو

جای دادن منطق تجاری در کاستوم ویو

در بخش قبلی از مزایای کاستوم‌ویو گفتیم. ولی این را هم بدانید که با استفادۀ نادرست از آن پیچیدگی برنامه بیشتر از قبل خواهد شد. ترکیب منطق تجاری با کاستوم ویو به خاطر سادگی‌اش بسیار وسوسه‌انگیز است. اما این کار مشکلاتی به همراه دارد:

  • قرارگیری منطق در ویوها جلوی بازمصرفی آن‌ها را می‌گیرد چرا که ما ویوها را به یوزکیس خاصی وابسته کرده‌ایم. برای اینکه بتوانیم از ویوها هر جای دیگری استفاده کنیم نباید آن‌ها را با کدهای منطق برنامه درگیر نماییم. یک ویوی خوش‌ساخت تنهای کار که انجام می‌دهد دریافت State و رندر کردن اجزاء صفحه بر اساس آن وضعیت است. ویوی خوب هیچ‌وقت عمل تصمیم گیری را انجام نمی‌دهد؛
  • افزودن منطق تجاری به ویوها، آزمون ویوها را هم سخت‌تر می‌کند. یکی ویوی خوب فقط به کامپوننت‌های UI که زیرمجموعۀ آن حساب می‌شود وابسته است. چنین ویویی را به صورت ایزوله می‌توان تست خودکار کرد. این تست‌ها ما را در یافتن تاثیر جانبی ریفکتور روی عناصر واسط کاربری یاری می‌کند.

راه‌های زیادی برای جدا کردن منطق تجاری از ویوها وجود دارد که بسته به معماری برنامه راه مورد نظر انتخاب می‌شود. مثلاً در معماریMVP کلِ منطق برنامه در لایۀ Presenter قرار دارد. یا در MVVM منطق در لایۀ ViewModel قرار گرفته است. منطق تجاری باید به عنوان بخشی از Intent باشد تا یک آبجکت immutable state را برای مصرف شدن در ویوها تولید کند. توصیه می‌کنیم برای ساخت کامپوننت‌های قابل استفادۀ مجدد این مقاله را بخوانید تا به خوبی با مفهوم unidirectional data flows آشنا شوید.

افراط در بهینه‌سازی

تا الان حرفی از پرفرمنس نزدیم. احتمالاً این سوال هم برایتان پیش آمده که چرا در ابتدای مطلب، پرفرمنس را به عنوان یکی از اصول طراحی UI معرفی نکردیم. حرف درستی است ولی به اعتقاد ما پرفرنمس به اندازۀ خوانایی، تست‌پذیری و قابلیت استفادۀ مجدد مهم نیست. دقیقاً به همین دلیل است که به جای زبان بسیار کارا و سریع اسمبلی ترجیح می‌دهیم از زبان‌های سطح بالا که اغلب کندتر هستند استفاده کنیم. دلیلش خوانایی بهتر این زبان‌هاست. پس همه چیز پرفرمنس نیست!

در اندروید، لایه‌های تودرتو یا اصطلاحاً nested layouts به شدت پرفرمنس برنامه را تحت تاثیر قرار می‌دهند. به همین دلیل در مقالات متعدد توصیه کرده‌ایم که از روش‌های بهتری مثل ConstraintLayout استفاده کنید. این حتی از RelativeLayout هم بهتر است. ولی باز هم این حرف مجوزی برای استفادۀ مطلق از این کامپوننت نیست. یعنی نباید همۀ عناصر UI را در یک ContraintLayout قرار دهیم. چند دلیل دارد:

  • ساختن کلِ واسط کاربری داخل یک ConstraintLayout به مشکل شلوغی UI منجر می‌شود که بالاتر تاثیر بدِ آن را در خوانایی، تست‌پذیری و قابلیت استفادۀ مجدد خواندید؛
  • نباید کلِ UI را به عنوان یک مجموعۀ واحد در نظر بگیریم. ما هیچ وقت کلِ منطق و کدهای برنامه را درون یک فرگمنت یا اکتیویتی قرار نمی‌دهیم. با همین منطق کلِ UI هم نباید در یک فایل XML واحد قرار بگیرد.

کد خوب را زمانی باید قربانی پرفرمنس کرد که واقعاً چاره‌ای جز آن نباشد. برای جلوگیری از افراط در بهینه‌سازی باید به صورت مرتب تست پرفرمنس انجام دهیم. این تست‌ها باید مشکلاتی را به ما گزارش کنند که با جایگزین کردن یک ویوی بزرگ حل شود و ضمناً هیچ چارۀ دیگری هم وجود نداشته باشد. شاید برایتان جالب باشد که بسیاری از مشکلات پرفرمنس به خاطر مسائل بایندینگ و رفرش کردن بیش از حد عناصر UI به وجود می‌آید.

بخوانید  آموزش زبان کاتلین – درس 25 (Data Class)

غفلت از بازبینی کدهای UI

بازبینی کدها همیشه سخت و زمان‌بر بوده و هست و از همه بدتر بررسی فایل‌های xml که به خاطر ماهیتشان خوانایی بدی دارند به‌خصوص اگر با xml بزرگی سروکار داشته باشیم. به همین دلیل تنبلی می‌کنیم و بازبینی کدهای UI را به تعویق می‌اندازیم یا اصلاً سراغش نمی‌رویم. اما کار اشتباهی است چون:

  • فراموش کردن بازبینی کد یعنی فراموش کردن کاربران. کاربران دنبال یک برنامۀ ساده، تمیز و بدون نقص هستند و این یعنی اهمیت بازبینی مرتب کدها و بهبود مداوم آن؛
  • واسط کاربری نصف برنامه است؛ بنابراین صرف زمان برای بهینه‌سازی فایل‌های xml به اندازۀ بهبود منطق تجاری اهمیت دارد.

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

  • همیشه از بررسی UI شروع کنید. چون هم سخت‌تر است و هم تاثیر بیشتری روی ظاهر و پرفرمنس برنامه دارد؛
  • اگر واسط کاربری را فرد دیگری ساخته و فایل‌های xml را بسیار بزرگ و شلوغ می‌بینید از طراح بخواهید تا فایل‌ها را به عناصر کوچکتری بشکند یا اگر نام‌گذاری‌ها و ساختار xml خوانا نیست از طراح بخواهید روی این موضوع کار کند؛
  • همیشه سوالات زیادی برای ما پیش می‌آید که مثلاً آیا دکمه باید حالت فشرده شدن را داشته باشد یا نه. فلان افکت را باید کجا به کار ببرم و سوالاتی از این قبیل. پاسخ این سوالات با جستجوی ساده‌ای در اینترنت یافت می‌شود و یا با خواندن مستندات طراحی متریال می‌توانید از این باید و نبایدها و قواعد آن مطلع شوید.

جمع‌بندی

ساخت واسط کاربری چند بار مصرف کار سختی نیست به شرطی که نظم و دیسیپلین داشته باشید. ما باید تفکر سابق مبنی بر اینکه صفحات برنامه را به صورت جدا در ذهن تصور می‌کنیم کنار بگذاریم و هر صفحه را مجموعه‌ای از عناصر و کامپوننت‌ها کنار هم تجسم کنیم.

استفادۀ مجدد از عناصر UI اضافه کردن فیچرهای جدید را ساده‌تر می‌کند. بین کیفیت و سرعت برنامه تفاوت زیادی هست. گاهی باید سرعت را فدای افزایش مهم‌تر یعنی کیفیت کرد؛ مثال اسمبلی و زبان‌های سطح بالا را به خاطر آورید.

خلاصۀ کلام:

  • از بیزنس مدل در ویوها نباید استفاده کرد. همیشه باید کدهای بکند از فرانت‌اند جدا شوند؛
  • کلِ واسط کاربری را داخل یک فایل xml قرار ندهید. از طراحی اتومیک پیروی کند. صفحه را به چندین کامپوننت کوچکتر تقسیم کنید؛
  • کدهای منطق تجاری را در واسط کاربری قرار ندهید. لاگ کردن، تست A/B یا تصمیم‌گیری‌ها مربوط به ویو نیستند. ویو فقط یک state دریافت کرده و عناصر صفحه را بر اساس آن رندر می‌کند. استفاده از MVI و unidirectional data flows در این زمینه کمک زیادی می‌کند؛
  • کیفیت برنامه زمانی باید فدای سرعت و پرفرمنس آن شود که واقعاً چاره‌ای نباشد. ما فقط زمانی باید از ویوهای یکپارچه (بدون اجزاء کوچکتر) استفاده کنیم که گزینۀ دیگری در اختیار ما نباشد. واسط کاربری را به طور استاندارد و باکیفیت طراحی کنید مگر زمانی که مشکلی در تست‌های پرفرمنس گزارش شود. آن موقع می‌توانید قسمت‌هایی را برای بهینه‌سازیِ پرفرمنس تغییر دهید؛
  • ساخت فیچرها را با سخت UI مربوط به آن شروع کنید. از طراحان بخواهید تا زمان بیشتری صرف افزایش خوانایی، تست‌پذیری و قابلیت استفاده مجدد UI کنند.

سید ایوب کوکبی

نویسنده و مترجم...

0 دیدگاه

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