How to change ListTile data with mobx without rebuilding the whole page #Flutter

Viewed 393

I am learning the Flutter state management with mobx, and I want to change the context of the dynamic listtile without rebuilding the whole page, I tried observable list from Mobx store but seems not to work here is my samples.

This sample I want to click the item the name change to clicked without rebuilding the screen, when I click nothing happens but when I go back to another page and come back then I found out that the name of the animal changed.

This is my stateless widget

Class AnimalPageList extends StatelessWidget{
 
 @override
 Widget build(BuildContext context){    

var animalStore = context.watch<AnimalStore>();
var animals = animalStore.animalsList;

return Scaffold(
  appBar: AppBar(
    title: Text("Animals Page"),
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
       Observer(builder: (_) =>
         ListView.builder(
           scrollDirection: Axis.vertical,
           shrinkWrap: true,
           itemCount: animals.length,
           itemBuilder: (BuildContext context, int index) {
             var pet = animalStore.getAnimal(index);
             //final petName = context.select((ObservableList<Animal> animals)=>animals[index].name);
             return ListTile(
                   key: ValueKey(pet.id),
                   enabled: true,
                   onTap: () async {
                     var rename = animals[index].name +" clicked";
                     animals[index].name = rename;
                   },
                   title: Observer(builder: (_) => Text(pet.name)),

             );
           },
         ),
       ),
      ],
    ),
  ),
);

And here is my model with the store

class Animal {
  final int id;

  @observable
  String name;

  Animal({this.id, this.name});

  factory Animal.fromJson(Map<dynamic, dynamic> json) {
    return new Animal(
      id: json['id'],
      name: json['name'],
    );
  }

  Map<dynamic, dynamic> toJson() {
    final Map<dynamic, dynamic> data = new Map<dynamic, dynamic>();
    data['id'] = this.id;
    data['name'] = this.name;
    return data;
  }
}

class AnimalStore = _AnimalStore with _$AnimalStore;

abstract class _AnimalStore with Store{


  @observable
  ObservableList animalsList = ObservableList<Animal>.of(
    [
      Animal(id: 1, name: 'cat'),
      Animal(id: 2, name: 'dog'),
      Animal(id: 3, name: 'mouse'),
      Animal(id: 4, name: 'horse'),
      Animal(id: 5, name: 'frog'),
    ]
  );

  @action
  getAnimal(int i){
    return animalsList[i];
  }


}

Which way I can arrange, the model, the store and the sreenwidget so that if I click on a button it changes the name as desired whithout requiring to rebuild the whole screen.

2 Answers

From your code it seems the list size is static and it doesn't change over time, based on this fact you can go for a simple solution as follow:

First, you are observing the whole list, and also the Text widget inside it, over-engineering here.

Just wrap the Observer around the ListTile, so that only a particular ListTile will rebuild upon updating to its observed var. Use UniqueKey() for the key of each element.

Also, you need to reference the vars store inside the Observer. You can call store actions anywhere without the need to be inside an Observer widget.

You need to create an action method to update the name of the animal.

Tips: avoid creating new vars inside a loop or a list that is used only for a temporary hold, the "var rename" inside the builder, is not a good practice.

Normally I create a global instance of the object representing the store and can be re-used by any widget/files on your project.

AnimalStore animalStore = AnimalStore();

The code should be adapted as follow:

//_AnimalStore class

   @action
   setAnimalName(int i, string name){
     animalsList[i].name = name;
   }

//AnimalPageList class

 Class AnimalPageList extends StatelessWidget{
     
     @override
     Widget build(BuildContext context){    
    
    // you dont realy need them
    var animalStore = context.watch<AnimalStore>();
    var animals = animalStore.animalsList;
    
    return Scaffold(
      appBar: AppBar(
        title: Text("Animals Page"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
           ListView.builder(
               scrollDirection: Axis.vertical,
               shrinkWrap: true,
               itemCount: animalStore.animalsList.length,
               itemBuilder: (BuildContext context, int index) {
                 return Observer(builder: (_) => ListTile(
                       key: ValueKey(pet.id),
                       enabled: true,
                       onTap: () {
                        animalStore.setAnimalName(index, animalStore.getAnimal(index).name +" clicked");
                       },
                       title: Text(animalStore.getAnimal(index)),
    
                 ));
               },
             ),
          ],
        ),
      ),
    );

First, I think @fpatelm already told everything I am going to say in different ways. Just adding almost the same explanation in a different way to try to make things more clear.

Changing the value of an element inside an ObservableList, will not update the screen, unless each individual inside the ObservableList is also an Observable. Observable lists without Observables as elements only changes on changing its size (ObservableList.length).

One way to solve this is to rebuild the Observable list when changing any element on it.

@action
void onEditElement(Animal obj){
    final elemIndex = animalsList.findElementIndex(obj);
    final aux = ObservableList<Animal>.of([]);
    aux.copyAll(animalsList);
    aux[elemIndex] = obj;
    a.nimalsList = aux;
}

Second, you are cloning the store inside a {}, which means you are in a different scope then the Store, not exactly sure if this is a problem, but looks like to me.

Try to create custom tile, the receives the store value and return the ListTile:

CustomTile extends StatelessWidget{
    const CustomTile(Animal pet);
    Builder{
        return ListTile(key: UniqueKey(), title: pet.name, ...,);
    }
}

itemBuilder: (BuildContext context, int index) {
    return CustomTile(
         animalStore.getAnimal(index),
    );
}

If it does not solve the problem try adding key: UniqueKey(); instead of pet.id.

Related