Using hook_preprocess_views_view_unformatted properly I have the following hook in mymodule.module
function mymodule_preprocess_views_view_unformatted(&amp;$variables) {
  $view = $variables['view'];
  $rows = $variables['rows'];

  if (in_array($view-&gt;id(),
      [
        'my_view',
      ]
    )
  )
    {
        foreach ($rows as $id =&gt; $row) {
          $custom_row_class = 'card-background-' . get_card_background();
          var_dump($custom_row_class);
          $variables['rows'][$id]['attributes']-&gt;addClass([$custom_row_class]);
        }

      var_dump($variables['rows'][0]['attributes']);
    }
}

The output is:
/Users/me/Sites/mysite/web/modules/custom/mymodule/mymodule.module:55:string 'card-background-11' (length=18)

/Users/me/Sites/mysite/web/modules/custom/mymodule/mymodule.module:58:
object(Drupal\Core\Template\Attribute)[7400]
  protected 'storage' =&gt; 
    array (size=1)
      'class' =&gt; 
        object(Drupal\Core\Template\AttributeArray)[7223]
          protected 'value' =&gt; 
            array (size=1)
              ...
          protected 'name' =&gt; string 'class' (length=5)

Right after supposedly adding the class to the rows attributes, it isn't there, and not surprisingly isn't in the attributes value passed to views-view-unformatted.html.twig.
I tested your code locally and the way you are adding the class did work for me. E.g.
function mymodule_preprocess_views_view_unformatted(&amp;$variables) {
  $rows = $variables['rows'];

  foreach ($rows as $id =&gt; $row) {
    $variables['rows'][$id]['attributes']-&gt;addClass(['test-123', 'test-abc']);
  }
}

... worked fine for me. I'd say your var_dump() is not giving you the full picture.
Try this instead:
var_dump($variables['rows'][0]['attributes']-&gt;__toString());

When I var dump with toString() - I see the class indicated in the output - hopefully you will too
/var/www/mysite/docroot/modules/custom/my_module/my_module.module:149:string ' class=&quot;card test-123 test-abc&quot;' (length=31)