Triggering page controller animation from ViewModel

Viewed 102

i'm trying to implement a View with a PageView widget, that is being controlled by the ViewModel. In my view i have buttons that trigger the following method in the ViewModel:

  @override
  nextPage() {
    if (_index < 4) {
      _index++;
      increaseProgress();
      currentIndex.add(_index);
    }
  }

currentIndex is just a sink of my _currentIndexController, and outputCurrentIndex is a stream of the controller:

  @override
  Sink get currentIndex => _currentIndexController.sink;

  @override
  Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) =>  currentIndex);

it looks like the value is added to the stream successfully, but i can't get it to trigger changing pages, i've set up this listener in the initState() method of the view:

_viewModel.outputCurrentIndex.listen((index) {
  _pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});

but it is not triggered for some reason.. what am i doing wrong?

here is a full code of my View:

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

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

class _RegisterViewState extends State<RegisterView> {
  final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
  final PageController _pageController = PageController(initialPage: 0);
  final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
  final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
  final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);

  @override
  void initState() {
    _bind();
    super.initState();
  }
  @override
  void dispose(){
    _viewModel.dispose();
    super.dispose();
  }

  _bind() {
    _viewModel.start();
    _viewModel.outputCurrentIndex.listen((index) {
      _pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
    });
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> pagesList = [
      const SexPage(),
      AgePage(
        scrollController: _ageScrollController,
      ),
      WeightPage(scrollController: _weightScrollController),
      HeightPage(scrollController: _heightScrollController),
    ];


    return MultiProvider(
      providers: [
        StreamProvider.value(value: _viewModel.outputProgress, initialData: 0.25),
        StreamProvider.value(value: _viewModel.outputCurrentIndex, initialData: 0),
      ],
      child: Scaffold(
        backgroundColor: ColorManager.backgroundColor,
        appBar: AppBar(
          systemOverlayStyle: SystemUiOverlayStyle(
            statusBarColor: ColorManager.backgroundColor,
            statusBarBrightness: Brightness.dark,
            statusBarIconBrightness: Brightness.dark,
          ),
          centerTitle: true,
          title: AppBarWidget(),
          elevation: AppSize.s0,
        ),
        body: PageView(
          reverse: true,
          controller: _pageController,
          physics: NeverScrollableScrollPhysics(),
          children: [...pagesList],
        ),
      ),
    );
  }
}

class AppBarWidget extends StatefulWidget {
  const AppBarWidget({
    Key? key,
  }) : super(key: key);

  @override
  State<AppBarWidget> createState() => _AppBarWidgetState();
}

class _AppBarWidgetState extends State<AppBarWidget> {
  final RegisterViewModel _viewModel = getIt<RegisterViewModel>();

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      mainAxisSize: MainAxisSize.max,
      children: [
        Expanded(
          flex: 1,
          child: InkWell(
            child: Text(
              AppStrings.skip,
              style: Theme.of(context).textTheme.labelMedium,
            ),
            onTap: () => _viewModel.nextPage(),
          ),
        ),
        Expanded(
          flex: 4,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
            child: Transform(
              alignment: Alignment.center,
              transform: Matrix4.rotationY(pi),
              child: Progresso(
                progress: Provider.of<double>(context),
                progressStrokeCap: StrokeCap.round,
                backgroundStrokeCap: StrokeCap.round,
                progressColor: ColorManager.primary,
                backgroundColor: ColorManager.progressBarBackgroundGrey,
                progressStrokeWidth: 10.0,
                backgroundStrokeWidth: 10.0,
              ),
            ),
          ),
        ),
        Expanded(
          flex: 1,
          child: Provider.of<int>(context) > 1
              ? InkWell(
                  child: Row(
                    children: [
                      Text(
                        AppStrings.back,
                        style: Theme.of(context).textTheme.labelMedium,
                      ),
                      Icon(
                        Icons.arrow_forward_ios,
                        color: ColorManager.subtitleGrey,
                      ),
                    ],
                  ),
                  onTap: () => _viewModel.previousPage(),
                )
              : Container(),
        ),
      ],
    );
  }
}

and my ViewModel:

class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
  final StreamController _progressBarController = StreamController<double>.broadcast();
  final StreamController _currentIndexController = StreamController<int>.broadcast();
  final StreamController _isBackEnabled = StreamController<bool>.broadcast();

  double _progress = 0.25;
  int _index = 0;

  @override
  void dispose() {
    _progressBarController.close();
    _currentIndexController.close();
    _isBackEnabled.close();
  }

  @override
  void start() {
    // TODO: implement start
  }

  @override
  Sink get currentIndex => _currentIndexController.sink;

  @override
  Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) =>  currentIndex);
  
  @override
  Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);


  @override
  Sink get progress => _progressBarController.sink;

  @override
  setCurrentIndex(int index) {
    currentIndex.add(index);
  }

  @override
  increaseProgress() {
    if (_progress <= 1.0) {
      _progress += 0.25;
      progress.add(_progress);
    }
  }

  @override
  decreaseProgress() {
    if (_progress > 0) {
      _progress -= 0.25;
      progress.add(_progress);
    }
  }

  @override
  Sink get isBackEnabled => _isBackEnabled.sink;

  @override
  Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);

  @override
  setIsBackEnabled(int index) {
    _isBackEnabled.add(index > 0 ? true : false);
  }

  @override
  nextPage() {
    if (_index < 4) {
      _index++;
      increaseProgress();
      setCurrentIndex(_index);
    }
  }

  @override
  previousPage() {
    if (_index > 0) {
      _index--;
      decreaseProgress();
      setCurrentIndex(_index);
    }
  }
}

abstract class RegisterViewModelInputs {
  register();

  increaseProgress();

  decreaseProgress();

  nextPage();

  previousPage();

  setIsBackEnabled(int index);

  Sink get currentIndex;

  Sink get isBackEnabled;

  Sink get progress;

}

abstract class RegisterViewModelOutputs {
  Stream<int> get outputCurrentIndex;

  Stream<double> get outputProgress;

  Stream<bool> get outputIsBackEnabled;

}

EDIT: I have moved the stream listener to the _AppBarWidgetState build method, and it seems to work now, but i don't fully understand why it hasn't worked before.. is it because the PageController wasn't assigned to a view yet? where is the correct place for the listener? it doesn't makes sense to me for it to be in a child widget.

0 Answers
Related