Widget 2.0 Tableview Performance

No problem. It’s more “thinking it through” than actual code (although the code is a little tricky because it’s all so inherently asynchronous - be sure to throw in a few extra print statements).

But on the other hand, you’re probably only 4-5 functions away from having cached web images popping up in your tableViews… And with lazy loading, cached images and smooth as butter scrolling, they’ll be world class tableviews too :slight_smile:

Thanks for the encouragement. I agree! Its amazing what you can do with such little code in Corona SDK once everything is firing off on all cylinders… Those 4-5 functions are going to take me some time to crack but I’ll keep trying.

Aww what the heck, these two are mostly complete, and pretty generic. Ones calls back, the other either returns the file pronto, or delays the callback until when the file does arrive…  If you can figure out how to use them, then your halfway done :slight_smile:

[lua]

local pendingDownloads = {}     – Files we’ve called to download, but haven’t arrived yet.
 

local function downloadComplete(event)          – Calls back original source, clears the pending flag…

    if( event.url ~= nil ) then
        pendingDownloadsevent.url      – Call the completion listener…     
        pendingDownloads[event.url] = nil       – And remove the pending flag…
    end

    return true
end
 –
 – download file takes a full url as arg, and checks actual filename at end to see if it is in cache already. If not, sends the request.
 – NOTE: uses system tempDirectory for storiing/caching files… When complete, calls back completionListener.
 –
 function downloadFile(fileLocation, completionListener)
    
–    print(" – downloadFile() – fileLocation == “, fileLocation)
 
    – Let’s get the base filename, without path…
    local fileName = getBaseFilename(fileLocation)
–    print( " — Found the base fileName, string is: " … fileName )    
 
     – Now  determine if the file is already cached…
     if( cacheFileExists(fileName) ) then
        local tempEvent = {}
        tempEvent.response = fileLocation
        tempEvent.phase = “ended”
        tempEvent.url = fileLocation
–        print(” **** Found the cache file, calling back ***")
        completionListener(tempEvent)       – Simulate OS callback event struct…    
     else     
        if( pendingDownloads[fileLocation] ~= nil ) then    – We’re still waiting for it??
–            print("  **** DOWNLOAD STILL PENDING FOR: “, fileLocation)
            return        
        else    – else not pending or chaced, start the downoload…
            pendingDownloads[fileLocation] = completionListener       
            network.download( fileLocation, “GET”, downloadComplete, fileName ,system.TemporaryDirectory )   
–            print(”  ************************ Downloading actual file ==", fileLocation)            
        end
    end
 
 end

[/lua]

and a couple of the little support functions:

[lua]

– cacheFileExists() – returns true if file exists in app Temp folder, false if not… Done this way because some web servers return large 404 files which count as files… so filesize must be greater than 512 to count/be able to tell…
function cacheFileExists(theFile)

    local retVal = false    – By default, file doesn’t exist
    local path = system.pathForFile( theFile, system.TemporaryDirectory )
    
    if( path ) then                              
–        print("***** Path for File ==", path)
        local fh, errStr = io.open( path, “r” )  
        if( fh ) then   
            local fileLength = fh:seek(“end”)
–            print(" – cache file fileLength == ", fileLength )
            if( fileLength > 512 ) then         – Our server returns an html file with message 404 error that is about 400 bytes long if graphic file requested is NOT there… so check for length of file server returned… Your mileage may vary.
                retVal = true                   – Tell the caller that the file DOES exist…
            end
            io.close(fh)        – close the file…
        end
    end
    
    return retVal
end
 

function getBaseFilename(fileLocation)      – Gets the base filename from a long path string ( the stuff after the last slash \ )

    – Let’s get the base filename, without path…
    local startChar=1
    local endChar=1
    local fileName = fileLocation
    
    – String find is giving me a hard time searching from the end, so we will whittle it down from the beginning…
    – I really should read up and practice with the string library :confused:

    – This entire function could probably be replaced with 1 line of code.  
    while ( startChar ~= nil ) do
        startChar, endChar = string.find(fileName, “/”)
    
–        print(" – found char at position: “, startChar)
        if( startChar ~= nil ) then
            fileName = string.sub(fileName, startChar+1, -1)  – Get the sub to the end of the string
–            print(” - fileName = ", fileName)    
        end
    end
    
    return fileName
end
 

[/lua]

You sir are an angel. Thank you very much!!!

oh snap ksan, I just made a workaround for my row.reRender problem… Since they changed the tableViews, they got rid of that ‘feature’… But after posting this and remembering how it works, I realized my pics are updating onscreen fine, without a reRender function / feature…

Turns out it’s because they are inserted, not just a text field changed… So I did some messing around, and got a text string to rerender on the fly in my tableviews now too… 

I am doing it by keeping a pointer to the rows text object in my other structure, the one I get back from server… When a minute passes (it’s a “minutes ago” counter), my routine scans through the rows, does a _view._rows[i]._view:remove(currentMessages[i].TS) (the last bit is my saved off pointer from when it was created for that row), regenerate the string and then insert into the same place… And it updates onscreen instantly now…

So, I got something out of it too, a way around the dreaded reRender issue for now :slight_smile:

My life is a series of "Oh snap!"s these days so whats yet another one…  :slight_smile:

I’m slowly working my way towards your code. My first step was to get the file name pointer which I now have. Next step is to go through the download process… Wish me luck! 

Thanks much for your help once again.

Making progress. In my onRowRender I go for something like : 

        if row.id ~= nil and row.id ~= “” then

            downloadFile(row.id, downloadComplete)  

        end

                       

        print(" – row.id == ", row.id)

I think I’m stuck at the last step you described as … 

  • populateImage listener a) loads image as picture, and  B) rifles through tableview rows until it finds the one with the mathcing url and does an insert() into the _view for that row

Any chance to get a glimpse at that populateImage listener and your call out to downloadFile in the onRowRender() ?

So sorry to be such a pest. I appreciate all your help. Thank you very much. 

These are full of my app specific stuff – dependent on my data I get from the server for my list… I chopped out a little flotsam and jetsom here and there… Caveat Emptor my friend.

From my image populate listener…

[lua]

– imagePopulateListener(event)    -called back when the image for the row onscreen is downloaded…


 local function imagePopulateListener(event)

    print(" – imagePopulateListener(event)  event.response ==", event.response)
    
    – Let’s see if messageList even exists still, user could have left screen before server replied…
    if( (this ~= nil) and (event ~= nil)) then       – Dont do it if user has left the screen…
        if( this.messageList ~= nil )   then           – Don’t do it if there’s no list of messages somehow
–            print(" – rows == “,  this.messageList._view._categoryGroup._rows)
            local rows = this.messageList._view._rows
            
            local maxRows = #rows#this.messageList.content.rows    
            if( maxRows == nil ) then
                maxRows = 0
            end            
–            print (” – maxRows == ", maxRows)

            if( (maxRows > 0) and (event.url ~= nil) ) then        
–                print(" – messageList.view.content.rows[1] == “, this.messageList.content.rows[1] )
                
                local localFilename = utils.getBaseFilename(event.url)     
–                print(”  – localFilename == ", localFilename)  
                
                – Search for the right list item for this image…
                local rowIndex = 0

                for i=1, maxRows do
        –            print(" – row #, view == “, i, this.messageList.content.rows[i].view)            
–                    print(” – fileloc, imageloc == “, rows[i].FILELOCATION, rows[i].imageLocation)
                    
                    if( rows[i].id ~= nil ) then
–                        print(” – Found one with an image… “, rows[i].id)
                    
                        if( utils.getBaseFilename(rows[i].id) == localFilename ) then
–                            print(” ******** Found the Image **********")
                            rowIndex = i    – Found the right rows index.
                            local myImage = getMessageThumbnail(localFilename)   – Routine that just loads up the image (thumbnail scale)
                          
                            myImage.x = 590                            
                            myImage.y = (rows[rowIndex]._height / 2) - 1    – Center it for now

                            rows[rowIndex]._view:insert(myImage)         – Jam it into the item.                                                        
                            myImage.isVisible = true                            
                            
–                           print("  ******** Added image to row: “, localFilename)                                        
                           refreshWG()                                 – refresh the popups if need be                         
                           break
                        end        
                    end
                end
            else
                print(” *** NO ROWS / no url")
            end
        else
            print(" – messageList == nil")
        end
    else
        print(" – this == nil")
    end
 
[/lua]

And this is the bit of my onRowRender that calls out to get it going if there is an image for the row…

[lua]

        if( utils.isEmpty(currMess.FILELOCATION) == false ) then    – Yes, we have an image for this row
    
            – Let’s see if we have the file in our download cache already…
            local localFilename = utils.getBaseFilename(currMess.FILELOCATION)
            
            if( utils.cacheFileExists(localFilename) == true ) then         – do we have an already downloaded and ready to go image with this message?            
                local myImage = getMessageThumbnail(localFilename)
                
                myImage.x = 588
                myImage.y = math.floor(row.height / 2)     – Center it for now                                                
                myImage.isVisible = true
                row:insert(myImage)       – add it to the row.
                
–                print("  ******** Added image to row: ", localFilename)                    
            else                    
               
                – Let’s call for the the file to be downloaded (it could already be cached, or in process of download too)…
                utils.downloadFile(currMess.FILELOCATION, imagePopulateListener)
                
            end
        end

[/lua]

I’ve had to hack it up a little, but I think it’s all there for reference anyways.

Thank you so very much. Don’t worry about the floatsam. I will clean it up for my application. You’re absolutely right. This is all reference and is most appreciated. I will report back once I can get the images to show. Thanks much once again!!!

OK, apparently you can see the code I just posted… It shows just one line in my browser… Ah well.

But ti sounds like you got it. There’s a bazillion print statements in there, make sure you use them to wade through to the goal line, good luck!

I see the code all messed up but I need to wade through it and clean it up for my application anyways so no worries about the format. 

Progress so far.

I get the first file downloaded and then when its existing I get a stack traceback on the 

pendingDownloadsevent.url

the message tells me I have : 

attempt to call field ‘?’ (a nil value)

Anyways, let me wade through the code above and see if I can make sense of it. 

Thanks much.

OK, the line of code you referenced it crashed on is filled in by the downloadFile() by the line:

pendingDownloads[fileLocation] = completionListener

It is setting an element in the table (pendingDownloads table) to the function pointer to call back. The key to the table (fileLocation) is the url… You should just check around these two lines of code to make sure the key matches. That would be my guess. No key match, no function pointer to call back.

@mpappas… you know what I’m going to ask next right… if you give a mouse a cookie… next he will ask for 

local myImage = getMessageThumbnail(localFilename)   :) 

Can I see inside that getMessageThumbNail() function?

I promise this will be the last thing I ask. I am so grateful for your help. Thank you so very very much!!!

[lua]

– getMessageThumbnail() – returns a thumbnail image for use on message screen.

local function getMessageThumbnail(localFilename)

local myImage = display.newImage( localFilename, system.TemporaryDirectory, 0, 0 )

    if( myImage ~= nil ) then
        myImage.width = 88               – Thumbnail size… It can be a little cropped if message is short… is ok…
        myImage.height = 132
        myImage.isVisible = false       – Lets make sure it doesn’t pop onscreen somewhere for now…
        myImage:removeSelf()           – Remove it from stage for now
    else
        print(" – error getting thumbnail - ", localFilename)
    end
    
    return myImage
end

[/lua]

Wohooo!!! I am sooo happy. Thank you so so very much!!!

My images download well without choking the tableview. 

If they are there when I bring the tableView up then they display well. 

If they were not there when I brought the tableView up they download nicely in the background. 

If I scroll down and then come back then the images show. Magic!!!

The only thing thats not working is for the image to show at the end of the download if I don’t move the tableView.

I think I need to fix the imagePopulateListener() code and I’ll be running all the way!!!

Thank you so very much. I could not have done this on my own ever!

Well done, congrats :slight_smile:

With all the lazy loading, caching and all going on - scrolling through potentially thousands of images (but only having the minimum handful of images in texture memory) in a list from a remote server… this tableView stuff scrolls pretty @#$%@#$ good, if ya ask me :slight_smile:

My caching idea and all is useful, but the lazy loading from the tableview is the real magic IMHO, without it memory would overflow from all the images being loaded in the list at once, so we’d have to write all our own lazy loading machinery. Instead, it’s built in, woohoo!

Regarding the first download not populating / being visible – I had some issues with making sure images didn’t populate *after* they scrolled out of view at first, and maybe something if off by default in there, But mine populate when downloaded without moving, just a matter of tracking it down I think. 

Its all your doing. I just “painted by numbers” and almost got Mona Lisa but not 100% yet!!!  :slight_smile:

I agree. the lazyloading is wonderful. And your event driven approach to getting the images to download and load when ready is a best in class implementation thats for sure. Thanks much for sharing this freely. 

I think I know the issue why my images are not downloading on first pass after completing the downloads. Its because I did not yet fix whats inside imagePopulateListener() where you look for a row containing the right file name etc… 

I am a little confused by the references to “this” in that function. I understand what it is doing but not sure how to use it yet. Searching CL docs for “this” is proving impossible. I’m referring to calls like the following : 

local maxRows = #rows – # this.messageList.content.rows

Almost there!!!

ah, the “this”. Thats my overall displaygroup for the entire screen.

this.messageList is where I keep my pointer to the tableView around / handy. I check “this” a lot at the beginning of functions (if this ~= nil) in case the user exits (and app destroys the screen), but a delayed call returns… Big no-no to add images when the screen (this) was destroyed.

So when I create the tableView, I insert it into the screen (this:insert(messageList), and I also set a pointer to it for use later (this.messageList = messageList). Kinda do that with a lot of graphic objects I might want to “throttle” a little bit later.

I think I get it!

So this is like “group” in the StoryBoard defaults from CL  i.e.

   local group = self.view

which you see in every storyBoard function. 

I see and understand what you do there. I just need to make it work for my scene. Almost there. 

Thank you so very much!!!

It works!!! I am so happy!!!

I spent a while adapting the insides of imagePopulateListener() and thought all was ok but it was still not working. imagePopulateListener() was running a about quarter of its code and then stopping.

I literally resorting to putting numbered print(“1”), print(“2”) etc after every line of code to see where the code is choking and guess what… I think I came across something funny in CL SDK. Might be even a bug. Not sure…

lines with the following reference 

     print(" – row #, view == ", i, tableView.content.rows[i].view)   

make code execution stop without any Corona errors. I did not see the rest of print()s I planted in there so I rem’med those suckers and guess what… it worked… so whatever was happening there it all goes away once you stop using 

tableView.content.rows[i].view

I am so very happy. Thank you very very much once again. I owe you big time.