چطور یک پروژه فلاتر سازگار با تمام دستگاه ها بسازیم؟(ریسپانسیو کردن اپ فلاتر)

3 سال پیش - خواندن 9 دقیقه

سلام

تو این پست میخوایم در مورد ریسپانسیو و انطباقی کردن پروژه های فلاتر صحبت کنیم؛ که این کار چطور انجام میشه.

۲ تا پکیج خفنم آموزش میدیم واقعا خفنن.

خب شروع میکنیم.

یه اپ اینجوری:)




طراحی انطباقی(Adaptive) چیه؟

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

Illustration

همونجور که تو عکس می‌بینید. طراحی انطباقی یعنی یه جور طراحی برای تمام افراد با هر نوع دستگاه.

عامل های مختلفی که توی عکس می‌بینید: 

  1. پلتفرم(اندروید، آی‌او‌اس، مک‌او‌اس، ویندوز، وب و ....)
  2. صفحه‌نمایش(سایز، تراکم‌پیکسل، padding، دارک/لایت مود)
  3. ورودی(صفحه‌لمسی، کیبورد و موس، صفحه‌خوان)
  4. دسترسی(مقیاس‌گذاری متن، کنتراست)
  5. کارایی(شبکه، پویا نمایی ها)

پلتفرم

دونستن اینکه سیستم‌عامل کاربر ما چیه باعث میشه ما از قابلیت های خاص اون سیستم‌عامل استفاده کنیم و این کار میتونه رو تجربه کاربر ما تاثیر زیادی داشته باشه. 

برای فهمیدن سیستم‌عامل از کلاس Platform استفاده میکنیم. چون تو این کلاس وب نیست مجبوریم که از ثابت kIsWeb استفاده کنیم.

import 'dart:io';
import 'package:flutter/foundation.dart';

@override
Widget build(BuildContext context) {
    if(kIsWeb || Platform.isMacOS || Platform.isLinux ||Platform.isWindows){
        return DesktopLayout();
    }
    
    return MobileLayout();
}


Illustrationفکر کنم بدونید برای آی‌او‌اس و اندروید ویجت های متفاوتی وجود داره. پس اگر کاربرانتون از آی‌او‌اس هم استفاده میکنن سعی کنید براشون از ویجت های Cupertino استفاده کنید تا احساس بهتری حین استفاده از اپ شما داشته باشند.


صفحه‌نمایش

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

MediaQuery

فکر کنم قبلا هنگام توسعه با Flutter از MediaQuery استفاده کردید.حتما به خاطر اینکه به اکثر خصوصیات توصیف کننده دستگاه کاربر دسترسی دارین.

تنها یه کار نیازه، اونم تعریف یه متغیر:

@override
Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    // Rebuilt every time a property of media query changes
    // ...
}


سایز صفحه‌نمایش

سیستم‌عامل هرچی که باشه اون صفحه نمایشه که به کاربر داده هارو نشون میده. پس ما باید به بهترین شکل ممکن از سایزش استفاده کنیم.

Illustration

برای فهمیدن سایز بهترین راه استفاده از size از کلاس MediaQuery هست:

@override
Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    if (mediaQuery.size.width > 1024) {
      return LargeLayout();
    }

    if (mediaQuery.size.width > 332) {
      return MediumLayout();
    }

    return SmallLayout();
}

تراکم‌پیکسل

Illustration

برای استفاده از  devicePixelRatio در MediaQuery کمک بگیرید:

@override
Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);

    if (mediaQuery.devicePixelRatio >= 2) {
      return HighDefinitionVideo();
    }

    return LowDefinitionVideo();
}

به عنوان مثال مورد استفاده: می توانید با نشون دادن ویدئویی از شبکه در کیفیت کمتری برای دستگاه هایی با تراکم کمتر، پهنای باند شبکه را ذخیره کنید چون تفاوت اون رو با یک فیلم HD نمی بینند.


Padding

Illustrationدارید می‌بینید که اگر از Padding استفاده نکنیم، اپ‌مون کاملا خراب میشه و کاربر ازش زده خواهد شد.



برای استفاده از  Padding در MediaQuery کمک بگیرید:

@override
Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    return Padding(
      padding: EdgeInsets.only(
        top: mediaQuery.padding.top,
      ),
      child: Child(),
    );
}

فلاتر همچنین به ما یک ابزارک میده  تا در انجام این کار به ما کمک کنه: SafeArea.

@override
Widget build(BuildContext context) {
    return SafeArea(
      left: false,
      right: false,
      bottom: false,
      child: Child(),
    );
}


Illustrationاز شایع ترین مشکلات استفاده نکردن از Padding همینه‌!

خود Scaffold این رو برای ما مدیریت میکنه ولی بعضی مواقع نیازه به طور مستقیم کنترلش کنیم:

@override
Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    return Padding(
      padding: EdgeInsets.only(
        bottom: mediaQuery.viewInsets.bottom,
      ),
      child: Child(),
    );
}


تنظیمات دسترسی

قشر خاصی از کاربرا اندازه فونتشون رو عوض میکنن به همین خاطر ماهم باید اندازه فونت اپ مون رو همینجور که کاربر میخواد تنظیم کنیم.

Illustration

بازم باید از MediaQuery عزیز کمک بگیریم: 

@override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    return Text(
      'Hello!',
      textScaleFactor: mediaQuery.textScaleFactor.clamp(1.0, 1.5),
      style: TextStyle(
        fontFamily: mediaQuery.boldText ? 'LightFont' : 'BoldFont',
      ),
    );
  }

نکته: شاید ندونید که  ? : یعنی چی؟ این دو علامت یه جور خلاصه if else هست یعنی: 

اگر شرط قبل از ? برقرار بود(true بود) این کار رو انجام بده درغیر این صورت هم بعد از : هست.

مثال: 

final name = "Hadi";

print(name == "Hadi" ? "Hello Hadi" : "Hello User")

یعنی اگر متغیر name برابر با Hadi بود پرینت کن Hello Hadi درغیر این صورت پرینت کن Hello User

دارک مود

از اونجایی که اندروید و آی‌او‌اس اجازه فعال کردن دارک مود رو میدن برنامه های زیادی قابلیت دارک مود رو دارن، پس ماهم باید استفاده کنیم:

Illustration

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

 @override
  Widget build(BuildContext context) {
    final mediaQuery = MediaQuery.of(context);
    return Image.asset(
      mediaQuery.platformBrightness == Brightness.dark
          ? 'assets/background_dark.png'
          : 'assets/background_light.png',
    );
  }

اگر از MaterialApp استفاده می کنید می توانید یک lightTheme و darkTheme برای سازگاری بیشتر برای خودتون تعریف کنید.


بین المللی شدن

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

Illustration

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

@override
Widget build(BuildContext context) {
  final locale = Localizations.localeOf(context);
  return Image.asset('flag_${locale.countryCode}.png');
}

برای اینکه نوع ساعت رو بفهمیم و تغییر بدیم هم:

@override
Widget build(BuildContext context) {
  final locale = Localizations.localeOf(context);
  final formattedDate = DateFormat.yMMMMEEEEd(locale.toString());
  return Text(formattedDate);
}

و در آخر برای اینکه بفهمیم rtl هست یا ltr از Directionality استفاده میکنیم:

@override
Widget build(BuildContext context) {
  final direction = Directionality.of(context);
  return Image.asset('horizontal_illustration_${direction.value}.png');
}

بخش صفحه نمایش تموم شد:)

ورودی کاربر

برنامه یک رابط دو طرفه است. صرفا انطباق با اطلاعات ارائه شده به کاربر کافی نیست، بلکه باید نحوه تعامل کاربر با برنامه ما هم خوب باشه تا کاربر بتونه با اپ ما ارتباط بگیره.


خواننده صدا

با پشتیبانی از خواننده های صدا (که از طریق تنظیمات سیستم فعال می شوند)، به کاربران معلول اجازه می دهید تا با صدای خودشون با برنامه ما تعامل داشته باشند.

Illustration

برای پشتیبانی از تعاملات صوتی فلاتر ویجت Semantics رو ارائه داده:

 @override
Widget build(BuildContext context) {
  return Semantics(
    button: true,
    label: 'My awesome button',
    child: Child(),
  );
}


کیبورد و موس

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

 Illustration

فلاتر هم که برا همه چی ویجت داره برای اینم داره ویجت Shortcuts برای کیبورد و MouseRegion برای موس. 

Shortcuts:

return Shortcuts(
  shortcuts: {
    LogicalKeySet(
      LogicalKeyboardKey.control,
      LogicalKeyboardKey.keyC,
    ): CopyIntent(),
  },
  child: Actions(
    actions: {
      CopyIntent: CallbackAction<Intent>(
        onInvoke: (intent) => copy(),
      ),
    },
    child: Child(),
  ),
);
return Shortcuts(
  shortcuts: {
    LogicalKeySet(
      LogicalKeyboardKey.control,
      LogicalKeyboardKey.keyC,
    ): CopyIntent(),
  },
  child: Actions(
    actions: {
      CopyIntent: CallbackAction<Intent>(
        onInvoke: (intent) => copy(),
      ),
    },
    child: Child(),
  ),
);

MouseRegion:

 @override
Widget build(BuildContext context) {
  return MouseRegion(
    onEnter: (event) => print('Enter'),
    onExit: (event) => print('Exit'),
    onHover: (event) => print('Hover'),
    child: Child(),
  );
}

کارایی

درآخر، چندین چیز به خود کاربر بستگی ندارد، بلکه به دستگاه و زمینه آن بستگی دارد.


Connectivity

Illustration

برای اینکه بفهمیم کاربر ما به اینترنت دسترسی داره یا نه از پکیج بالا استفاده میکنیم.

اینگونه:

import 'package:connectivity/connectivity.dart';

var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.mobile) {
  //کابر به داده موبایل وصله
} else if (connectivityResult == ConnectivityResult.wifi) {
  // کاربر به وای‌فای وصله
} else if (connectivityResult == ConnectivityResult.none){
 //کاربر به اینترنت وصل نیست
}


انیمیشن‌ها

Illustration

برای اینکه بفهمید کاربر انیمیشن هارو فعال گذاشته یا نه از پراپرتی disableAnimation در MediaQuery استفاده کنید:

@override
Widget build(BuildContext context) {
  final mediaQuery = MediaQuery.of(context);
  return mediaQuery.disableAnimations ? StaticChild() : AnimatedChild();
}

پکیج هایی برای بهبود سازگاری:

1. FlutterScreenUtil:

پکیج FlutterScreenUtil واقعا برای طراحی واکنش گرا عالیه.

اول اونو به pubspec.yaml اضافه کنید:

dependencies:
  flutter:
    sdk: flutter
  # add flutter_screenutil
  flutter_screenutil: ^4.0.4+1

بعد اونو import کنید:

import 'package:flutterscreenutil/flutterscreenutil.dart';

در مرحله بعد ،باید یه خط قبل return Scaffold این کد را وارد کنید:

ScreenUtil.init(BoxConstraints(maxHeight: MediaQuery.of(context).size.height,
maxWidth: MediaQuery.of(context).size.width));

 حالا کار باهاش:

Container(
width: 50.w,
height:200.h
)

انواع مقادیر موجود در این پلاگین:

۵۰.w این واحد براساس عرض صفحه مقداری تولید میکنه
۲۰۰.h این واحد براساس طول صفحه مقداری تولید میکنه
۰.۴.sw که مساوی با ۴۰ درصد است
۱۶.sp که برای سایز فونت استفاده می شه، که باعث میشه تو صفحه‌نمایش های بزرگ، کمی سایزش بزرگ شه و تو صفحه‌نمایش های کوچیک، سایزش کمی کوچیک میشه که در کل سایز متن رو ریسپانسیو می کنه.

مقادیر های دیگه هم داره که برین بخونین:)


۲. Sizer:

یه جورایی این پکیج مکمل پکیج بالاست و من فقط برای یه چیزی ازش استفاده میکنم؛ DeviceType

شما میتونید استفاده نکنید ولی خوبه.

import 'package:sizer/sizer.dart';

    
return SizerUtil.deviceType == DeviceScreenType.Tablet
        ? Container(
            width: 100.w,
            height: 20.h, // استفاده از پکیج بالا
          )                     //for Tablet
        : Container(
            width: 100.w,
            height: 12.h,
          );                    //for Mobile


۳. DevicePreview:

این پکیج عالیه عالی!

باهاش میشه اپلیکیشن تون رو تو سایز های مختلف تست کنید.

پیاده سازیشم خیلی سخت نیست:

import 'package:device_preview/device_preview.dart';

void main() => runApp(
  DevicePreview(
    enabled: !kReleaseMode,
    builder: (context) => MyApp(),
  ),
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: DevicePreview.appBuilder, 
      home: HomePage(),
    );
  }
}

برای درک بیشتر این فیلم رو ببینید:




این هم اپ بنده است با ترکیب هرسه پکیج بالا:


[webp-to-png output image]امیدوارم بعد از خوندن این پست بتونید از اینجور اپ ها بنویسید:)))

خب این پست هم تموم شد. اگر خوشتون اومد لایک کنید و کامنت بزارید.

منبع 


                         به پایان آمد این دفتر      حکایت همچنان باقیست...

2
دونیت
دونـیت
کــارمـا :
3664
هادی هستم؛ ۱۴ ساله، برنامه‌نویس. عاشق فلاتر و گو:)
بفرست

مشاهده نظرات بیشتر...
hadi7546