diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-22 11:19:32 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-22 11:19:32 +0200 |
| commit | 44d4a132c193907ee53de180ad28e697828009e3 (patch) | |
| tree | 001b9088b55dfd7d4ab73a1687bce1abe3da6e47 | |
| parent | bf319064f127e3cf97a46d2c172bdacdc8f8a783 (diff) | |
| download | mygram-droid-44d4a132c193907ee53de180ad28e697828009e3.tar.gz mygram-droid-44d4a132c193907ee53de180ad28e697828009e3.zip | |
Fix lifecycle, focus loss, crossfade and tab race review findings
- MainActivity: remove rotate callbacks in onDestroy, ignore connect
results once destroyed, rotate the background only while resumed,
guard the fade end action.
- HomeRows: each row owns one TileAdapter over a mutable list; rebinds
refill it and notifyDataSetChanged instead of setAdapter, so focus and
scroll survive a sample arriving (change payload keeps the holder).
- Crossfade: flip front only on onResourceReady, cancel the animation
before resetting alpha.
- Tab loads ignore errors from a tab that is no longer shown; Home does
not fire a second /folders request while one is in flight.
- Load the instance logo after login in connect().
- FolderActivity: request grid focus after layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | app/src/main/java/mygram/droid/FolderActivity.java | 2 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/HomeRows.java | 10 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/MainActivity.java | 59 |
3 files changed, 54 insertions, 17 deletions
diff --git a/app/src/main/java/mygram/droid/FolderActivity.java b/app/src/main/java/mygram/droid/FolderActivity.java index 8da2533..10a9940 100644 --- a/app/src/main/java/mygram/droid/FolderActivity.java +++ b/app/src/main/java/mygram/droid/FolderActivity.java @@ -53,7 +53,7 @@ public final class FolderActivity extends Activity { Collections.shuffle(posts); grid = Ui.grid(this, Ui.postTiles(this, posts), Ui.POST_W, Ui.POST_H, 6); show(grid); - grid.requestFocus(); + grid.post(grid::requestFocus); // after layout }, e -> show(Ui.text(this, "Cannot load folder: " + e.getMessage(), 18))); } diff --git a/app/src/main/java/mygram/droid/HomeRows.java b/app/src/main/java/mygram/droid/HomeRows.java index ed638b6..e4abeb5 100644 --- a/app/src/main/java/mygram/droid/HomeRows.java +++ b/app/src/main/java/mygram/droid/HomeRows.java @@ -37,11 +37,14 @@ final class HomeRows extends RecyclerView.Adapter<HomeRows.Row> { static final class Row extends RecyclerView.ViewHolder { final TextView title; final RecyclerView tiles; + /** Backing list of the row's one adapter: rebinds refill it instead of replacing the adapter. */ + final List<TileAdapter.Tile> list = new ArrayList<>(); Row(LinearLayout v, TextView title, RecyclerView tiles) { super(v); this.title = title; this.tiles = tiles; + tiles.setAdapter(new TileAdapter(list, Ui.POST_W, Ui.POST_H)); } } @@ -80,17 +83,18 @@ final class HomeRows extends RecyclerView.Adapter<HomeRows.Row> { samples.put(f.slug, posts); // marks it loading, so it is fetched once Api.async(() -> Api.folderSample(f, SAMPLE), p -> { samples.put(f.slug, p); - notifyItemChanged(folders.indexOf(f)); + notifyItemChanged(folders.indexOf(f), Boolean.TRUE); // payload: keep the holder, so focus stays }, e -> { // the row keeps only its Continue tile }); } - List<TileAdapter.Tile> tiles = new ArrayList<>(); + List<TileAdapter.Tile> tiles = row.list; + tiles.clear(); for (Post p : posts) tiles.add(TileAdapter.post(p, () -> open(f, p))); tiles.add(new TileAdapter.Tile(null, "Continue", R.drawable.ic_arrow_forward, -1, () -> FolderActivity.open(activity, f))); - row.tiles.setAdapter(new TileAdapter(tiles, Ui.POST_W, Ui.POST_H)); + row.tiles.getAdapter().notifyDataSetChanged(); } @Override diff --git a/app/src/main/java/mygram/droid/MainActivity.java b/app/src/main/java/mygram/droid/MainActivity.java index 6f8d76d..7578f05 100644 --- a/app/src/main/java/mygram/droid/MainActivity.java +++ b/app/src/main/java/mygram/droid/MainActivity.java @@ -59,7 +59,8 @@ public final class MainActivity extends Activity { private RecyclerView homeList; private HomeRows homeRows; private int tab = HOME; - private boolean ready; + private boolean ready, resumed, homeLoading; + private ImageView logo; @Override protected void onCreate(Bundle state) { @@ -98,11 +99,9 @@ public final class MainActivity extends Activity { bar.setGravity(Gravity.CENTER_VERTICAL); int p = Ui.dp(this, 16); bar.setPadding(p * 2, p, p * 2, 0); - LinearLayout logo = Ui.iconButton(this, R.drawable.ic_home, "Home", v -> show(HOME)); - // Foldergram's logo is AGPLv3: loaded from the instance, never bundled. - Glide.with(this).load(Api.resolve(Prefs.server(), "/icon-192.png")).error(R.drawable.ic_home) - .into((ImageView) logo.getChildAt(0)); - bar.addView(logo); + LinearLayout home = Ui.iconButton(this, R.drawable.ic_home, "Home", v -> show(HOME)); + logo = (ImageView) home.getChildAt(0); + bar.addView(home); bar.addView(new Space(this), new LinearLayout.LayoutParams(0, 1, 1)); bar.addView(Ui.iconButton(this, R.drawable.ic_reels, "Reels", v -> Playlist.openReels(this))); bar.addView(Ui.iconButton(this, R.drawable.ic_favorite, "Liked", v -> show(LIKED))); @@ -121,12 +120,19 @@ public final class MainActivity extends Activity { Likes.load(); return Api.randomFeed(); }, posts -> { + if (isDestroyed()) + return; ready = true; + // Foldergram's logo is AGPLv3: loaded from the instance after login, never bundled. + Glide.with(this).load(Api.resolve(Prefs.server(), "/icon-192.png")).error(R.drawable.ic_home).into(logo); backgrounds = posts; handler.removeCallbacks(rotate); rotateBackground(); show(tab); - }, this::failed); + }, e -> { + if (!isDestroyed()) + failed(e); + }); } private void failed(Exception e) { @@ -151,20 +157,31 @@ public final class MainActivity extends Activity { return; } message("Loading…"); + if (homeLoading) + return; // the request in flight shows the list when it lands + homeLoading = true; Api.async(Api::folders, folders -> { + homeLoading = false; homeRows = new HomeRows(this, folders); homeList = Ui.list(this, RecyclerView.VERTICAL); homeList.setAdapter(homeRows); if (tab == HOME) setContent(homeList); - }, this::failed); + }, e -> { + homeLoading = false; + if (tab == t) + failed(e); + }); } else if (t == LIKED) { message("Loading…"); Api.async(Api::likes, posts -> { if (tab == LIKED) setContent(posts.isEmpty() ? centered("No liked posts yet") : Ui.grid(this, Ui.postTiles(this, posts), Ui.POST_W, Ui.POST_H, 6)); - }, this::failed); + }, e -> { + if (tab == t) + failed(e); + }); } else { message("Loading…"); Api.async(Api::folders, folders -> { @@ -175,13 +192,17 @@ public final class MainActivity extends Activity { tiles.add(new TileAdapter.Tile(f.avatarUrl, f.name, R.drawable.ic_folder, -1, () -> FolderActivity.open(this, f))); setContent(Ui.grid(this, tiles, 160, 160, 5)); - }, this::failed); + }, e -> { + if (tab == t) + failed(e); + }); } } @Override protected void onResume() { super.onResume(); + resumed = true; if (!ready) return; handler.removeCallbacks(rotate); @@ -195,7 +216,14 @@ public final class MainActivity extends Activity { @Override protected void onPause() { super.onPause(); + resumed = false; + handler.removeCallbacks(rotate); + } + + @Override + protected void onDestroy() { handler.removeCallbacks(rotate); + super.onDestroy(); } private void rotateBackground() { @@ -204,6 +232,7 @@ public final class MainActivity extends Activity { Post p = backgrounds.get(ThreadLocalRandom.current().nextInt(backgrounds.size())); // Never pass the old drawable as a Glide placeholder: Glide may recycle its bitmap. ImageView back = layers[1 - front], top = layers[front]; + back.animate().cancel(); back.setAlpha(0f); back.bringToFront(); Glide.with(this).load(p.media.get(0).thumbnailUrl).listener(new RequestListener<Drawable>() { @@ -215,12 +244,16 @@ public final class MainActivity extends Activity { @Override public boolean onResourceReady(Drawable d, Object model, Target<Drawable> target, DataSource source, boolean first) { - back.animate().alpha(1f).setDuration(1500).withEndAction(() -> Glide.with(top).clear(top)).start(); + front = 1 - front; // only now, so a failed load leaves the old layer on top + back.animate().alpha(1f).setDuration(1500).withEndAction(() -> { + if (!isDestroyed()) + Glide.with(top).clear(top); + }).start(); return false; } }).into(back); - front = 1 - front; - handler.postDelayed(rotate, Prefs.backgroundSeconds() * 1000L); + if (resumed) + handler.postDelayed(rotate, Prefs.backgroundSeconds() * 1000L); } private void message(String s) { |
