برنامه نویسی 166 بازدید

«فلاتر» (Flutter) ‌یک SDK برای ساخت اپلیکیشن‌های چندپلتفرمی است که از سوی گوگل در سال 2018 معرفی شده است. فلاتر از زمان معرفی خود محبوبیت زیادی کسب کرده است. شرکت‌ها امروزه در تلاش برای کاهش هزینه‌ها هستند و از این رو به دنبال روش‌های بهتر و کارآمدتر برای ساخت اپلیکیشن‌های موبایل و به طور کلی نرم‌افزار‌های چندپلتفرمی هستند. فلاتر از همه پلتفرم‌های عمده پشتیبانی می‌کند. همچنین پشتیبانی از وب و همه سیستم‌های عامل دسکتاپ اصلی نیز در حال توسعه است. در این مقاله یک اپلیکیشن موسیقی در فلاتر توسعه می‌دهیم و در این مسیر با الگوهای طراحی اصلی توسعه اپلیکیشن‌های فلاتر آشنا خواهیم شد.

برای آشنایی با روش نصب فلاتر به این صفحه مستندات (+) مراجعه کنید. همچنین سورس کد کامل این پروژه در این ریپازیتوری (+) ارائه شده است.

ساخت اپلیکیشن موسیقی در فلاتر

مفاهیم ابتدایی

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

بهره‌گیری بهینه از قابلیت‌های یک زبان، تفاوتی به اندازه تبدیل یک اپلیکیشن شلوغ و مستعد باگ به یک شاهکار هنری ایجاد می‌کند. زبان دارت قابلیت‌های زیادی دارد که به ساخت UX ناهمگام و با تعامل‌پذیری بالا به همراه «مدیریت حالت» کمک می‌کند.

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

  • ویجت‌های فلاتر از قبیل Container ،SizedBox و Column
  • استفاده از Timer و Stopwatch برای کار با بازه‌های زمانی
  • پیاده‌سازی سرویس‌ها با Stream / StreamController

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

شرح اجمالی پروژه

معماری و UX اپلیکیشن ایجاد بیت موسیقی (Beat) تا حد امکان ساده حفظ شده است تا شبیه دستگاه‌های بیت‌ساز دهه 190 و 1980 میلادی به نظر برسد. در این دستگاه‌ها از منابعی مانند سوئیچ‌های مکانیکی، اجزای مسی و CPU-های جذاب و جدید 80 بیتی محدود بودند. در آن دوره ساخت دستگاه‌هایی که نوازندگان توانایی خرید آن را داشته باشند، نیازمند این بود که هزینه‌های طراحی و ساخت در کمترین حد ممکن باقی بمانند.

چارچوب اصلی UI به چهار ویجت «پنل جلویی» تقسیم شده است که هر یک بخشی از تعامل‌پذیری را عرضه می‌کنند و منطق دستگاه درون یک سرویس playback ساده و سرویس موتور صوتی قرار گرفته است. این معماری شبیه اجزای یک آلت موسیقی سخت‌افزاری واقعی است که داده‌ها را از طریق کابل‌های MIDI ارسال می‌کند.

نقطه ورودی اپلیکیشن

اپلیکیشن ما درون فایل main.dart مقداردهی می‌شود:

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

ویجت پایه

چهار ویجت اصلی در چارچوب (main) ‌یک کلاس مشترک مبنا را بسط می‌‌دهند تا به موتور صوتی وصل شوند. این کلاس در مسیر views/base-class.dart تعریف شده است:

کلاس‌های BaseWidget و BaseeState به ترتیب اقدام به بسط StatefulWidget و State می‌کنند و یک استریم داخلی را پیاده‌سازی می‌کنند که در زمان مقداردهی اولیه، یک شنونده را به AudioEngine متصل می‌سازد و هنگامی که سیگنالی از موتور صوتی دریافت شود، حالت را رفرش می‌کند. از این رو هر ویجت که AudioEngine را بسط دهد، ‌در مواردی که موتور صوتی سیگنالی ارسال کند که رویدادی درون موتور رخ داده است، بازسازی می‌شود و از این رو UI باید از نو ساخته شود.

پنل نمایش

بالاترین کامپوننت در ستون scaffold به نام پنل نمایش در مسیر views/display.dart قرار دارد:

کلاس DisplayPanel نشانگرهای موقعیت BPM و Step را در ابتدای صفحه رندر می‌کند و زمانی که کلاس مبنا سیگنالی دریافت کند، به صورت خودکار رفرش می‌شود. با کلیک کردن روی نشانگر BPM یک دیالوگ BPMSelector به همراه فهرستی از گزینه‌های عددی از 1 تا 256 باز می‌شود. با انتخاب یکی از این گزینه‌ها میزان BPM روی موتور صوتی تنظیم می‌شود.

نشانگرهای Step نیز تولید می‌شوند که هر یک از آن‌ها زمانی که موتور اجرا شود و گام کنونی با شاخص روی چرخه رندر تطبیق پیدا کند، روشن می‌شوند.

تقطیع‌کننده الگو

ویجت ادیتور «تقطیع‌کننده الگو» (Pattern Sequencer) در مسیر views/sequencer.dart تعریف می‌شود:

این سکوئنسر یک «ویجت بی‌حالت» (StatelessWidget) است، چون هیچ نوع تعامل‌پذیری از خود ندارد، اما به جای آن ویجت‌های Track را که UX هر ترک را ارائه می‌کنند رندر می‌کند.

یک ردیف بسط‌یافته نیز برای هر سمپل (نمونه) تولید می‌شود که دارای یک برچسب در سمت چپ است و یک Track دارد که به صورت خودکار باز شده و باقی فضای ردیف را پر می‌کند.

ترک سکوئنسر

ادیتور تِرَک سکانس در مسیر views/track.dart تعریف شده است:

ویجت Track در واقع «ویجت پایه» (BaseWidget) را بسط داده است و از این رو هر زمان که یک سیگنال از موتور صوتی دریافت کند، بازسازی می‌شود. هر ترک درون خود یک فهرست از هشت نشانگر نُت دارد که وقتی کلیک شود، یک رویداد به موتور صوتی ارسال می‌کند. به این ترتیب حالت نت به صورت داخلی خاموش/روشن می‌شود و علامتی برای یک رفرش فرستاده می‌شود. رنگ هر نشانگر بلوک نت از روی این مسئله که نت در موقعیت کنونی موجود باشد و این که نت در حال پخش باشد یا نه، تعیین می‌شود. زمانی که یک نت موجود نباشد، رنگ ستون‌های مجاور به منظور خوانایی و UX تغییر می‌یابد.

کنترل انتقال

در این بخش ویجت «کنترل انتقال» (Transport Control) را در مسیر views/transport.dart تعریف می‌کنیم:

کلاس Transport یک ردیف از دکمه‌های کنترل انتقال ایجاد می‌کند که هر کدام از آن‌ها را وقتی بزنیم یک فراخوانی onTap ایجاد می‌کند و رویداد تغییر حالت به موتور ارسال می‌شود که به نوبه خود از طریق ویجت پایه یک رفرش را روی این ویجت علامت‌دهی می‌کند. زمانی که یک دکمه با حالت کنونی موتور تطبیق پیدا کند، با ارسال مقدار null به متد onPressed مربوط به MaterialButton غیر فعال می‌شود.

پد بانک

پد بانک درام در مسیر views/pad-bank.dart تعریف می‌شود:

ویجت PadBank نیز ویجت StatelessWidget را بسط می‌دهد و از این رو هیچ مشخصه «تغییرپذیر» (mutable) ندارد. به همین جهت این ویجت نیازی به حالت ندارد. این ویجت یک کانتینر با 3/1 ارتفاع فضای موجود روی والا خود با دو ردیف ویجت‌های Pad رندر می‌کند که هر یک اندازه تعریف‌شده‌ای دارند و مقدار آن نیز از اندیس لیست جاری گرفته می‌شود.

پد درام

ویجت پد درام در مسیر views/pad.dart تعریف می‌شود:

ویجت پد یک ویجت بی‌حالت است که سه پارامتر تغییرناپذیر نهایی (final) به عنوان آرگومان می‌گیرد. سه مشخصه get تعریف شده‌اند تا DRUM_SAMPLE را همراه با نام و رنگ سمپل مربوطه دریافت کنند. زمانی که روی یک پد بزنید، یک PadEvent به موتور صوتی ارسال می‌شود تا در آنجا پردازش شود. در ادامه به بررسی طرز کار داخلی این اپلیکیشن ایجاد بیت می‌پردازیم.

سمپلر

تعاریف سمپل و بارگذاری/بازپخش در مسیر services/sampler.dart تعریف شده‌اند:

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

موتور صوتی

در این بخش به بررسی سرویس صوتی می‌پردازیم که در مسیر services/audio-service.dart تعریف شده است:

سرویس «موتور صوتی» (AudioEngine) حالت «کنترل انتقال» را مدیریت می‌کند، رویدادهای ورودی را اداره کرده و فرایند «کمّی‌سازی» (quantization) را در زمان ضبط کردن، روی نت‌های ورودی اجرا می‌کند. همچنین موتور صوتی داده‌های ترک را ذخیره کرده و به همه ویجت‌هایی که گوش می‌دهند بسته به نیاز علامت می‌دهد که UI را رفرش کنند.

کلاس‌های Event برای هر نوع از رویدادی که موتور صوتی نیاز دارد، تعریف شده‌اند و یک کلاس placeholder Signal نیز تعریف شده است تا به عنوان یک سیگنال با کاربرد عمومی برای رفرش کردن UI مورد استفاده قرار گیرد. در سناریوهای پیچیده‌تر، می‌توان کلاس Signal را برای ارسال انواع مختلفی از سیگنال‌ها به UI بسط داد.

«وضوح الگو» (Pattern Resolution) و Step همراه با حالت کنترل، BPM، داده‌های ترک اولیه، محاسبات Timer / Watch / _tick تعریف شده‌اند. همچنین یک StreamController به همراه listener تعریف شده‌اند تا ویجت‌ها بتوانند به سیگنال‌های دریافتی گوش دهند.

زمانی که سطح کنترل (مانند پد درام) on را با یک وهله از Event فراخوانی کند، متد از ژنریک‌ها برای سوئیچ کردن به نوع رویداد و اجرای عمل صحیح استفاده می‌کند. به این ترتیب همه پیام‌های ورودی از طریق یک مکان مسیریابی می‌شوند و بر همین اساس مدیریت خواهند شد. هر نوع رویداد با یک سری از عملیات درون موتور متناظر است. متدهای control ،edit ،next و synchronize در زمانی که همه به‌روزرسانی‌ها تکمیل شدند، هر کدام یک سیگنال را به UI می‌فرستند.

طراحی موتور صوتی به ما امکان می‌دهد که UI را به صورت درجا (on-the-fly) به‌روزرسانی کنیم و نیازی به راه‌اندازی مجدد موتور وجود ندارد. به این ترتیب امکان ضبط با یک تغییر حالت ساده میسر می‌شود و با این تغییر نت‌های ورودی آتی باید از متد process بگذرند تا بتوانند تمپو را در میانه الگو تنظیم کنند. همچنین از یک متد synchronize برای تنظیم تایمر جاری به BPM جدید استفاده می‌شود.

وقتی که رویداد EditEvent دریافت شود، داده‌های این رویداد برای عوض کردن آن مقدار بولی که نمایانگر روشن/خاموش بودن نت برای این ترک و موقعیت گام است مورد استفاده قرار می‌گیرند. زمانی که موتور صوتی آغاز شود، یک تایمر دوره‌ای ایجاد می‌شود که با هر مقدار _tick سکوئنسر را به پیش می‌برد و next را فرا می‌خواند که تایمر را افزایش داده یا ریست می‌کند و سپس نت را برای هر ترک روی گام جاری بررسی کرده و در نهایت کمّی‌سازی ‎_watch را ریست کرده و سیگنالی به UI ارسال می‌کند.

سخن پایانی

ساخت اپلیکیشن موسیقی در فلاتر

در این مقاله یک پروژه را معرفی کردیم که قدرت Flutter SDK را برای طراحی و توسعه سریع اپلیکیشن‌ها نشان می‌دهد. فلاتر گزینه‌ای عالی برای ساخت اپلیکیشن‌های چندپلتفرمی با تعامل‌پذیری و پایداری بالا است. این SDK با پشتیبانی از پلتفرم‌های موبایل، دسکتاپ و وب، ‌به توسعه‌دهندگان امکان می‌دهد که اپلیکیشن‌هایی با کیفیت بالا بسازند که در هر محیطی عملکردی عالی دارند و نگهداری آن‌ها آسان است.

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

اگر این مطلب برای شما مفید بوده است، آموزش‌ها و مطالب زیر نیز به شما پیشنهاد می‌شوند:

میثم لطفی (+)

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

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

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