ورود و عضویت
0
سبد خرید شما خالی است
0
سبد خرید شما خالی است

آموزش پیاده سازی معماری MVVM در فلاتر

0 دیدگاه
10 دقیقه برای مطالعه

اگر یک مهندس نرم افزار یا برنامه نویس تازه کار هستید حتما در پروژه هایی با این مشکل مواجه شده اید که به دلیل بزرگ شدن ابعاد کار اضافه کردن قابلیت های جدید و یا تغییرات در بخش های مختلف به یک چالش اساسی برای شما تبدیل شده است.

آموزش معماری MVP در فلاتر

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

معماری MVVM

امروزه معماری های مورد استفاده در زمینه برنامه نویسی موبایل و غیره محدود هستند که یکی از محبوب ترین آنها معماری MVVM میباشد.

در این مقاله خیلی درباره مباحث تئوری الگوی MVVM ریز نخواهیم و هدف بررسی و پیاده سازی عملی آن در فریمورک فلاتر میباشد.

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

Model: این بخش مسئول دریافت اطلاعات میباشد که در تعامل نزدیکی با viewmodel است.

View: بخش نمایش اطلاعات میباشد. همچنین viewmdel را از عملکردهای کاربر آگاه میکند.

ViewModel: اطلاعات دریافت شده به بخش view منتقل میکند. همانند یک پل بین دو بخش دیگر عمل میکند.

معماری mvvm

برای شروع کار به سه پکیج زیر احتیاج داریم تا آنها را وارد پروژه خود کنیم.

Provider

Get_it

Equatable

در ویدیو زیر نتیجه نهایی پروژه را مشاهده میکنید.

شروع کدنویسی

در اولین قدم کلاس پایه خود را به نام StateData ایجاد میکنیم که از این کلاس در آینده برای مدیریت state و طراحی رابط کاربری استفاده میکنیم.

abstract class StateData extends Equatable {
  const StateData();
}

در قدم بعدی بخش ViewModel را پیاده سازی میکینم.

abstract class ViewModel<T extends StateData> implements ValueListenable<T> {
  ViewModel(T initial) : _stateDataNotifier = ValueNotifier(initial);

  final ValueNotifier<T> _stateDataNotifier;

  @protected
  set stateData(T value) => _stateDataNotifier.value = value;

  @override
  T get value => _stateDataNotifier.value;

  @override
  void addListener(VoidCallback listener) {
    _stateDataNotifier.addListener(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    _stateDataNotifier.removeListener(listener);
  }

  @mustCallSuper
  void dispose() {
    _stateDataNotifier.dispose();
  }
}

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

برای دستیابی به این قابلیت از اینترفیس ValueListenable  استفاده میکنیم که صفحه مورد نظر با گوش دادن به آن میتواند هر زمان اطلاعات جدیدی دریافت شد را نمایش دهد.

همچنین ابزار مفید دیگری به نام کلاس ValueNotifier در اختیار داریم که ValueListenable  را پیاده سازی میکند و به عنوان نماینده بخش ViewModel عمل میکند. کار این کلاس جلوگیری از بازسازی ویجت مورد نظر بدون تغییر اطلاعات میباشد.

  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

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

abstract class Screen extends StatefulWidget {
  const Screen({Key? key}) : super(key: key);

  @override
  ScreenState createState();
}

abstract class ScreenState<T extends Screen, T2 extends ViewModel<T3>,
    T3 extends StateData> extends State<T> {
  ScreenState();

  late final T2 viewModel;

  Widget buildScreen(BuildContext context);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ValueListenableProvider<T3>.value(value: viewModel),
      ],
      builder: (context, _) => buildScreen(context),
    );
  }

  @override
  @mustCallSuper
  void initState() {
    super.initState();
    viewModel = context.read<ViewModelFactory>().create<T2>();
  }

  @override
  @mustCallSuper
  void dispose() {
    viewModel.dispose();
    super.dispose();
  }
}

در این بخش هیچ اتفاق پیچیده ای رخ نداده است فقط در متد initState یک نمونه ViewModel  ایجاد کرده ایم. همچنین در متد dispose هم منابع مصرفی توسط آن را آزاد کرده ایم. درباره ViewModelFactory  هم نگران نباشید جلوتر در بخش تزریق وابستگی درباره آن صحبت خواهیم کرد.

حالا نوبت به ساخت اولین صفحه برنامه میرسد که این صفحه دارای یک viewmodel و یک stateData نیز میباشد.

الگوی طراحی Factory

کلاسی تحت عنوان HomeData ایجاد میکنیم و کدهای زیر را در آن مینویسیم.

class HomeData extends StateData {
  const HomeData({
    required this.todoTiles,
    required this.showEmptyState,
    required this.showLoading,
  });

  const HomeData.initial()
      : todoTiles = const [],
        showLoading = false,
        showEmptyState = false;

  final List<TodoTileData> todoTiles;
  final bool showLoading;
  final bool showEmptyState;

  @override
  List<Object?> get props => [
        todoTiles,
        showLoading,
        showEmptyState,
      ];
}

همانطور که از کدها احتمالا متوجه شده اید صفحه مورد نظر که قرار است یک لیستی از کارهای قابل انجام را نمایش دهد شامل سه حالت مختلف میباشد. خالی, در حال لود شدن و حالت سوم نمایش لیست میباشد.

کلاس دیگری بصورت زیر برای بخش viewmodel ایجاد میکنیم.

@injectable
class HomeViewModel extends ViewModel<HomeData> {
  HomeViewModel(
    this._todoRepository,
  ) : super(const HomeData.initial());

  final TodoRepository _todoRepository;

  StreamSubscription<List<TodoTileData>>? _todoSub;

  void init() {
    _updateList();

    _todoSub = _todoRepository
        .observeTodoList()
        .map((todoList) => todoList.map(TodoTileData.fromTodo).toList())
        .listen(_onTodoChanged);
  }

  @override
  void dispose() {
    _todoSub?.cancel();
    super.dispose();
  }

  Future<void> changeTodoStatus(String id, bool isDone) async {
    await _todoRepository.updateTodoStatus(id, isDone);
  }

  void _updateState({
    List<TodoTileData>? tiles,
    bool? isLoading,
  }) {
    tiles ??= value.todoTiles;
    isLoading ??= value.showLoading;

    stateData = HomeData(
      todoTiles: tiles,
      showEmptyState: tiles.isEmpty && !isLoading,
      showLoading: isLoading,
    );
  }

  Future<void> _updateList() async {
    _updateState(isLoading: true);

    await _todoRepository.updateTodoList();

    _updateState(isLoading: false);
  }

  void _onTodoChanged(List<TodoTileData> tiles) {
    _updateState(tiles: tiles);
  }
}

به متد _updateState  توجه کنید. تمام پارامترهای این متد اختیاری هستند و میتوانید فقط یک بخش را مثل _updateList بروزرسانی کنید. همچنین در این بخش stateData  ساخته میشود و اطلاعات منطقی برنامه را در خود نگهداری میکند.

نوبت به تکمیل کردن بخش صفحه میباشد. همانطور که گفتیم صفحه ما شامل سه حالت میباشد پس به سه ویحت نیز نیاز داریم. اما این ویجت ها چطوری به اطلاعات صفحه دسترسی پیدا میکنند؟ از طریق کلاس Provider که در پروژه های فلاتر هم بسیار از آن استفاده میشود.

class _ProgressBar extends StatelessWidget {
  const _ProgressBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Selector<HomeData, bool>(
      selector: (_, data) => data.showLoading,
      builder: (context, showLoading, _) => Visibility(
        visible: showLoading,
        child: const CircularProgressIndicator(),
      ),
    );
  }
}

کار این ویجت تنها نمایش یک حالت لودینگ به کاربر میباشد.

class HomeScreen extends Screen {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState
    extends ScreenState<HomeScreen, HomeViewModel, HomeData> {
  @override
  void initState() {
    super.initState();
    viewModel.init();
  }

  @override
  Widget buildScreen(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Todo List')),
      body: Stack(
        fit: StackFit.expand,
        children: [
          _TodoList(
            changeListener: _todoChangeListener,
          ),
          const Center(child: _ProgressBar()),
          const _EmptyState(),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          CreateTodoScreenRoute().push(context);
        },
        child: const Icon(Icons.add),
      ),
    );
  }

  void _todoChangeListener(TodoChange change) {
    viewModel.changeTodoStatus(change.id, change.isDone);
  }
}

class _ProgressBar extends StatelessWidget {
  const _ProgressBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Selector<HomeData, bool>(
      selector: (_, data) => data.showLoading,
      builder: (context, showLoading, _) => Visibility(
        visible: showLoading,
        child: const CircularProgressIndicator(),
      ),
    );
  }
}

class _EmptyState extends StatelessWidget {
  const _EmptyState({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Selector<HomeData, bool>(
      selector: (_, data) => data.showEmptyState,
      builder: (context, showEmptyState, _) => Visibility(
        visible: showEmptyState,
        child: Padding(
          padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
          child: Text(
            'Add something first',
            style: Theme.of(context).textTheme.headline4,
          ),
        ),
      ),
    );
  }
}

class _TodoList extends StatelessWidget {
  const _TodoList({
    Key? key,
    required this.changeListener,
  }) : super(key: key);

  final ValueChanged<TodoChange> changeListener;

  @override
  Widget build(BuildContext context) {
    return Selector<HomeData, List<TodoTileData>>(
      selector: (_, data) => data.todoTiles,
      builder: (context, todoTiles, _) {
        return ListView.separated(
          itemBuilder: (context, index) {
            final data = todoTiles[index];
            return TodoTile(
              key: ValueKey(data.id),
              data: data,
              changeListener: changeListener,
            );
          },
          separatorBuilder: (_, __) => const Divider(),
          itemCount: todoTiles.length,
        );
      },
    );
  }
}

تزریق وابستگی Dependency injection

هر کلاس ViewModel ما از ترکیب سه پکیجی که در ابتدای آموزش قرار داده بودیم تشکیل شده است. همانطور که دیدید کلاس HomeViewModel شامل دستور @injectable بود که به معنی آن است که قابلیت ارائه به get_it  را برای ایجاد نمونه از کلاس را دارد. اما برای پیاده سازی این کار نیاز به ساخت دو کلاس جدید داریم.

  1. ViewModelFactory برای ساخت نمونه از کلاس viewmodel
  2. GlobalProvider که از طریق آن صفحه مورد نظر به وسیله BuildContext به ViewModelFactory دسترسی خواهد داشت.
abstract class ViewModelFactory {
  T create<T extends ViewModel>();
}

@Singleton(as: ViewModelFactory)
class ViewModelFactoryImpl implements ViewModelFactory {
  @override
  T create<T extends ViewModel>() => getIt.get<T>();
}

طراحی کلاس ViewModelFactory مشکل خاصی ندارد. GlobalProvider نیز یک ویجت است که تمام پروژه را داخل آن قرار میدهیم.

class GlobalProvider extends StatelessWidget {
  const GlobalProvider({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider.value(value: getIt.get<ViewModelFactory>()),
      ],
      child: child,
    );
  }
}

امیدوارم که این آموزش برای شما مفید بوده باشد.

دیدگاهتان را بنویسید

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

Hesam
07 آوریل 2022
آموزش فارسی فلاتر
آموزش فارسی flutter