11

I want to complete get request to server to get data for my app at it start. I read several topics that describe how to run method after building widgets. But all of them are describe situations when provider is not using. And I am not sure that it's good idea to do this request inside widget.

I tried several approaches but did not get success. Here is my code:

void main() async {
      runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider<TenderApiData>(
          builder: (_) => TenderApiData(), child: HomePage()),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(), body: MainContainer());
  }

}

class TenderApiData with ChangeNotifier {
  String access_token;
  List<Map<String, String>> id_names;

  String access_token_url =  "https://...";

  getApiKey() async { // I need to call this method at app start up 
    var response = await http
        .post(access_token_url, headers: {"Accept": "application/json"});
    if (response.statusCode == 200) {
      access_token = json.decode(response.body)['access_token'];
      notifyListeners();
    }
  }

}

class MyTestWidget extends StatefulWidget {
  MyTestWidgetState createState() => MyTestWidgetState();

}

class MyTestWidgetState extends State<MyTestWidget> {

  bool isKeyGetted = false;

  // before I used this when I extracted data on click event. 
  // I am not sure that it's needed now
  @override
  void didChangeDependencies() { 
    if (!isKeyGetted) {
      Provider.of<TenderApiData>(context).getApiKey();
      isKeyGetted = !isKeyGetted;
    }
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {

    if (!isKeyGetted) {
      Provider.of<TenderApiData>(context).getApiKey();
      isKeyGetted = !isKeyGetted;
    }

    var result = Provider.of<TenderApiData>(context).access_token;
    var test = Provider.of<TenderApiData>(context).id_names;
    return Column(
      children: <Widget>[
        RaisedButton(
          onPressed: Provider.of<TenderApiData>(context).getRegionsList,
          child: Text("get regions"),
        ),
      ],
    );
  }


}

class MainContainer extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Table(
      children: [
        TableRow(children: [
          Row(
            children: <Widget>[
              Container(child: MyTestWidget()),
              Container(child: Text("Regions"),),

              Expanded(child: SelectRegions(),  )
            ],
          )
        ]),
        TableRow(children: [
          Row(
            children: <Widget>[
              Text("Label"),
              Text("Value"),
            ],
          )
        ]),
      ],
    );
  }
}
Dmitry Bubnenkov
  • 9,415
  • 19
  • 85
  • 145

4 Answers4

6

You can store TenderApiData as member of MyApp, make a startup call in MyApp constructor and pass existing instance to descendants.
Here is how it will look:

class MyApp extends StatelessWidget {

  final TenderApiData _tenderApiData = TenderApiData();

  MyApp() {
    _tenderApiData.getApiKey();
  };

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ChangeNotifierProvider<TenderApiData>(
          builder: (_) => _tenderApiData, child: HomePage()),
    );
  }
}

Other classes will stay unchanged.

Another option would be to pass TenderApiData as constructor parameter into MyApp, making it more testable.

void main() {
  final TenderApiData tenderApiData = TenderApiData();
  tenderApiData.getApiKey(); // call it here or in MyApp constructor - now it can be mocked and tested
  runApp(MyApp(tenderApiData));
}

class MyApp extends StatelessWidget {

  final TenderApiData _tenderApiData;

  MyApp(this._tenderApiData);

// ...
Mikhail Ponkin
  • 2,563
  • 2
  • 19
  • 19
  • thanks! it's there any profits from your solution? Or Another answer about constructor are totally equivalent? – Dmitry Bubnenkov Aug 02 '19 at 10:35
  • While I've been thinking about that I came up with another more clean solution. It would be best to pass `TenderApiData` instance as parameter into `MyApp`. It will make code more testable. Another solution will work too if you have single instance of `TenderApiData`. If you create it multiple times - it will make call every time it's created. Also IMHO it adds some complexity to testing and debugging. – Mikhail Ponkin Aug 02 '19 at 11:11
  • thanks! But what if I need call: await _tenderApiData.getApiKey(); _tenderApiData.getRegionsList(); (the second method could not be run befor first compelte). Could constructor be `async` ? – Dmitry Bubnenkov Aug 02 '19 at 12:02
  • No, constructor has to be synchronous. It can start futures, but can't `await` for result. However it's possible to schedule chain calls using `.then` on futures – Mikhail Ponkin Aug 02 '19 at 12:11
  • Could you add mention about it to your answer? – Dmitry Bubnenkov Aug 02 '19 at 12:14
  • I still do not understand how to call two function. In both cases I need to use constructor. – Dmitry Bubnenkov Aug 02 '19 at 12:17
  • @DmitryBubnenkov either create helper function which can be async `void myFunction() async { await _tenderApiData.getApiKey(); _tenderApiData.getRegionsList() }` or chain calls using .then `_tenderApiData.getApiKey().then((_) => _tenderApiData. getRegionsList());` – Mikhail Ponkin Aug 02 '19 at 13:05
2

You can add a constructor on your TenderApiData do trigger custom logic:

class TenderApiData with ChangeNotifier {
  TenderApiData() {
    // TODO: call `getApiKey`
  }
}
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
0

You can use FutureProvider value.

Separate method api to service (my_service.dart):

class MyService {
  Future<String> getApiKey() async {
    // I need to call this method at app start up
    var response = await http
        .post(access_token_url, headers: {"Accept": "application/json"});
    if (response.statusCode == 200) {
      return json.decode(response.body)['access_token'];
    }
  }
}

And than call from MyApp

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureProvider<String>.value(
      value: MyService().getApiKey(),
      child: HomePage(),
    );
  }
}

In HomePage:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String token = Provider.of<String>(context);
    return Scaffold();
  }
}
windupurnomo
  • 1,121
  • 1
  • 11
  • 22
0

You could also just use a boolean flag to run the method only once.

Provider

import 'screen.abstract.dart';

class MyProvider with ChangeNotifier {
  _hasInitialized = false;

  // This will only be run once, when called
  fetchApiOnce() {
    if (_hasInitialized) {
      return;
    }
    _hasInitialized = true;

    /* DO STUFF */
  }
}
Ben Winding
  • 10,208
  • 4
  • 80
  • 67