[Flutter] Stacked 패키지 Reactive Provider 설계하기

2023. 3. 19. 20:08Developers 공간 [Shorts]/Frontend

728x90
반응형
<분류>
A. 수단
- OS/Platform/Tool : Linux, Kubernetes(k8s), Docker, AWS
- Package Manager : node.js, yarn, brew, 
- Compiler/Transpillar : React, Nvcc, gcc/g++, Babel, Flutter

- Module Bundler  : React, Webpack, Parcel

B. 언어
- C/C++, python, Javacsript, Typescript, Go-Lang, CUDA, Dart, HTML/CSS

C. 라이브러리 및 프레임워크 및 SDK
- OpenCV, OpenCL, FastAPI, PyTorch, Tensorflow, Nsight

 


1. What? (현상)

Stacked Package에는 다양한 ViewModel을 제공합니다.

  • IndexTrackingViewModel : BottomNavigationBar에서쓰이는 index를 tracking하기 위한 View Model입니다.
  • StreamViewModel : StreamBuilder 대신에 Stream을 View와 ViewModel을 Bind해서 사용하기 위한 ViewModel 입니다.
  • FutureViewModel : Future를 다루기 위한 View Model 입니다.
  • ReactiveViewModel : ReactiveServiceMixin으로 구현된 Service와 연동해 사용하기 위한 ViewModel입니다. ReactiveServiceMixin은 Provider로 하나의 값을 Listen하는 서비스를 구현하는데 상속하는 class입니다.
  • MultiFutureViewModel & MultiStreamViewModel 
더보기

-----------------------------------------------------------------------------------------------------

<ViewModel 별 예시 : v3.1.0 이전 기준이므로 참조만 합니다>

  • IndexTrackingViewModel
// View
class HomeView extends StatelessWidget {
  const HomeView({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ViewModelBuilder<HomeViewModel>.reactive(
      builder: (context, model, child) => Scaffold(
        body: getViewForIndex(model.currentIndex),
        bottomNavigationBar: BottomNavigationBar(
          ...
        ),
      ),
      viewModelBuilder: () => HomeViewModel(),
    );
  }

  Widget getViewForIndex(int index) {
    switch (index) {
      case 0:
        return PostsView();
      case 1:
        return TodoView();
      default:
        return PostsView();
    }
  }
}
// ViewModel
class HomeViewModel extends IndexTrackingViewModel {
}
  •  StreamViewModel
// View
class StreamCounterView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ViewModelBuilder<StreamCounterViewModel>.reactive(
      builder: (context, viewModel, child) => Scaffold(
            body: Center(
              child: Text(viewModel.title),
            ),
          ),
      viewModelBuilder: () => StreamCounterViewModel(),
    );
  }
}
// ViewModel
class StreamCounterViewModel extends StreamViewModel<int> {

  String get title => 'This is the time since epoch in seconds \n $data';

  @override
  Stream<int> get stream => locator<EpochService>().epochUpdatesNumbers();
}
// Service (registered using injectable, NOT REQUIRED)
@lazySingleton
class EpochService {
  Stream<int> epochUpdatesNumbers() async* {
    while (true) {
      await Future.delayed(const Duration(seconds: 2));
      yield DateTime.now().millisecondsSinceEpoch;
    }
  }
}
  • FutureViewModel
// ViewModel
class FutureExampleViewModel extends FutureViewModel<String> {
  @override
  Future<String> futureToRun() => getDataFromServer();

  Future<String> getDataFromServer() async {
    await Future.delayed(const Duration(seconds: 3));
    return 'This is fetched from everywhere';
  }
}
  • ReactivewViewModel
// Service
class InformationService with ReactiveServiceMixin { //1
  InformationService() {
    //3
    listenToReactiveValues([_postCount]);
  }

  //2
  ReactiveValue<int> _postCount = ReactiveValue<int>(initial: 0);
  int get postCount => _postCount.value;

  void updatePostCount() {
    _postCount.value++;
  }

  void resetCount() {
    _postCount.value = 0;
  }
}
// ViewModel
class WidgetOneViewModel extends ReactiveViewModel {
  // You can use get_it service locator or pass it in through the constructor
  final InformationService _informationService = locator<InformationService>();

   @override
  List<ReactiveServiceMixin> get reactiveServices => [_informationService];
}

-----------------------------------------------------------------------------------------------------

 

이번엔 ReactiveServiceMixin을 활용해 State(상태)를 관리하기 위한 Provider를 구현하는 방법을 정리해보고자 합니다.

** Provider 패턴 : Flutter에서 제안한 플러그인으로, Bloc과 같이 상태관리를 하기 위해 활용합니다. 기존에 State가 변할 때마다 Widget Tree를 매번 모두 다시 Rendering 해야하는 단점을 보완하고, 많은 위젯 간의 데이터의 공유를 위해서 따로 Provider라는 객체를 만들어 관리하는 것입니다.


2. Why? (원인)

  • X

3. How? (해결책)

Provider에 관한 설명과, 기본적으로 필자의 프로젝트 구성방식에 대해 이해하시려면 아래 링크를 참조하시면 좋습니다.

https://tkayyoo.tistory.com/56

 

  • Object 구현하기 : o2_1_MyState.dart 에 가장 기본적으로 공유하고 싶은 Object를 만들어주었습니다.
import 'package:flutter/material.dart';

class MyState {
  Color savedcolor = Colors.white;
  DateTime saveddate = DateTime.now();
  String savedstring = "초기값";

  MyState() {}
}
  • ChangeNotifierProvider Service만들어주기 : stacked패키지를 사용하므로 아래 명령어를 활용해 my_provider_service.dart를 만들어주고, 아래와 같이 구성해주었습니다.
    • ListenableServiceMixin을 mixin하여 기능을 모두 가져 옵니다.
    • 내부에 ReactiveValue라는 공유할 변수를 선언해주고, instance를 선언해줍니다.
    • listenToReactiveValues()를 통해 .addListener()형태를 만들어줍니다.
    • 외부에서 state를 변화할 때의 상황을 구현해주고, notifyListners()를 통해 나를 Subscribe하고 있는 instance들에게 알려주도록 합니다.
stacked create service my_provider
import 'package:stacked/stacked.dart';
import 'package:flutter/material.dart';
import 'package:project/OBJECTS/o2_1_MyState.dart';

class MyProviderService with ListenableServiceMixin {
  ReactiveValue<MyState> _listenableValue = ReactiveValue<MyState>(
      new MyState());

  MyProviderService() {
    listenToReactiveValues([_listenableValue]);
  }

  MyState get mystate => _listenableValue.value;

  void updateInstance(MyState state) {
    this._listenableValue.value = state;
    notifyListeners();
  }
  void updateColor(Color state) {
    this._listenableValue.value.savedcolor = state;
    notifyListeners();
  }
  void updateDate(DateTime state) {
    this._listenableValue.value.saveddate = state;
    notifyListeners();
  }
  void updateString(String state) {
    this._listenableValue.value.savedstring = state;
    notifyListeners();
  }
}
  • View/ViewModel 구현하기 : 이제 stacked 패키지에서 이것들을 Subscribe할 위젯을 만들어 보겠습니다. 역시나  stacked패키지를 사용하므로 아래 명령어를 활용해 view/viewmodel을 만들어줍니다
    • View에서 ViewModel의 onDatePickerChange()onTextFieldChange() 함수를 만들고, getMyState getter를 만들어 두었네요. 아래에서 ViewModel 을 살펴보겠습니다.
stacked create view my_view
// View
class MyView extends StackedView<MyViewModel> {
  const MyView({Key? key}) : super(key: key);
  
  @override
  Widget builder(
    BuildContext context,
    MyViewModel viewModel,
    Widget? child,
  ) {
    return 
          ...
          GestureDetector(
            onTap: () {
              DatePicker.showDatePicker(context, onConfirm: (value) {
              	viewModel.onDatePickerChange(value);
              }
            );
          },
          ...
          Text(
            DateFormat('yy.MM.dd.').format(viewModel.getMyState.saveddate),
            style: TextStyle(
              color: viewModel.getMyState.savedcolor,
            ),
          ))
          ...
          TextField(
            controller: viewModel.textController!,
            onSubmitted: (String text) {
              viewModel.onTextFieldChange(text);
            },
          )
    );
  }

  @override
  MyViewModel viewModelBuilder(
    BuildContext context,
  ) => MyViewModel();

  @override
  void onViewModelReady(MyViewModel viewModel) =>
      SchedulerBinding.instance
          .addPostFrameCallback((timeStamp) => viewModel.init());
}
  • ViewModelReactiveViewModel을 상속해 만들었습니다.
    • (필수) locator<MyProviderService>() 를 활용해 Provider 서비스를 사용할 것을 선언해줍니다.
    • (필수) List<ListenableServiceMixin>를 return하는 listenableServices를 override 해 줌으로써 어떤 객체를 addListener()할 지 명시 해줍니다.
    • getMyState를 활용해 해당 Provider의 값을 얻는 방법을 구현해주었습니다.
    • 기존에 Provider 서비스에서 만들어준 update() 함수들을 활용해 업데이트를 하고 해당 값을 이용해 구현해둔 view를 위해서 notifyListener()과 같은 rebuildUi() 함수를 call해줍니다.
// ViewModel

import 'package:stacked/stacked.dart';
import 'package:flutter/material.dart';
import 'package:project/app/app.locator.dart';
import 'package:project/services/my_provider_service.dart';
import 'package:project/OBJECTS/o2_1_MyState.dart';

class MyViewModel extends ReactiveViewModel {
  TextEditingController? textController;
  final MyProviderService stateController = locator<MyProviderService>();
  
  MyState get getMyState => stateController.mystate;
  
  @override
  List<ListenableServiceMixin> get listenableServices => [stateController];

  void init(){
    textController = TextEditingController(text:getMyState.savedstring);
    if(textController==null){
      print("Error");
    }else{
      rebuildUi();
    }
  }

  void onTextFieldChange(String text){
    stateController.updateString(text);
    rebuildUi();
  }
  void onDatePickerChange(DateTime date){
    stateController.updateDate(date);
    rebuildUi();
  }

  @override
  void dispose() {
    textController!.dispose();
    super.dispose();
  }
}

 

 


https://morioh.com/p/24d6359a5b92

728x90
반응형