Flutter - Как обновить состояние (или значение?) Future/List, используемого для построения ListView (через FutureBuilder)

Я вставляю соответствующий код ниже, но вы можете ответить на него, основываясь на моем псевдо-объяснении.

Я использую FutureBuilder для создания списка.

  • Я начинаю с использования init() для асинхронного HTTP-вызова API и разбора его в список объектов (Location), отображаемых для представления результата json.
  • Этот список местоположений затем возвращается в Future<List<Location>> _listFuture переменная (которая является будущим для FutureBuilder).
  • Как только будущее "возвращается" или "заканчивается", FutureBuilder запускается и использует ListView.builder/Container/ListTile для циклического прохождения и построения списка.
  • В какой-то момент я захочу обработчик onTap() (в ListTile), который изменяет цвет фона любого элемента списка.
  • Чтобы поддержать это, у меня есть член backgroundColor в классе Location (который содержит ответы JSON), для которого я по умолчанию использую "#fc7303" для всех элементов (предположим, что все изначально изначально не проверено). Затем я хочу изменить фон того, что выбрано на "#34bdeb" в onTap().
  • Я предполагаю, что я мог бы вызвать setState(), который вызовет обновление, и новый цвет фона будет замечен / использован при перерисовке.

Проблема в том, что ListView/Contrainer/ListTile управляется

Future<List<Location>>

, Я могу передать "повернутый" индекс моему обработчику ontap, но я не верю, что мой _changeBackground() может просто обновить значение backgroundColor для выбранного индекса и вызвать setState(), потому что вы не можете напрямую получить доступ или обновить будущее таким образом (Я получаю ошибку ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.)

Я не уверен, что выбрал правильный подход. В этом случае, я думаю, я всегда мог теоретически разделить отслеживание "фонового" цвета на новый отдельный список (вне будущего) и отслеживать / ссылаться на него таким образом, используя выровненные индексы из onTap().

Тем не менее, я не уверен, что это всегда будет работать. В будущем мне может понадобиться изменить значения / состояние того, что было возвращено в будущем. Например, подумайте, хочу ли я иметь возможность щелкнуть элемент списка и обновить "companyName". В этом случае я бы изменил значение, сохраненное в будущем напрямую. Полагаю, я мог бы технически отправить новое имя на сервер и полностью обновить список таким образом, но это кажется неэффективным (что, если они решат "отменить", а не сохранить изменения?).

Любая помощь приветствуется. Спасибо!

этот класс на самом деле содержит соответствующие данные для списка

// Location
class Location {

  // members
  String locationID;
  String locationName;
  String companyName;
  String backgroundColor = 'fc7303';

  // constructor?
  Location({this.locationID, this.locationName, this.companyName});

  // factory?
  factory Location.fromJson(Map<String, dynamic> json) {
    return Location(
      locationID: json['locationID'],
      locationName: json['locationName'],
      companyName: json['companyName'],
    );

  }

}

этот класс является родительским ответом json с сообщениями "результат" (успех / ошибка). он создает приведенный выше класс в виде списка для отслеживания фактических записей компании / местоположения

//jsonResponse
class jsonResponse{

  String result;
  String resultMsg;
  List<Location> locations;

  jsonResponse({this.result, this.resultMsg, this.locations});

  factory jsonResponse.fromJson(Map<String, dynamic> parsedJson){

    var list = parsedJson['resultSet'] as List;
    List<Location> locationList = list.map((i) => Location.fromJson(i)).toList();
    return jsonResponse(
        result: parsedJson['result'],
        resultMsg: parsedJson['resultMsg'],
        locations: locationList
    );
  }

} // jsonResponse

Вот виджеты состояния и состояния, которые используют вышеупомянутые классы для анализа данных API и создания ListView.

class locationsApiState extends State<locationsApiWidget> {

  // list to track AJAX results
  Future<List<Location>> _listFuture;

  // init - set initial values
  @override
  void initState() {
    super.initState();
    // initial load
    _listFuture = updateAndGetList();
  }

  Future<List<Location>> updateAndGetList() async {

    var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
    if (response.statusCode == 200) {
      var r1 = json.decode(response.body);
      jsonResponse r = new jsonResponse.fromJson(r1);
      return r.locations;
    } else {
      throw Exception('Failed to load internet');
    }

  }

  _changeBackground(int index){
    print("in changebackground(): ${index}");       // this works!
    _listFuture[index].backgroundColor = '34bdeb';   // ERROR: The operator '[]' isn't defined for the class 'Future<List<Location>>'.
  }

  // build() method
  @override
  Widget build(BuildContext context) {

    return new FutureBuilder<List<Location>>(
        future: _listFuture,
        builder: (context, snapshot){

          if (snapshot.connectionState == ConnectionState.waiting) {
            return new Center(
              child: new CircularProgressIndicator(),
            );
          } else if (snapshot.hasError) {
            return new Text('Error: ${snapshot.error}');
          } else {
            final items = snapshot.data;
            return new Scrollbar(
              child: new RefreshIndicator(
                  child: ListView.builder(
                    physics: const AlwaysScrollableScrollPhysics(),
                    //Even if zero elements to update scroll
                    itemCount: items.length,
                    itemBuilder: (context, index) {
                      return
                        Container(
                            color: HexColor(items[index].backgroundColor),
                            child:
                            ListTile(
                              title: Text(items[index].companyName),
                              onTap: () {
                                print("Item at $index is ${items[index].companyName}");
                                _changeBackground(index);
                              }  // onTap
                            )
                        );
                    },
                  ),
                  onRefresh: () {
                    // implement later
                    return;
                  } // refreshList,
              ),
            );
          }// else
        } // builder
    ); // FutureBuilder
  } // build
} // locationsApiState class


class locationsApiWidget extends StatefulWidget {
  @override
  locationsApiState createState() => locationsApiState();
}

вспомогательный класс (взятый где-то в stackru) для преобразования HEX в целочисленные цвета

class HexColor extends Color {
  static int _getColorFromHex(String hexColor) {
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) {
      hexColor = "FF" + hexColor;
    }
    return int.parse(hexColor, radix: 16);
  }

  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}

Спасибо!

1 ответ

Я бы порекомендовал удалить цвет фона из вашего класса местоположения и вместо этого сохранить в своем штате, какие местоположения выбраны. Таким образом, ваш список местоположений не нужно будет изменять при выборе элементов. Я бы также создал StatelessWidget для вашего элемента местоположения, который установил бы цвет фона, в зависимости от того, выбран он или нет. Так:

// for the LocationItem widget callback
typedef void tapLocation(int index);

class locationsApiState extends State<locationsApiWidget> {

  // list to track AJAX results
  Future<List<Location>> _listFuture;
  final var selectedLocationIndices = Set<int>();

  // init - set initial values
  @override
  void initState() {
    super.initState();
    // initial load
    _listFuture = updateAndGetList();
  }

  Future<List<Location>> updateAndGetList() async {

    var response = await http.get("http://XXX.XXX.XXX.XXX/api/listCompanies.php");
    if (response.statusCode == 200) {
      var r1 = json.decode(response.body);
      jsonResponse r = new jsonResponse.fromJson(r1);
      return r.locations;
    } else {
      throw Exception('Failed to load internet');
    }
  }

  void _toggleLocation(int index) {
    if (selectedLocationIndices.contains(index))
      selectedLocationIndices.remove(index);
    else
      selectedLocationIndices.add(index);
  }

  // build() method
  @override
  Widget build(BuildContext context) {

    return new FutureBuilder<List<Location>>(
        future: _listFuture,
        builder: (context, snapshot){

          if (snapshot.connectionState == ConnectionState.waiting) {
            return new Center(
              child: new CircularProgressIndicator(),
            );
          } else if (snapshot.hasError) {
            return new Text('Error: ${snapshot.error}');
          } else {
            final items = snapshot.data;
            return new Scrollbar(
              child: new RefreshIndicator(
                  child: ListView.builder(
                    physics: const AlwaysScrollableScrollPhysics(),
                    //Even if zero elements to update scroll
                    itemCount: items.length,
                    itemBuilder: (context, index) {
                      return LocationItem(
                        isSelected: selectedLocationIndices.contains(index),
                        onTap: () => setState({
                          _toggleLocation(index);
                        })
                      );
                    },
                  ),
                  onRefresh: () {
                    // implement later
                    return;
                  } // refreshList,
              ),
            );
          }// else
        } // builder
    ); // FutureBuilder
  } // build
} // locationsApiState class


class locationsApiWidget extends StatefulWidget {
  @override
  locationsApiState createState() => locationsApiState();
}

И запись в списке предметов:

class LocationItem extends StatelessWidget {

  final bool isSelected;
  final Function tapLocation;

  const LocationItem({@required this.isSelected, @required this.tapLocation, Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: isSelected ? HexColor('34bdeb') : HexColor('fc7303'),
      child: ListTile(
        title: Text(items[index].companyName),
        onTap: () => tapLocation() // onTap
      )
    );
  }
}

Простите, я не могу его скомпилировать, поэтому надеюсь, что это правильно. Но я думаю, вы уловили идею: пусть виджет с отслеживанием состояния отслеживает отдельно выбранные местоположения, и пусть местоположение решает, как отрисовывать себя после восстановления.

Возможно, вам придется использовать ListviewBuilder вместо FutureBuilder. У меня была аналогичная проблема, когда мне приходилось загружать данные из firestore, манипулировать ими и только затем отправлять их в ListView, поэтому я не мог использовать FutureBuilder. По сути, я зациклил QuerySnapShot, внес необходимые изменения в каждый документ, а затем добавил его в объект List (List chatUsersList = List();):

String seenText = "";
chatSnapShot.forEach((doc) {
      if (doc.seen) {
        seenText = "not seen yet";
      } else {
        seenText = "seen " + doc.seenDate;
      }
      ...
      chatUsersList.add(Users(id, avatar, ..., seenText));
    }

Затем в ListViewBuilder:

ListView.builder(
  itemBuilder: (context, index) {       
    return
   UserTile(uid, chatUsersList[index].peerId,
      chatUsersList[index].avatar,..., chatUsersList[index].seenText);
  },
  itemCount: chatUsersList.length, ),

А затем в UserTile:

class UserTile extends StatelessWidget {
  final String uid;
  final String avatar;
  ...
  final String seenText;

  ContactTile(this.uid, this.avatar, ..., this.seenText);
  @override
  Widget build(BuildContext context) {

    var clr = Colors.blueGrey;
    if (seenText == "not seen yet") clr = Colors.red;
    ...

    return
    ListTile(
      isThreeLine: true,
      leading:
      Container(

        width: 60.0,

        height: 60.0,
        decoration: new BoxDecoration(
          shape: BoxShape.circle,
          image: new DecorationImage(
            fit: BoxFit.cover,
            image: new CachedNetworkImageProvider(avatar),
          ),
        ),
      ),
      title: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: < Widget > [
          Expanded(
            child: Text(
              name, style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, ),
              overflow: TextOverflow.ellipsis,
              maxLines: 1
            )
          ),
          Text(
            timestamp, style: TextStyle(fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis,
            maxLines: 1
          ),
        ],
      ),
      subtitle: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: < Widget > [
          Text(sub, style: TextStyle(color: Colors.black87, fontSize: 14.0, ),
            overflow: TextOverflow.ellipsis, ),
          Text(**seenText**, style: TextStyle(color: **clr**, fontSize: 12.0, fontStyle: FontStyle.italic), ),
        ],
      ),
      trailing: Icon(Icons.keyboard_arrow_right),
      onTap: () {
     ...

      });

  }

Другие вопросы по тегам