Anophel-آنوفل Flutter کد نویسی تمیز و ساده با معماری MVVM

Flutter کد نویسی تمیز و ساده با معماری MVVM

تاریخ انتشار:
Educational
زمان مطالعه: 11 دقیقه

Flutter، با چارچوب قوی خود برای ساخت برنامه های کاربردی کامپایل شده بومی، محبوبیت زیادی در دنیای توسعه برنامه های تلفن همراه به دست آورده است. در این مقاله در مورد مدیریت صحیح و تمیز state در فلاتر بدون نیاز به کتابخانه های شخص ثالث است. ما از الگوهای طراحی مهندسی نرم افزار خالص استفاده خواهیم کرد که برای چندین دهه وجود داشته است: الگوی Observer، کارخانه، تزریق وابستگی (برای آزمایش پذیری). این الگوها در قلب اکثر کتابخانه‌های مدیریت state مدرن مانند BloC، MobX، Get و … وجود دارد.

 

با استفاده صحیح از این تکنیک‌ها، در نهایت به یک الگوی ساده می‌رسیم که می‌توانیم دوباره از آن در سراسر برنامه‌مان استفاده کنیم.ما مجبور نیستیم برنامه‌مان را با کدهای جادویی پر کنیم (مثلاً ابزارک‌هایمان را با کدهای عجیب بپیچانیم.).

پروژه شما نه تنها از جادوی کمتری بهره می برد. بلکه از وابستگی های کمتر (بحرانی!) نیز بهره می برد. به عنوان مثال، BloC در حال حاضر در نسخه 8.12 است. با هر تغییر بزرگ نسخه، تغییرات API ایجاد می‌شد که به این معنی است که کد شما فقط به کد قدیمی تبدیل می‌شود و بدهی فنی را جمع‌آوری می‌کنید. ما می توانیم با در دست گرفتن مدیریت state به طور کامل از شر آن خلاص شویم. و امیدواریم با این مقاله خواهید دید که انجام این کار چندان پیچیده نیست.

 

این مقاله برای چه کسانی است؟

برای هر کسی که دوست دارد بداند مدیریت state به طور کلی چگونه انجام می شود و چگونه می توانید بدون استفاده از هیچ کتابخانه ای به آن دست پیدا کنید، به ویژه در فلاتر.

قبل از شروع، اجازه دهید ابتدا توضیح دهیم که چرا مدیریت state مهم است و MVVM چیست.

 

چرا مدیریت state مهم است؟

مدیریت state بسیار مهم است، به ویژه برای برنامه های کاربردی تلفن همراه. عناصر رابط کاربری یک چارچوب تلفن همراه (مانند Flutter یا Android Native) چرخه حیاتی دارند که به محدودیت‌های فیزیکی دستگاه و رویدادهای خارجی وابسته است. به این معنی که آن عناصر رابط کاربری (ویجت‌های موجود در فلاتر) می‌توانند به طور غیرمنتظره‌ای از بین بروند و براساس زمانی که برای مثال تلفن چرخانده می‌شود یا کاربر یک تماس دریافتی دریافت می‌کند و توسط دستگاهش از برنامه خارج می‌شود، دوباره ایجاد می‌شوند. هر زمان که این اتفاق می افتد، مهم است که وضعیت شما به درستی بازیابی شود، بنابراین وقتی کاربر دوباره به برنامه می پرد، مجبور نیست دوباره فرم ها را پر کند (یک برنامه پیام رسان را تصور کنید که کاربر فقط چند جمله را تایپ کرده و سپس ناگهان صفحه می چرخد و ورودی از بین می رود). به همین دلیل است که الگوهای مدیریت state مختلفی توسعه یافتند و تعداد کمی از آنها تبدیل به استانداردهای اصلی شدند. محبوب ترین آنها MVC و MVVM هستند.

 

MVVM چیست؟

MVVM - مخفف Model-View-ViewModel - یک الگوی طراحی یا دیزاین پترن است که با معرفی ViewModels و Models state را از UI (View) جدا می کند. الگوی زیربنایی، الگوی observer/observable است. ViewModel به عنوان Observable و UI به عنوان Observer عمل می کند. در سطح بالا به این صورت عمل می کند: هر زمان که کاربر با برنامه تعامل داشته باشد، رویداد در UI ثبت می شود (به عنوان مثال ویجت)، سپس رابط کاربری متدی را در ViewModel فراخوانی می کند، که سپس تماس را به مدل منتشر می کند (مثلاً مخزن یا لایه دامنه ما) و سپس یک به روز رسانی را برای همه ناظران ثبت شده منتشر می کند.

 

پیاده سازی در فلاتر

اکنون می خواهیم معماری MVVM را در Dart پیاده سازی می کنیم و آن را در Flutter اعمال می کنیم. برای این کار، ما یک برنامه ساده برای انجام کار طراحی خواهیم کرد. ما یک کلاس StatefulWidget ساده خواهیم داشت که به عنوان UI عمل می کند و طبق متد Flutter، یک کلاس State دارد. آن کلاس State Observer و مرجع ما را پیاده سازی می کند و در یک ViewModel مشترک می شود که رابط Observable را پیاده سازی می کند. ViewModel حاوی یک مخزن است، داده ها را واکشی می کند و سپس به روز رسانی داده ها را آغاز می کند. با توجه به ماهیت الگوی Observer، همه Observer ها مشترک آن به روز رسانی (در مورد ما، رابط کاربری) را دریافت خواهند کرد.

 

مرحله 1: یک پروژه جدید جدید ایجاد کنید

فکر می‌کنم می‌دانید چگونه یک پروژه فلاتر جدید در Android Studio ایجاد کنید، نام آن را هر چه می‌خواهید بگذارید.

 

مرحله 2: Observer را ایجاد کنید

در پوشه lib خود یک model جدید به نام mvvm ایجاد کنید. در داخل mvvm یک فایل به نام Observer.dart ایجاد کنید، کد زیر را در فایل observer بنویسید.

 

abstract class EventObserver {
  void notify(ViewEvent event);

}

abstract class ViewEvent {
  String qualifier;

  ViewEvent(this.qualifier);

  @override
  String toString() {
    return 'ViewEvent{qualifier: $qualifier}';
  }
}

 

بیایید آن را تجزیه کنیم: هر Observer ی متد notify(ViewEvent event) را پیاده سازی می کند. جایی است که جادو اتفاق می‌افتد: این روش توسط ViewModel فراخوانی می‌شود و باید منطق به‌روزرسانی وضعیت رابط کاربری را داشته باشد.

 

مرحله 3: ViewModel را ایجاد کنید

اکنون در همان model یک فایل viewmodel.dart ایجاد کنید و کد زیر را وارد کنید:

 

import 'observer.dart';

abstract class EventViewModel {
  final List<EventObserver> _observerList = List.empty(growable: true);

  void subscribe(EventObserver o) {
    if (_observerList.contains(o)) return;

    _observerList.add(o);
  }

  bool unsubscribe(EventObserver o) {
    if (_observerList.contains(o)) {
      _observerList.remove(o);
      return true;
    } else {
      return false;
    }
  }

  void notify(ViewEvent event) {
    for (var element in _observerList) {
      element.notify(event);
    }
  }
}

 

این به عنوان کلاس پایه برای ViewModels ما عمل خواهد کرد. با گسترش EventViewModel، هر کلاسی که EventObserver را پیاده‌سازی می‌کند، می‌تواند ثبت نام کرده و به‌روزرسانی‌ها را گوش کند. علاوه بر این، کلاسی که EventViewModel را گسترش می‌دهد، دارای یک مجموعه داخلی از Observer خواهد بود، که هر زمان که یک Observer جدید Subscribe را در مدل view فراخواند، پر می‌شود. همچنین حاوی یک روش لغو اشتراک برای حذف صحیح Observer است.

 

هنگامی که notify فراخوانی می شود، ViewModel از طریق مجموعه Observer خود تکرار می شود و رویداد را منتشر می کند. این روش همان چیزی است که به روز رسانی state را در رابط کاربری فعال می کند.

 

تا اینجا حله همه چیز. ما اکنون همه عناصر کلیدی را برای انجام مدیریت صحیح state در اختیار داریم.

اگر یک پروژه جدید ایجاد کرده اید، ساختار model شما باید شبیه موردی که در اینجا پیاده سازی کردیم باشد.

 

مرحله 4: طراحی معماری برنامه ما

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

وظیفه (مدل)
TaskRepository
TaskViewModel
TaskUI (ویجت وضعیتی)


توصیه می‌کنم به الگوهایی مانند Clean Architecture نگاهی بیاندازید تا به درستی کد خود را در لایه‌های خاص ساختار دهید، اما برای سادگی، همه آن‌ها را در این نسخه نمایشی با هم خرد می‌کنیم.

 

یک model جدید به نام taskright در پوشه lib ایجاد کنید و سپس 4 فایل زیر را ایجاد کنید: model.dart repository.dart ui.dart و viewmodel.dart

 

مرحله 5: مدل و لایه داده

در این مرحله یک مدل کار ساده و یک لایه داده برای برنامه خود ایجاد می کنیم.

کد زیر را در model.dart قرار دهید:

 

class Task {
  int id;
  String title;
  String description;
  bool done;

  Task(this.id, this.title, this.description, this.done);
}

 

این باید خود توضیحی باشد. اکنون در repository.dart قرار دهید:

 

import 'model.dart';

class TaskRepository {
  final List<Task> _taskList = [
    Task(
        0,
        "Study MVVM",
        "In order to avoid ugly state management librares and collect continuously technical debt, I should study proper state management patterns",
        false),
  ];

  void addTask(Task task) {
    task.id = _taskList.length;
    _taskList.add(task);
  }

  void removeTask(Task task) {
    _taskList.remove(task);
  }

  void updateTask(Task task) {
    _taskList[_taskList.indexWhere((element) => element.id == task.id)] = task;
  }

  Future<List<Task>> loadTasks() async {
    // Simulate a http request
    await Future.delayed(const Duration(seconds: 2));
    return Future.value(_taskList);
  }
}

 

ما اکنون یک مخزن داریم که از تمام عملیات CRUD برای Tasks در حافظه پشتیبانی می کند (در عوض می توانید ادامه دهید و یک حافظه محلی اضافه کنید یا با پشتیبان خود تماس بگیرید). ما یک تاخیر کوتاه به ()loadTasks اضافه کردیم تا کمی واقعی‌تر عمل کند، معمولاً در اینجا منتظر پاسخ شبکه هستید.

 

مرحله 6: مدل TaskView و رویدادها

در این مرحله، ما TaskViewModel و رویدادهایی را که می تواند راه اندازی کند، تعریف می کنیم. کد زیر را در viewmodel.dart وارد کنید:

 

import 'package:mvvmtodoapp/mvvm/viewmodel.dart';
import 'package:mvvmtodoapp/task/repository.dart';

import '../mvvm/observer.dart';
import 'model.dart';

class TaskViewModel extends EventViewModel {
  TaskRepository _repository;

  TaskViewModel(this._repository);

  void loadTasks() {
    notify(LoadingEvent(isLoading: true));
    _repository.loadTasks().then((value) {
      notify(TasksLoadedEvent(tasks: value));
      notify(LoadingEvent(isLoading: false));
    });
  }

  void createTask(String title, String description) {
     notify(LoadingEvent(isLoading: true));
     // ... code to create the task
     notify(TaskCreatedEvent());
     notify(LoadingEvent(isLoading: false));
  }
}

class LoadingEvent extends ViewEvent {
  bool isLoading;

  LoadingEvent({required this.isLoading}) : super("LoadingEvent");
}

class TasksLoadedEvent extends ViewEvent {
  final List<Task> tasks;

  TasksLoadedEvent({required this.tasks}) : super("TasksLoadedEvent");
}

// should be emitted when 
class TaskCreatedEvent extends ViewEvent {
  final Task task;

  TaskCreatedEvent(this.task) : super("TaskCreatedEvent");
}

 

ViewModel ما یک متد ()loadTasks را نشان می دهد که به هر کسی که ViewModel را به Observer هایش فراخوانی می کند می گوید که رویدادهای زیر را به ترتیب داده شده مصرف کند:

LoadingEvent(isLoading: true); // رابط کاربری باید یک انیمیشن در حال بارگذاری را نشان دهد
TasksLoadedEvent (وظایف: مقدار); // رابط کاربری باید وضعیت داده خود را به روز کند
LoadingEvent(isLoading: false); // رابط کاربری باید انیمیشن بارگیری را پنهان کند


هر زمان که می خواهید وضعیت جدیدی را به رابط کاربری خود معرفی کنید، متد را در ViewModel خود و رویداد منطبق با آن تعریف می کنید (به عنوان مثال TaskCreatedEvent)

 

مرحله 7: ایجاد رابط کاربری

ما یک ویجت state ساده می‌سازیم که حاوی یک چرخش پیشرفت است و زمانی که در state بارگیری است آن را نشان می‌دهیم. علاوه بر این، یک ListView نیز خواهد داشت که داده ها را رندر می کند. در Flutter یک ویجت state دار باید شی State خود را داشته باشد، ما از آن شی State برای پیاده سازی EventObserver خود استفاده خواهیم کرد (مرحله 2 را ببینید).

 

کد زیر در ui.dart وارد کنید:

 

import 'package:flutter/material.dart';
import 'package:mvvmtodoapp/mvvm/observer.dart';
import 'package:mvvmtodoapp/task/repository.dart';
import 'package:mvvmtodoapp/task/viewmodel.dart';

import 'model.dart';

class TaskWidget extends StatefulWidget {
  const TaskWidget({super.key});

  @override
  State<StatefulWidget> createState() {
    return _TaskWidgetState();
  }
}

class _TaskWidgetState extends State<TaskWidget> implements EventObserver {
// Consider making TaskRepository() a singleton by using a factory
  final TaskViewModel _viewModel = TaskViewModel(TaskRepository());
  bool _isLoading = false;
  List<Task> _tasks = [];

  @override
  void initState() {
    super.initState();
    _viewModel.subscribe(this);
  }

  @override
  void dispose() {
    super.dispose();
    _viewModel.unsubscribe(this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("TaskApp 2000"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _viewModel.loadTasks();
        },
        child: const Icon(Icons.refresh),
      ),
      body: _isLoading
          ? const Center(
              child: CircularProgressIndicator(),
            )
          : ListView.builder(
              itemCount: _tasks.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(_tasks[index].title),
                  subtitle: Text(_tasks[index].description),
                );
              },
            )
    );
  }

  @override
  void notify(ViewEvent event) {
    if (event is LoadingEvent) {
      setState(() {
        _isLoading = event.isLoading;
      });
    } else if (event is TasksLoadedEvent) {
      setState(() {
        _tasks = event.tasks;
      });
    }
  }
}

 

متد های initState و dispose را بررسی کنید. مهم است که آن متد ها را override کنید و از viewmodel خود در آنجا subscribe و unsubscribe کنید. در initState همچنین می توانید ()viewModel.loadTasks را فراخوانی کنید تا داده های شما در ابتدا واکشی شوند.

 

اکنون کد UI ساختگی ایجاد شده در lib/main.dart را با کد زیر جایگزین کنید:

 

import 'package:flutter/material.dart';
import 'package:mvvmtodoapp/task/ui.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home:  TaskWidget(),
    );
  }
}

 

و تمام. اکنون باید برنامه ای با Floating Action Bar داشته باشید که داده ها را در حالی که وضعیت بارگیری را نشان می دهد واکشی می کند و پس از واکشی موفقیت آمیز، رابط کاربری خود را به روز می کند.

 

state اشتراک گذاری

یک مورد استفاده رایج که کاربران کتابخانه را مجبور می‌کند کتابخانه‌های بیشتری را جذب کنند، اشتراک‌گذاری state بین view ها است. راه رسیدن به این هدف با استفاده از Singletons است. Singleton یک شی نمونه سراسری است، به این معنی که شما نمی توانید بیش از یک نمونه از آن کلاس ها را همزمان داشته باشید.

 

ساختن مخزن خود به صورت تکی

مخزن شما داده ها را واکشی می کند و آن ها را در حافظه پنهان ذخیره می کند. اگر یک کش را به مخزن خود وارد کنید، می‌توانید مخزن خود را تک‌تنی کنید و هر ViewModel می‌تواند مقدار ذخیره‌شده را بگیرد.

 

ساخت ویو مدل خود را به صورت تکی

راه دیگر این است که ViewModel خود را به صورت تکی بسازید، اگر رابط کاربری شما از کامپوننت های Stateful مختلفی تشکیل شده باشد که همزمان رندر می شوند و نیاز به برقراری ارتباط با یکدیگر دارند، این می تواند تکنیک خوبی باشد.

 

نتیجه

ما فقط به شما یک جعبه ابزار دادم تا 99٪ از برنامه هایی را که در پلی استور می بینید بسازید. شما به هیچ کتابخانه مدیریت stateی نیاز نخواهید داشت و بهترین چیز این است که یک بیلد تمیز و زیبا و بدون چیزی اضافه از کتابخانه های دیگر دارید، و شما را مجبور می کند که رابط کاربری زیبای خود را با کدهای متورم، تولید شده به صورت خودکار و جادویی بپیچید.

 

شما باید مطمئن شوید که چرخه عمر رابط کاربری شما به درستی مدیریت می شود، در غیر این صورت، اگر فراموش کردید که اشتراک Observer خود را از Observable خود override کنید، در معرض خطر نشت حافظه قرار می گیرید.

ViewModels شما 100% تست شده است، می توانید از Mockito برای EventObserver خود استفاده کنید و به این ترتیب نه تنها تعامل با مدل اصلی خود (مخزن)، بلکه ترتیب صحیح نحوه انتشار رویدادها را نیز آزمایش خواهید کرد! چقدر جالب است (بیشتر کتابخانه های مدیریت stateی شما را مجبور می کنند که وابستگی های بیشتری برای آزمایش پذیری داشته باشید).

الگویی که به شما معرفی کردیم نیز الزامی به بال زدن ندارد، اما بر روی هر پلتفرمی قابل پیاده سازی است. MVVM الگوی اصلی معماری در توسعه اندروید است.

هر چیزی مزایا و معایب خود را دارد، برخی ممکن است نخواهند خودشان با وضعیت مدیریت سروکار داشته باشند - به قیمت اینکه در خطر جمع آوری بدهی های فنی به دلیل به روز نبودن کتابخانه های مورد استفاده قرار می گیرند - برخی دیگر مایلند کنترل بخش های حیاتی را حفظ کنند. برنامه آنها، مانند مدیریت state - به قیمت داشتن لایه دیگری از پیچیدگی در برنامه آنها (به عنوان مثال ViewEvents).

 

 

#دیزاین_پترن #فلاتر #flutter #mvvm