1

I followed this excellent Riverpod tutorial. In the last steps the author uses the following code:

final _buttonState = Provider<ButtonState>((ref) {
  return ref.watch(timerProvider.state).buttonState;
});
final buttonProvider = Provider<ButtonState>((ref) {
  return ref.watch(_buttonState);
});

and

final _timeLeftProvider = Provider<String>((ref) {
  return ref.watch(timerProvider.state).timeLeft;
});
final timeLeftProvider = Provider<String>((ref) {
  return ref.watch(_timeLeftProvider);
});

I tried using _buttonState and _timeLeftProvider and, from what I see, the app works correctly. So, my questions are:

  • What need is there to create and use buttonProvider and timeLeftProvider?
  • How many Providers are really needed?

Thank you very much!

2020-10-26 UPDATE (main.dart code and output image)

My main.dart code is:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_timer_app/timer.dart';

final timerProvider = StateNotifierProvider<TimerNotifier>(
  (ref) => TimerNotifier(),
);

final _buttonState = Provider<ButtonState>((ref) {
  return ref.watch(timerProvider.state).buttonState;
});

final buttonProvider = Provider<ButtonState>((ref) {
  return ref.watch(_buttonState);
});

final _timeLeftProvider = Provider<String>((ref) {
  return ref.watch(timerProvider.state).timeLeft;
});
final timeLeftProvider = Provider<String>((ref) {
  return ref.watch(_timeLeftProvider);
});

void main() {
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('building MyHomePage');

    return Scaffold(
      appBar: AppBar(title: Text('My Timer App')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TimerTextWidget(),
            SizedBox(height: 20),
            ButtonsContainer(),
          ],
        ),
      ),
    );
  }
}

class TimerTextWidget extends HookWidget {
  const TimerTextWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final timeLeft = useProvider(timeLeftProvider);

    print('building TimerTextWidget $timeLeft');

    return Text(
      timeLeft,
      style: Theme.of(context).textTheme.headline2,
    );
  }
}

class ButtonsContainer extends HookWidget {
  const ButtonsContainer({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('building ButtonsContainer');

    final state = useProvider(buttonProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (state == ButtonState.initial) ...[
          StartButton(),
        ],
        if (state == ButtonState.started) ...[
          PauseButton(),
          SizedBox(width: 20),
          ResetButton(),
        ],
        if (state == ButtonState.paused) ...[
          StartButton(),
          SizedBox(width: 20),
          ResetButton(),
        ],
        if (state == ButtonState.finished) ...[
          ResetButton(),
        ],
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building StartButton');
    return FloatingActionButton(
      onPressed: context.read(timerProvider).start,
      child: Icon(Icons.play_arrow),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building PauseButton');

    return FloatingActionButton(
      onPressed: context.read(timerProvider).pause,
      child: Icon(Icons.pause),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building ResetButton');

    return FloatingActionButton(
      onPressed: context.read(timerProvider).reset,
      child: Icon(Icons.replay),
    );
  }
}

If I click on the ‘Play’ button and then let the 10 seconds pass, in the end I get the same result in the 2 cases:

Output

2020-10-27 UPDATE (main.dart code without using buttonProvider and timeLeftProvider)

This is the output even if buttonProvider and timeLeftProvider are not used, like in the following main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_timer_app/timer.dart';

final timerProvider = StateNotifierProvider<TimerNotifier>(
  (ref) => TimerNotifier(),
);

final _buttonState = Provider<ButtonState>((ref) {
  return ref.watch(timerProvider.state).buttonState;
});

// final buttonProvider = Provider<ButtonState>((ref) {
//   return ref.watch(_buttonState);
// });

final _timeLeftProvider = Provider<String>((ref) {
  return ref.watch(timerProvider.state).timeLeft;
});

// final timeLeftProvider = Provider<String>((ref) {
//   return ref.watch(_timeLeftProvider);
// });

void main() {
  runApp(
    const ProviderScope(child: MyApp()),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('building MyHomePage');

    return Scaffold(
      appBar: AppBar(title: Text('My Timer App')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TimerTextWidget(),
            SizedBox(height: 20),
            ButtonsContainer(),
          ],
        ),
      ),
    );
  }
}

class TimerTextWidget extends HookWidget {
  const TimerTextWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final timeLeft = useProvider(_timeLeftProvider);

    print('building TimerTextWidget $timeLeft');

    return Text(
      timeLeft,
      style: Theme.of(context).textTheme.headline2,
    );
  }
}

class ButtonsContainer extends HookWidget {
  const ButtonsContainer({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('building ButtonsContainer');

    final state = useProvider(_buttonState);

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (state == ButtonState.initial) ...[
          StartButton(),
        ],
        if (state == ButtonState.started) ...[
          PauseButton(),
          SizedBox(width: 20),
          ResetButton(),
        ],
        if (state == ButtonState.paused) ...[
          StartButton(),
          SizedBox(width: 20),
          ResetButton(),
        ],
        if (state == ButtonState.finished) ...[
          ResetButton(),
        ],
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building StartButton');
    return FloatingActionButton(
      onPressed: context.read(timerProvider).start,
      child: Icon(Icons.play_arrow),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building PauseButton');

    return FloatingActionButton(
      onPressed: context.read(timerProvider).pause,
      child: Icon(Icons.pause),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    print('building ResetButton');

    return FloatingActionButton(
      onPressed: context.read(timerProvider).reset,
      child: Icon(Icons.replay),
    );
  }
}

What am I doing wrong?

Marco
  • 593
  • 8
  • 23
  • 1
    `buttonProvider` and `timeLeftProvider` are created to prevent unnecessary rebuilds of time left string and various buttons. – gegobyte Oct 26 '20 at 14:12

1 Answers1

5

Those providers are used to prevent unnecessary rebuilds but aren't fundamentally necessary. Only create providers you need - especially as these providers will never be disposed of in the app lifecycle, they are just wasted space. However, preventing unnecessary rebuilds should be the top priority.

In the linked article, the author is utilizing a workaround recommended by the package author to prevent rebuilds when listening to a specific attribute of a StateNotifier. So, for now, that is the most efficient way to accomplish the task. I will try to update this answer if new functionality is introduced to solve it.

I would refer to the package creator's examples for more context.

Here's a quick example of why you might use multiple providers to cache responses from an external API:

class ExampleApiRepository {
  ExampleApiRepository(this._read);

  static final provider = Provider((ref) => ExampleApiRepository(ref.read));

  final Reader _read;

  Future<Example> search(String query) async {
    final response = await _call('api/example/$query');
    return Example.fromJson(response.data);
  }
}

final searchExample = FutureProvider.family<Example, String>((ref, query) async {
  return ref.watch(ExampleApiRepository.provider).search(query);
});

In this example, if the same query is passed to the searchExample provider, it will return the previously fetched result. Could this be achieved without multiple providers? Yes - and for most cases this will hold true. Creating a provider is about convenience and efficiency. So don't be afraid to use many providers, but don't create them for the sake of creating them.

That said, the article you linked is informative and appreciated.

Alex Hartford
  • 5,110
  • 2
  • 19
  • 36
  • Thank you very much! – Marco Oct 23 '20 at 18:34
  • 1
    Those providers are [actually suggested](https://github.com/rrousselGit/river_pod/issues/158#issuecomment-699638027) by the creator of Riverpod to prevent unnecessary rebuilds. – gegobyte Oct 26 '20 at 14:52
  • Hi, @gegobyte, I updated my answer to reflect this. Thanks for pointing that out. – Alex Hartford Oct 26 '20 at 15:19
  • Hi @gegobyte. The output I have shows that the the numbers of builds is the same. In other words, there are no extra rebuilds. So, I still don’t understand why I should use that workaround. – Marco Oct 26 '20 at 17:10
  • @Marco No it can't be same. Please check again, without those 2 providers, every time `timeLeft` changes, buttons will also get rebuilt. For example, time left changes from 10 seconds to 9 seconds and then 8 seconds. You are not touching pause and restart button and yet they are also getting rebuilt every second along with the `timeLeft` string. – gegobyte Oct 26 '20 at 18:03
  • @gegobyte what’s the best way to show you my code and output? By modifying the question and adding them there? – Marco Oct 26 '20 at 22:19
  • @Marco In your updated question, you are using those extra providers so you are preventing unnecessary rebuilds. If you remove those providers, you would be rebuilding every thing every second. – gegobyte Oct 27 '20 at 04:45
  • @gegobyte the output would be the same even if you use `final timeLeft = useProvider(_timeLeftProvider);` in `TimerTextWidget` and `final state = useProvider(_buttonState);` in `ButtonsContainer`. I will update my question in order to be clearer. – Marco Oct 27 '20 at 13:19
  • @Marco The author of that timer article had also posted question and answer on this very topic. You can find it [here](https://stackoverflow.com/q/64346019/4152581). – gegobyte Oct 27 '20 at 13:26
  • @gegobyte I’m sorry, but I don’t understand. In that question he explains why we need `timeLeftProvider` and `buttonProvider`. My problem is that I have the *same* output even *without using them* (as I show in the 2020-10-27 update of my question). – Marco Oct 27 '20 at 13:40
  • 1
    I decided to pull the repo and check myself. @Marco is correct. I'm not sure what to think here but those providers are not impacting rebuilds at all. – Alex Hartford Oct 27 '20 at 15:39
  • 1
    I think that [this](https://github.com/rrousselGit/river_pod/issues/158#issuecomment-718744644) will help to understand. – Marco Oct 29 '20 at 13:45