flutter bloc and how a bloc state change can then trigger a 2nd async call before updating the Widget/UI with both? (code attached)

Issue

How would one load a set of local images for display in a widget (noting this is async – see function below) triggered by (based on) a change in state from a flutter_bloc for “settings”? Noting this bloc is persisted too via hydrated_bloc. So the use case for which I’m asking how do I code this in flutter (noting I’m using flutter_bloc) is:

Use Case – Render different set of images on the same Widget I have for dynamically displaying a room for a 2D point & click adventure type game, BASED ON the “room” the user goes to. A change in room event is passed to a SettingsBloc which will then determine the new “room” to move to. This new “room” state is available via the Bloc concept, however how & where do I then do the async load of the specific set of images I need for this room (nothing they are set at compile time)? (To feed it to the dynamic room rendering widget with a CustomPainter that I have – i.e. images painted onto canvas).

For example which approach is recommended to do this? (then ideally what the code looks like to achive this)

a) Listen for a change in “room” within the widget, and then trigger (within a widget) to call the async function to dynamically load the ui.Image’s that I need? But if yes, how do you do this in code within a Widget? (or is this not best practice). Refer my code below which does work/run but seems to have a infinite loop happening OR

b) Should I setup Images as a separate bloc (e.g. images_bloc). But in this case what is the flutter code that would be required to do this: i.e.

  • user does something in UI that triggers an event & passes it to SettingsBloc
  • the SettingsBloc then may determine there is a change in "room" due to this and change/emit the new "currentRoom"
  • the SettingsBloc would then then to trigger an aynch request to ImagesBlock (somehow) get the new List of ui.Images for this "room", using the async code below
  • The UI/Widget then needs to pickup the changes for “String room” and List of ui.Image’s

c) Another approach?


Here is my best try at approach (a) above so far. Works in UI re changing background, however there seems to be an infinite loop:

Logging:

Launching lib/main.dart on iPhone 12 Pro Max in debug mode...
lib/main.dart:1
Xcode build done.                                           29.5s
Connecting to VM Service at ws://127.0.0.1:49364/lyUIDblZgmY=/ws
flutter: game_main.build ----------------------------
flutter: trying to get room data for current room: room2
flutter: About to load imagename:plane.png
flutter: game_main.build ----------------------------
flutter: trying to get room data for current room: room2
flutter: About to load imagename:plane.png
flutter: game_main.build ----------------------------
flutter: trying to get room data for current room: room2
flutter: About to load imagename:plane.png
flutter: game_main.build ----------------------------
ETC ETC

Code:

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:adventure/ui/widgets/game_painter.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:adventure/bloc/settings_cubit.dart';
import 'package:adventure/game_design/room.dart';
import 'package:flutter/material.dart';
import 'package:touchable/touchable.dart';

class GameMain extends StatefulWidget {
  GameMain({Key key}) : super(key: key);
  @override
  _GameMainState createState() => _GameMainState();
}

class _GameMainState extends State<GameMain> {
  final RoomsData roomsData = RoomsData.getPopulated();
  ui.Image _backgroundImage;

  Future<ui.Image> _loadImageAsync(imageString) async {
    ByteData bd = await rootBundle.load(imageString);
    final Uint8List bytes = Uint8List.view(bd.buffer);
    final ui.Codec codec = await ui.instantiateImageCodec(bytes);
    final ui.Image image = (await codec.getNextFrame()).image;
    return image;
  }

  Future<void> _updateBackgroundImageState(String imagename) async {
    print('About to load imagename:$imagename');
    final image = await _loadImageAsync('assets/images/$imagename');
    setState(() {
      _backgroundImage = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('game_main.build ----------------------------');

    // Get Persistant Data Settings 
    SettingsCubit settingsCubit = context.watch<SettingsCubit>();

    // Get Room Configuration (for room we're now in)
    print('Trying to get room data for current room: ${settingsCubit.state.currentRoom}');
    Room roomData = roomsData.getRoom(settingsCubit.state.currentRoom);

    // Update Background Image
    _updateBackgroundImageState(roomData.backgroundImage);
    
    // Create and return Canvas
    return Container(
      child: BlocBuilder<SettingsCubit, SettingsState>(
        builder: (context, state) {
          return Stack(children: <Widget>[

            CanvasTouchDetector(
              builder: (context) => CustomPaint(
                painter: GamePainter(context, settingsCubit, roomData, _backgroundImage)
              )
            ),

            FlatButton(
              // For testing changes to background image
              color: Colors.blue,
              textColor: Colors.white,
              onPressed: () {
                var newRoomString = settingsCubit.state.currentRoom == 'room1' ? 'room2' : 'room1';
                settingsCubit.setRoom(newRoomString);
              },
              child: Text(
                "Flat Button",
                style: TextStyle(fontSize: 20.0),
              ),
            ),

          ]);
        }
      )
    );

  }
}

Solution

The problem of yours code is that you put the setStatus inside build, so it call build -> _updateBackgroundImage -> setStatus -> rebuild -> _updateBackgroundImage … (infinite)

I think the date update flow looks like:

change SettingsCubit -> get RoomData ->  (wait for async image data) -> _updateBackgroundImage

The point is what do you want to show if the image is still in loading?

The answers may be like:

  1. Change background/room until the image loaded
  2. Show Loading Indicator
  3. Show sync data first, and change the background later

The last two can be done easily with FutureBuilder. But for bloc concept, you can add new state inside SettingsCubit and control the image state when loading

class SettingsCubit extends Cubit{
  ...
  Future setRoom(String newRoom) async {
    // emit state (imageIsLoading = true)
    // load image async func
    // emit state (imageIsLoading = false, image data update in state)
  }
}
...

@override
Widget build(BuildContext context) {
  SettingsCubit settingsCubit = context.watch<SettingsCubit>();

  if(settingsCubit.state.imageIsLoading){
    // show what do you want to show when image is not ready
  }else{
    // show room with image (ex. from settingsCubit.state.imageData)
  }

Of course you can add new bloc like ImagesBloc for more structured code.

Answered By – yellowgray

Answer Checked By – Candace Johnson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *