Home > OS >  Flutter memory leak with listview builder
Flutter memory leak with listview builder

Time:08-30

I'm new to flutter and have a memory leak in my program. I was wondering if anyone jumped out to anyone about my code as to why it might be causing a memory leak. I was hoping I made some sort of newbie mistake that someone could call out. I commented out my listviewbuilder row widget and replaced it with simple text, and that seemed to lessen the memory leak. So it seems like it might be row related, but I don't understand how.

I am not scrolling or anything like that I just fire up the memory profiler. It shows that there is an ever-increasing size in the filtered node, but does not say why.

I went back through my changesets and found some code that seemed to be causing the issues. Specifically the memory leak went away when I rmeoveLoadingAnimationWidget:

AnimatedOpacity(
                  opacity: isUpdating ? 1.0 : 0.0,
                  duration: isUpdating
                      ? const Duration(milliseconds: 0)
                      : const Duration(milliseconds:500),
                  child: Row(
                    children: [
                      LoadingAnimationWidget.fourRotatingDots(
                        color: Colors.black12,
                        size: 20,
                      ),
                      SizedBox(
                        width: 3,
                      ),
                      Text(
                        "Saving...",
                        style: TextStyle(color: Colors.grey.shade600),
                      ),
                    ],
                  ),


import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:save_extra_money/main.dart';
import 'package:save_extra_money/shared/loading/LoadingIndicator.dart';
import 'package:save_extra_money/transactions/TransactionUpdaterProvider.dart';
import 'package:save_extra_money/transactions/TransactionsLoadingStates.dart';
import 'package:save_extra_money/transactions/transactionGridRow.dart';
import 'package:http/http.dart' as http;

import '../Shared/ColorContainer.dart';
import '../model/Transaction.dart';
import '../model/webCallInputsAndOutputs/RetrieveTransactionsResults.dart';
import '../shared/globals/GlobalUi.dart';
import 'TransactionsViewModel.dart';

class Transactions extends ConsumerStatefulWidget {
  Transactions({Key? key}) : super(key: key);

  @override
  TransactionsState createState() => TransactionsState();
}

class TransactionsState extends ConsumerState<Transactions> {
  ScrollController? controller;

  @override
  void initState() {
    super.initState();
    controller = ScrollController()..addListener(_scrollListener);
  }

  @override
  void dispose() {
    if (controller != null) {
      controller!.removeListener(_scrollListener);
    }
    super.dispose();
  }

  void _scrollListener() {
    if (controller == null) {
      return;
    }
    // print(controller.position.extentAfter);
    if (controller!.position.extentAfter < 300) {
      ref.read(transactionsViewModel.notifier).addMorePagedData();
    }
  }

  @override
  Widget build(BuildContext context) {

    Size size = MediaQuery.of(context).size;
    var transactionUpdater = ref.watch(transactionUpdaterProvider);
    var viewModel = ref.watch(transactionsViewModel);

    viewModel.loadInitialData();
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(viewModel.getTitle()),
        backgroundColor: GlobalUI.appBar,
        centerTitle: true,
      ),
      body: ColorContainer(
        size: size,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(5.0),
          child:
          getInitialLayout(viewModel, transactionUpdater),
        ),
      ),
    );
  }

  Widget getInitialLayout(TransactionsViewModel viewModel,
      TransactionUpdaterProvider transactionUpdater){
    if(viewModel.state == TransactionsLoadingStates.InitialLoad){
      return LoadingIndicator(text: 'Transactions...');
    }
    else if(viewModel.state == TransactionsLoadingStates.InitialError) {
      return Text('Error: ${viewModel.errorText}');
    }
    else{
      return ListView.builder(
          controller: controller,
          itemCount: viewModel.totalCount,
          itemBuilder: (BuildContext context, int index) {
            if(index==viewModel.transactions.length){
              return Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.only(
                    bottomLeft: Radius.circular(5),
                    bottomRight: Radius.circular(5)
                  ),
                  color: GlobalUI.rowBackground,
                ),
                child: LoadingAnimationWidget.fourRotatingDots(
                  color: Colors.black12,
                  size: 60,
                ),
              );
            }

            if(index>viewModel.transactions.length){
              return SizedBox(height: 0,width: 0,);
            }

            var transaction = viewModel.transactions[index];
            return Text(transaction.id);
            /*
            return TransactionGridRow(
              transaction: transaction,
              isUpdating: transactionUpdater.isUpdating(transaction.id),
            );

             */
          });
    }
  }
}

CodePudding user response:

You should not make api calls in your widget build method

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:save_extra_money/main.dart';
import 'package:save_extra_money/shared/loading/LoadingIndicator.dart';
import 'package:save_extra_money/transactions/TransactionUpdaterProvider.dart';
import 'package:save_extra_money/transactions/TransactionsLoadingStates.dart';
import 'package:save_extra_money/transactions/transactionGridRow.dart';
import 'package:http/http.dart' as http;

import '../Shared/ColorContainer.dart';
import '../model/Transaction.dart';
import '../model/webCallInputsAndOutputs/RetrieveTransactionsResults.dart';
import '../shared/globals/GlobalUi.dart';
import 'TransactionsViewModel.dart';

class Transactions extends ConsumerStatefulWidget {
  Transactions({Key? key}) : super(key: key);

  @override
  TransactionsState createState() => TransactionsState();
}

class TransactionsState extends ConsumerState<Transactions> {
  ScrollController? controller;

  @override
  void initState() {
    super.initState();
    controller = ScrollController()..addListener(_scrollListener);
    viewModel.loadInitialData(); // You can call your async methods in initstate
  }

  @override
  void dispose() {
    if (controller != null) {
      controller!.removeListener(_scrollListener);
    }
    super.dispose();
  }

  void _scrollListener() {
    if (controller == null) {
      return;
    }
    // print(controller.position.extentAfter);
    if (controller!.position.extentAfter < 300) {
      ref.read(transactionsViewModel.notifier).addMorePagedData();
    }
  }

  @override
  Widget build(BuildContext context) {

    Size size = MediaQuery.of(context).size;
    var transactionUpdater = ref.watch(transactionUpdaterProvider);
    var viewModel = ref.watch(transactionsViewModel);

    viewModel.loadInitialData(); // Here is the method you should not call here
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(viewModel.getTitle()),
        backgroundColor: GlobalUI.appBar,
        centerTitle: true,
      ),
      body: ColorContainer(
        size: size,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(5.0),
          child:
          getInitialLayout(viewModel, transactionUpdater),
        ),
      ),
    );
  }

  Widget getInitialLayout(TransactionsViewModel viewModel,
      TransactionUpdaterProvider transactionUpdater){
    if(viewModel.state == TransactionsLoadingStates.InitialLoad){
      return LoadingIndicator(text: 'Transactions...');
    }
    else if(viewModel.state == TransactionsLoadingStates.InitialError) {
      return Text('Error: ${viewModel.errorText}');
    }
    else{
      return ListView.builder(
          controller: controller,
          itemCount: viewModel.totalCount,
          itemBuilder: (BuildContext context, int index) {
            if(index==viewModel.transactions.length){
              return Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.only(
                    bottomLeft: Radius.circular(5),
                    bottomRight: Radius.circular(5)
                  ),
                  color: GlobalUI.rowBackground,
                ),
                child: LoadingAnimationWidget.fourRotatingDots(
                  color: Colors.black12,
                  size: 60,
                ),
              );
            }

            if(index>viewModel.transactions.length){
              return SizedBox(height: 0,width: 0,);
            }

            var transaction = viewModel.transactions[index];
            return Text(transaction.id);
            /*
            return TransactionGridRow(
              transaction: transaction,
              isUpdating: transactionUpdater.isUpdating(transaction.id),
            );

             */
          });
    }
  }
}

CodePudding user response:

  • In addition to batuhand, Avoid this:
if(index>viewModel.transactions.length){
   return SizedBox(height: 0,width: 0,);
}

Each time index is greater than length it will built a useless SizedBox.

  • Why do you use "viewModel.totalCount" and "viewModel.transactions.length"? It should be the same.

  • And if you compare index and length add a minus to length

if(index==viewModel.transactions.length-1)
  • Related