Notes on learning stage of developing my own React Native education app using Expo Framework - Part 2

Last updated on 23 Aug 2025

Main tutorial used for learning: Simon Grimm: Build Your Own MASTERCLASS clone in React Native, https://www.youtube.com/watch?v=fO3D8lNs10c , 4 hrs. 42 mins, Mar. 2025.

Quick-Info

Steps for reliable and reproducible build from working React Native and Expo project source files
  1. Use working package.json and package-lock.json in the build. This will ensure that same package versions are used in current build as in working version.
  2. Delete node_modules, .expo and android directories, if present.
  3. Run: npm ci
  4. Run: npx expo prebuild --clean
  5. Run: npx expo run:android 
The above should give a successful build with APK as in previous successful build with working version of project source files.

If step 5 fails and the reason seems to be that current Gradle and AGP (Android Gradle Plugin) versions used by step 5 are not working with the source code and artifacts produced by earlier steps, then check if the Gradle and AGP versions used in the last successful build have been noted in the project source files (e.g. in AndroidBuildConfig folder). If so, then ideally after step 4 or after failed step 5, the following files should be modified as indicated by the notes of the last successful build :
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/build.gradle
The above modifications should lock the Gradle version used in step 5 to be same as in last successful build and lock or narrow the range of the AGP version used in step 5 to be same as/close to that of last successful build.
Then step 5 can be run again. I think it may not be necessary to restart from step 1 but if step 5 fails again, then perhaps that could be tried.

========== End Quick-Info =================

Summary of SG project study as of 19 Aug 2025

Addl. Ref: info.txt in top-level folder for project.

1) https://github.com/Galaxies-dev/lms-react-native is the repo for the front-end (lms-react-native). That did not successfully build. So I am using that repo related project: build-failed-lms-react-native, only as a reference repo for source code (including copying source code from it to the front-end repo I am using) - See "21 & 22 Apr. 2025" entry in Main Ref.

2) So I created a new front-end project: lms, using npx create-expo lms. The starter project got built and ran successfully. [See "21 & 22 Apr. 2025" entry in Main Ref.] I coded along with the video in this project. At times, I faced issues not seen in the video and had to figure out fixes for the same. To reduce git repo size, I recreated git repo and excluded images (assets) from it. Now lms is the active frontend and is working as expected at this stage of the code-along.

3) https://github.com/Galaxies-dev/lms-api is the repo for the back-end (lms-api). I was able to build this project though it needed audit fix --force and .env file setup. Initial successful version was in folder SG-lms-api. Then to reduce git repo size, I created Ravi-lms-api with a new git repo which excluded 15 MB database that SG-lms-api had. Now Ravi-lms-api is the active backend and is working as expected. See "22 & 23 Apr. 2025" and "23 Apr." entries in Main Ref.

======== end Summary of SG project study as of 19 Aug 2025 ==========


Entries below are in chronological order
=====================
Possibly on 4th or 5th of July 2025

I had started viewing the above video perhaps in April 2025 but then felt I need to get some exposure to React Native and Expo. So I migrated an earlier Dave Gray tutorial CrudApp which IIRC used only React Native, to React Native and Expo. That work is covered in the above post and that was done from 18 Apr (2025) to around 21/22 Apr
Key periods reflected in above post dates:
Phase 1: 18 Apr (2025): Beginning tutorial but switching to Dave Gray app work mentioned above quite quickly. Probably just a day or so of this activity.
Phase 2: 21/22 Apr to 25 Apr. This was followed by a break of around 3 weeks.
Phase 3: 15 May to 24 May
-----------

Now I am restarting the work after another break. That will make it:
Phase 4: 4th or 5th Jul to -
----------------------

5 Jul 2025
Was able to run SG lms app on DPC on both web and Android emulator.
Frontend folder: SG-EduApp\lms
Backend folder: SG-EduApp\Ravi-lms-api
[At some point around 19 May 2025, I renamed SG-lms-api folder to Ravi-lms-api and modified its .gitignore to ignore database and image files and thus reduce the .git folder size. info.txt file in SG-EduApp has details.]
I had to check ip address and set in .env file in frontend:
EXPO_PUBLIC_STRAPI_API_URL=http://{ip-addr}:1337

The Android app needed two or three retries but then it came up properly.
Both web and android apps remembered my Google logins properly.

I think now the app on my desktop PC (DPC) is roughly at this point in the video: https://youtu.be/fO3D8lNs10c?t=6499 .
============
============

20:19 08 August 2025

Restarting study and code-along of SG lms app on DPC after a break.

The app top-level folder has got renamed (NIBSProj is the parent folder now whereas earlier it was NotInBackupScript). That may break current frontend build and may need a rebuild.
Frontend folder: lms
Backend folder: Ravi-lms-api

Was able to run backend (in Ravi-lms-api), with: npm run develop
Strapi admin (http://localhost:1337/admin) userid: riyer02@gmail.com, password: ... worked straight away and I was shown last edited and last published database entries.
I did not have to do steps like clean node_modules and reinstall it.

Tried out front-end (lms) with:  npm run start (without any clean and rebuild).
Opened web app at http://localhost:8081/ , showed login, was able to login with Google id. But am not being shown data in My Studio and Browse courses.

Changed ip address in lms .env to current pc ip address:
EXPO_PUBLIC_STRAPI_API_URL=http://192.168.137.1:1337

Now it shows the backend data (image and text in My Studio, and two courses in Browse Courses).
Hmm. So web app is not impacted by parent folder rename.

Opened Expo development build Android app using Scan code of phone (not in Expo Go). Was able to login but am not being shown data in My Studio and Browse courses.
Saw this error on VSCode terminal:
DOM Bundling failed 413ms node_modules\expo\dom\entry.js (1 module)
Unable to resolve "./../../../../../../NotInBackupScript/SG-EduApp/lms/components/HomeBlock.tsx" from "node_modules\expo\dom\entry.js"
-----

This is due to folder rename of NotInBackupScript to NIBSProj.

Tried: npm start -- --clear (Based on Gem input)
Still got the same error.
Deleted .expo folder - got same error

This indicated that I have to do a clean rebuild of the app as my project folder rename had broken the earlier app build. I sought Gemini's advice to do so which included deleting package-lock.json which was a mistake as that resulted in newer versions of some packages being pulled in which needed 'expo-module-gradle-plugin' which in turn resulted in build errors (Plugin [id: 'expo-module-gradle-plugin'] was not found).

Trouble-shooting that took a lot of time but there was some good learning as I understood better how to investigate such build errors. These notes are not critical to learning about this SG Edu app and so I have moved them to sub-section: 'Troubleshooting build errors due to package-lock.json delete in clean rebuild' in section: 'Non-critical Notes' at bottom of post.

Some key points from the moved section relevant to this part of the post are:
  1. Doing clean rebuild using old package-lock.json and old package.json corresponding to commit in repo for last successful build worked without any issues.
  2. While above clean rebuild worked with a later version of Gradle (and possibly later version of AGP as well), it may be useful to record the version of Gradle and either exact version of AGP  or narrow range of AGP version based on specific versions of other tools used for the build.
  3. The procedure related to above two points are captured in "Steps for reliable and reproducible build from working React Native and Expo project source files" in Quick-Info section at the top of this post.
  4. For Android app, the older dev build app has to be uninstalled and the new dev build app created by above clean rebuild has to be installed. 
  5. Clean rebuild of app did not clean Metro cache as that is stored outside the project folder, and so I got some error related to old project folder name. Solution was to clean Metro cache by running: npx expo start --clear (I think npm start --clear will also do the job based on package.json script in project).
=================
16:39 19 August 2025

PC hotspot ip addr changed and so updated .env in lms.
Now Ravi-lmi-api server is running and its admin page is showing data correctly.
Web lms is running and showing screens and data correctly.
Android emulator lms is running and showing screens and data correctly.
Android phone (Samsung M21) lms is running and showing screens and data correctly.

https://youtu.be/fO3D8lNs10c?t=2453 - I don't seem to have setup a util/cache.ts
But I use: import { tokenCache } from '@clerk/clerk-expo/token-cache' (in app\layout.tsx)
The key maybe: "But now there is a lot of difference between SG video and Clerk Expo doc. Will need to try out stuff." ... "Now I will try out a hybrid of Clerk documentation and SG-lms app minimal code to step-by-step add the functionality that the SG-video adds." from "25 Apr. 2025" entry in https://raviswdev.blogspot.com/2025/04/notes-on-learning-stage-of-developing.html ... Yes, the tokenCache part I have used above seems to be from https://clerk.com/docs/quickstarts/expo.

To check later on: In handleSignInWithSSO() in app\login.tsx, does createuser() of StrapiContextType return error if user exists? I think that depends on ${baseUrl}/api/auth/local/register implementation. I guess that it may not as otherwise handleSignInWithSSO() will report error every time a previously signed up user, logs in again, IINM (if I am not mistaken).

==========================
Introduction to Expo Router Layout Files, https://www.youtube.com/watch?v=Yh6Qlg2CYwQ , by Expo, around 13 mins, Apr. 2025.
Using a Stack Navigator with Expo Router, https://www.youtube.com/watch?v=izZv6a99Roo , by Expo, around 12 mins, Apr. 2025.
Using a Tab Navigator with Expo Router, https://www.youtube.com/watch?v=BElPB4Ai3j0 , by Expo, around 16 mins, Apr. 2025.
Using Modals with Expo Router, https://www.youtube.com/watch?v=gNzuJVRmyDk , by Expo, around 10 mins, Apr. 2025.


=====================
From Google Search AI:

The flex-1 class in Tailwind CSS is a utility class used within a flex container to control the sizing and behavior of individual flex items. When applied to a flex item, flex-1 sets the flex CSS property to 1 1 0%. This shorthand value dictates the following: 
flex-grow: 1: The item is allowed to grow and fill available space within the flex container. If multiple items have flex-grow: 1, they will share the available space equally.
flex-shrink: 1: The item is allowed to shrink if necessary to prevent overflow, even if it means shrinking below its initial content size.
flex-basis: 0%: The initial size of the item is considered to be 0% before any growing or shrinking occurs. This means the item's content size is ignored when calculating the initial distribution of space, and the item will primarily rely on the flex-grow and flex-shrink properties to determine its final size.
In essence, flex-1 makes a flex item highly flexible, allowing it to expand to fill available space and shrink when needed, effectively ignoring its initial content size in the process. This is particularly useful for creating responsive layouts where elements need to adapt dynamically to different screen sizes and available space.
-----------

======================

21 August 2025


Note that at some stage, weeks ago, I seem to have copied many source files from the SG lms repo folder on my PC, into the live lms code-along project. IIRC, I had also commented out some code in some files as that was creating some issues.

In getCourses() in providers\StrapiProvider.tsx, made the image fix as follows:

      result.data = result.data.map((item: any) => {
        const image = (__DEV__)
        ? `${baseUrl}${item.image.url}`  // In Development
        : `${item.image.url}`
        return {
          ...item,
          image,
        }
      });
-----

Now images are shown in Browse Courses page on web and Android phone.
Commit: image fix for getCourses
--------

In the video - https://youtu.be/fO3D8lNs10c?t=7705 - finished percentage and progress bar are shown in each Course card. But that seems to be due to code being intermeditate. In the video at above TS, the check for hasCourse has not yet been added to CourseCard.tsx and so progress bar is displayed in the card.

At https://youtu.be/fO3D8lNs10c?t=7925 he introduces the hasCourse check after which the progress bar is not shown (when user has not taken the course - we are not into that part yet).

----

Added VSCode bookmark extension based on Google Search AI suggestion:
Visual Studio Code (VS Code) does not include a built-in bookmarking feature. However, bookmark functionality can be added and managed through the use of extensions available in the VS Code Marketplace.
...
e.g., "Bookmarks" by alefragnani [Installed the same extension.]
...
Ctrl+Alt+K [Add bookmark]
...
.. jump to the next or previous bookmark (e.g., Ctrl+Alt+L and Ctrl+Alt+J for the "Bookmarks" extension)

---------


==============

13:25 22 August 2025

Just after adding course folder in app source folder: https://youtu.be/fO3D8lNs10c?t=8076

Revenue Cat related stuff starts here: https://youtu.be/fO3D8lNs10c?t=10412 (2 hrs. 53 min)

------------------

Copied course folder of SG lms app repo to my code-along lms app project. Also copied RichtTextContent.tsx of Components folder. Also copied yoga.png in assets/images.

Now I need to comment out/modify code to keep it in sync with video lms app as at: https://youtu.be/fO3D8lNs10c?t=10412 (2 hrs. 53 min) .
Liberally commented out code in the new files that were added which were showing errors.
Got the code to show, on web, most of each course's content but the image is not shown.
Commit: On web, course data shown but without image

Fixed getCourse() in StrapiProvider.tsx as follows:
      const result = await response.json();

      const image = (__DEV__)
       ? `${baseUrl}${result.data[0].image.url}`  // In Development
       : `${result.data[0].image.url}`

      result.data[0] = {
        ...result.data[0],
        image,
        // image: `${result.data[0].image.url}`,
      };
---
Now the image is shown but is vertically cropped quite badly when windows is full screen but that issue is there even for Home page image. On reducing window width more of the image is shown. 

Noted from the video that I need to uncomment some lines in app\(app)\(authenticated)\_layout.tsx
      <Stack.Screen
        name="course/[slug]/index"
...
----
Did that. But that seems to impact only Android app as there is an _layout.web.tsx for web (which does not have this Stack stuff).
At https://youtu.be/fO3D8lNs10c?t=8387 he starts covering the web app for Course page.

https://youtu.be/fO3D8lNs10c?t=9053 shows that SG app also faces the vertical cropping of image for web issue.
So at this stage, my web app seems to be in sync with above TS stage in SG video.
I will now check the Android app.

On Android emulator app, a black screen is shown on going to course page.

At 2:34:20 in the video, in index.tsx for course, in lessons list section, the code shows an additional style of 'bg-white dark:bg-black' which is not there in the SG repo code.
That did not fix the issue. I also checked on my phone app - same issue and also change in mode to light did not make a difference.

After trials for perhaps 2 hours if not more, I found that removing flex-1 from the Animated.ScrollView fixed the problem. As per Gemini, this is a known issue with Android. 

Gem: "The blank display on Android when using flex-1 with ScrollView is a known behavior related to how React Native handles layout and sizing within ScrollView. The ScrollView component doesn't inherently understand how to size itself based on a flex value like flex-1 because it's designed to contain content of unknown or variable height." 
..
"This behavior is specific to Android because iOS handles layout and sizing within ScrollView differently, often defaulting to a more flexible and forgiving calculation that prevents the zero-height issue."
..
"The most reliable and cross-platform way to handle this is to avoid applying flex properties directly to the ScrollView or Animated.ScrollView component.

Instead, the ScrollView should size itself based on the content it holds. By simply removing flex-1, you are letting the component revert to this default behavior, allowing the content to render and the scroll functionality to work correctly.

Your solution of simply removing flex-1 from Animated.ScrollView is the standard and most reliable fix for this specific problem on Android. It highlights a key difference in how React Native's layout engine works on the two platforms."
---

Now the issue is that Android app does not show some of the text content of the course in dark mode (in light mode, it shows it).
Before I get into that issue, I decided to commit the current stage.
Commit: Course page: Fixed image display on web; Fixed blank screen on Android; Dark mode issue on Android remains

The dark mode issue seems to be with RichtTextContent (which is a DOM component or whatever that is called - has a 'use dom'; statement in top of file). It seems to be similar to the dark mode issue I faced with HomeBlock which I resolved by passing the colorScheme as a prop to HomeBlock from its caller - app\(app)\(authenticated)\(tabs)\index.tsx.

Adopted a similar approach for RichtTextContent but as RichtTextContent is used by some other pages which I have not studied yet, I made it an optional prop with a default value of 'light'. The code now works on Android phone. I changed only app\(app)\(authenticated)\course\[slug]\index.tsx and not app\(app)\(authenticated)\course\[slug]\index.web.tsx as I incorrectly thought the web app continues to work properly for light and dark mode (for course page). I think in my web app tests, I did not clear some cache due to which it worked but at a later stage, I faced an issue which is noted below.

Commit: Course page: Fixed dark mode issue on Android

I checked that the Android app Course page dark/light mode works on emulator too.

Around 2:48:44 SG covers using api\add-user-course+api.ts . I copied that file into lms project.
I already had a full access key but its token value was not shown to me (shown only once). However, I have an option to regenerate it. Regenerated it and copied token value to .env file in lms (STRAPI_API_ADMIN_TOKEN).

At 2:50:01, app.json -> "web"."output" is set to "server". On my lms app (created from fresh app), it is set to "static". But SG repo has it as "server". Changed it to "server" on lms app.json. [I presume this should require at least expo prebuild + app build steps to create new dev APK.]

At 2:52:28, SG shows app.json with an "origin" entry under plugins which he says was required to get the above add-user-course+api.ts code working such that choosing 'Start Course' resulted in addition of entry in User-Course collection type. He said the "origin" entry he added is a fake one and that it is usually not required. As of now, I have not added a 'fake' origin entry to app.json. If my Android app test fails then I may consider adding it.

I think now my code is close to be in sync (but for above api stuff) with Revenue Cat starting TS: https://youtu.be/fO3D8lNs10c?t=10412 (2 hrs. 53 min).
-----

==============
23 August 2025

Noted that web app dark mode does not show course contents properly. Made similar colorScheme change in app\(app)\(authenticated)\course\[slug]\index.web.tsx as in app\(app)\(authenticated)\course\[slug]\index.tsx. That fixed the issue.
Commit: Fixed dark mode issue in Web app Course page; app.json change for api

Now tried to start the free course on web app. Got a 404 error for the api being called on front-end 8081.
As app.json had been changed, restarted expo server with: - npx expo start --clear
Got same error again on web app. The console error message:
Failed to load resource: the server responded with a status of 404 (Not Found)  :8081/api/add-user-course:1 
Call Stack
addUserToCourse
providers/StrapiProvider.tsx
onStartCourse
app/(app)/(authenticated)/course/[slug]/index.web.tsx
-----

Tried with hard refresh (Ctrl + F5) ... error message changed slightly:
http://localhost:8081/node_modules/expo-router/entry.bundle?platform=web&dev=true&hot=false&transform.routerRoot=app
 POST 
 http://localhost:8081/api/add-user-course 404 (Not Found)
-----

The issue was that I had created the api directory at project root. It should have been within app directory. It took some time to figure out the error. Initially I tried Gemini but it did not spot the issue and suggested some other stuff which did not work.

In the process, I did the following multiple times:
npx expo prebuild --clean
npx expo start --clear

Eventually, reading Expo router API docs showed me that the api route has to be within app folder. [I later noted that SG lms repo also had the api route within app folder. So I seem to have made a mistake while copying that content into my lms.] Did that and then tested web app 'Start Course' for the free 'Beginner's Yoga Journey - Flow Into Balance'. That worked. A new entry was created in STRAPI backend for User-Course collection type.

A small issue is that the page shown after 'Start Course' seemed to have a dark mode issue as it seemed that some image and text were not shown. 

Commit: Start Course API works for web
========================

To get history of PS commands as saved (not limited to current session): 
Get-Content (Get-PSReadLineOption).HistorySavePath

To get lines matching 'expo' in above list:
Get-Content (Get-PSReadLineOption).HistorySavePath | Where-Object { $_ -like "*expo*" }
----

========================

As per Gem: 'npx expo-router-sitemap' lists only publicly accessible pages (routes that render a view) and does not list API routes.
-----

16:51 23 August 2025

To now test the Android app for api. Creating new dev APK with:
npm run android

It has started building app. It did not download a new gradle version. It did report AGP and Gradle versions in:" > Configure project :react-native-reanimated:" as:
Android gradle plugin: 8.6.0
Gradle: 8.10.2
----

lms> npm run android

> lms@1.0.0 android
> expo run:android

env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY STRAPI_API_ADMIN_TOKEN
› Opening emulator Medium_Phone_API_35
› Building app...
Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details
Configuration on demand is an incubating feature.

> Configure project :app
 ??  Applying gradle plugin 'expo-dev-launcher-gradle-plugin' (expo-dev-launcher@5.0.35)

> Configure project :expo

Using expo modules
  - expo-application (6.0.2)
  - expo-asset (11.0.5)
  - expo-blur (14.0.3)
  - expo-constants (17.0.8)
  - expo-crypto (14.0.2)
  - expo-dev-client (5.0.20)
  - expo-dev-launcher (5.0.35)
  - expo-dev-menu (6.0.25)
  - expo-file-system (18.0.12)
  - expo-font (13.0.4)
  - expo-haptics (14.0.1)
  - expo-json-utils (0.14.0)
  - expo-keep-awake (14.0.3)
  - expo-linking (7.0.5)
  - expo-manifests (0.15.8)
  - expo-modules-core (2.2.3)
  - expo-secure-store (14.0.1)
  - expo-splash-screen (0.29.24)
  - expo-system-ui (4.0.9)
  - expo-web-browser (14.0.2)


> Configure project :react-native-reanimated
Android gradle plugin: 8.6.0
Gradle: 8.10.2
<======-------> 48% EXECUTING [1m 19s]
> :expo-manifests:compileDebugKotlin > Unpacking build cache entry
> IDLE
----------------------
...
BUILD SUCCESSFUL in 6m 12s

==========
The 'Start Course' button does not seem to initiate any API call to backend. Does it need the 'fake' origin in app.json as SG has done?
Added some console log statements to code. Its output:
 (NOBRIDGE) LOG  Entered onStartCourse
 (NOBRIDGE) LOG  Entered addUserToCourse with courseId: caidhct21sy7lhabvqhauho9
 (NOBRIDGE) LOG  In addUserToCourse before fetch call to /api/add-user-course with body: {"clerkId": "user_2xM5iCisxx9uJQ3XjDJM83T0oLz", "courseId": "caidhct21sy7lhabvqhauho9"}
 (NOBRIDGE) LOG  In addUserToCourse error handler with error: [TypeError: Network request failed]
-----

So addUserToCourse() in providers\StrapiProvider.tsx has the error. Its fetch call (to the api) fails.
I had added an entered console log statement in the api. It is not printed. So the fetch call to the api does not even seem to invoke it.

I asked Gemini about the above.

Gem: Your network request is failing on Android because fetch in a native app context doesn't automatically know how to resolve the relative URL /api/add-user-course. The Network request failed error is the generic error message for this type of issue.

The Problem

On the web, the browser handles relative URLs by prepending the window.location.origin (e.g., http://localhost:8081). However, in a React Native app running on a device or emulator, there is no browser and thus no window.location.origin. The app doesn't know where /api/add-user-course is, so the request fails.

The Solution

You need to use a full, absolute URL for the fetch call, including the server's IP address or hostname and port.

...

[Example code]

const response = await fetch(`http://192.168.1.5:8081/api/add-user-course`, {

  // ... rest of your code

});

Getting More Detailed Errors

The minimal error output you're seeing is a limitation of the native development environment. Unlike a web browser that provides a detailed console, native logs often just report the low-level TypeError for a failed network request. The Network request failed message comes from the native fetch implementation, and it's intentionally non-specific for security and performance reasons.

-----

Me: In the tutorial, where the author is running the app on an iOS emulator he did not change the code to have absolute URL with ip but he added a 'fake' origin in the plugins in app.json.

...

Gem: The fact that you are also using Expo Dev Client completely changes things and explains why the tutorial's code worked and why we've been running in circles.

How Expo Dev Client Works

The expo-dev-client is designed to solve exactly this kind of networking problem. When you build and install an app using expo-dev-client, it is specifically created to be a direct link to your local development server. It bypasses many of the standard networking limitations of a regular app.

The expo-dev-client automatically knows the full URL of your local development server (e.g., http://192.168.1.5:8081). Because of this, when your app tries to fetch a relative URL like /api/add-user-course, the dev client automatically prepends your local dev server's URL to the request, turning it into a valid, absolute URL. This allows your API calls to work seamlessly.

My advice to use a hard-coded IP address was a general-purpose solution that would work on any device, but it's completely unnecessary when you are using expo-dev-client as the client handles it for you.

Me: But I still seem to need a fake origin in app.json as otherwise it is not working for Android app (but works for web app).

Gem: I gave you an oversimplified explanation based on an ideal scenario, but your experience proves that the origin in app.json is still necessary for your specific dev client setup.

The "fake origin" is not just for production. It is a fallback base URL for fetch requests in the dev client. While expo-dev-client is designed to handle relative URLs automatically, a TypeError: Network request failed on Android confirms that this automatic behavior is failing in your project.

Why It's Needed

Even with expo-dev-client, the origin property in app.json provides a clear, defined base URL that the native fetch implementation can reliably use to resolve relative paths. It's a way of telling the app, "If you don't know where a request is going, use this as the starting point."

The fact that it's working for your web app but not Android points to a subtle networking or configuration difference between the two platforms. Using the origin property in your app.json is the correct and standard way to resolve this specific cross-platform issue in a dev environment.

-----------

Modified app.json entry:

    "plugins": [
      [
        "expo-router",
        {
          "origin": "https://fake-devonly-lms.expo.app"
        }
      ],
      [
        "expo-splash-screen",
...
----

Me: I guess I can try without --clean option for npx expo prebuild.
Gem: Yes, you can try without the --clean option.

For a simple configuration change in app.json, running npx expo prebuild on its own is typically sufficient. It will merge your new plugin configuration into the existing native project files, which is faster than a full rebuild.

The --clean option is usually reserved for when you're making major changes or if you run into persistent build errors that can be solved by a complete refresh of the native project folders.
----
Ran:
npx expo prebuild
npm run android
----
...
BUILD SUCCESSFUL in 24s
---

I think the quick build above was due to only configuration change. No file compilation messages were shown.

Gem: That's a very sharp observation, and you're exactly right. The quick build time is because only configuration files were changed, not the app's source code.

Build systems like Gradle for Android use a process called caching and incremental builds. They are designed to be smart. When you run npm run android, the system first checks to see if any source code files have been modified. Since your code was unchanged and only the native configuration was updated, it was able to reuse the previously compiled code.

This process significantly reduces build times, which is why it only took 24 seconds to package the app. It's a sign that your build environment is working efficiently.


Me: If I had used: npx expo prebuild --clean
then 'npm run android' would have had to all the compilation stuff again as above command would have deleted the previously compiled outputs. 

Gem: Yes, that's exactly right.

If you had used npx expo prebuild --clean, the command would have completely deleted the android folder, including all of its compiled outputs and caches. When you then ran npm run android, the build system would have had to perform a full, clean build from scratch, compiling every file again.

This is why a clean build is always significantly slower than an incremental one, which is what you had with your quick 24-second build.
--------

Me: Now the Android app code invokes the api, and works as expected (like the web app). Hmm. Interesting to know about this fake origin + expo-dev-client combo providing this fix for dev env.

Gem: You've perfectly summarized the solution. The combination of expo-dev-client and a defined origin in app.json is a powerful and standard way to get a robust development environment for API calls. The dev client acts as a local proxy, and the origin provides a reliable base URL, which is often needed for consistency across different platforms and their native networking implementations.

Me: But please note that the origin (          "origin": "https://fake-devonly-lms.expo.app") is a fake one. I mean, there is no such domain as fake-devonly-lms.expo.app.

But presence of this fake origin gets the code to work (in dev env). Absence of fake origin leads to api failure for Android

Gem: That's the entire paradox of this particular Expo behavior, and you've described it perfectly.

The origin is a "fake" one because your app isn't actually making API calls to that domain in development. Instead, the presence of that URL serves as a placeholder for the native networking system.

Here's why it works:

When your app runs on Android, the fetch implementation needs a complete, absolute URL to make a network request. It doesn't inherently understand what a relative path like /api/add-user-course means. By providing a base origin in app.json, you are giving it a defined starting point.

The Expo dev client then steps in. It sees that you are in a development environment and intercepts the request that would have gone to the "fake" origin. It intelligently reroutes the request to your actual local development server (http://192.168.1.5:8081). This gives your native app a valid URL to work with, while still allowing you to write clean, relative API paths in your code.

==========
Commit: Used fake origin in app.json to get start course API working for Android

Tested Android app on phone for start course API (deleted previous user-course entry). It worked as expected.
------

The issue of page (my-content.tsx) not being shown properly after 'start course' goes through was related to an image problem and not dark/light mode. The fix for that was to modify getUserCourses() in providers\StrapiProvider.tsx as follows:
      const result = await response.json();
      result.data.forEach((entry: any) => {
        entry.course.image = (__DEV__)
        ? `${baseUrl}${entry.course.image.url}`  // In Development
        : `${entry.course.image.url}`;
        // entry.course.image = `${entry.course.image.url}`;
      });
----

Tested on Android - it worked on phone. I presume it will work on emulator as well as on web.
Commit: Fixed image display in my-content
==============

============================
============================

Non-Critical Notes

Troubleshooting build errors due to package-lock.json delete in clean rebuild

08 August 2025

So will now do clean rebuild:
1) Delete node_modules and package-lock.json
2) Delete .expo
3) npm install
4) npm start -- --clear

First npm install gave some error and said it could be due to network connectivity (It was done slightly after 9 PM on Jio 4G).
After node_modules delete again (0 files but some folders), npm install again succeeded. It said:
"added 1256 packages, and audited 1257 packages in 3m" .. "found 0 vulnerabilities".

Ran: npm start -- --clear
Now the DOM bundling error is gone. Also noted that it took quite some time for the step: Android Bundled 35690ms node_modules\expo-router\entry.js (1602 modules)
Looks like it recreated node_modules\expo-router\entry.js which was the source of the DOM bundling error earlier on.
But now there is another error:
 (NOBRIDGE) ERROR  ReanimatedError: [Reanimated] Native part of Reanimated doesn't seem to be initialized (Worklets).
See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#native-part-of-reanimated-doesnt-seem-to-be-initialized for more details. [Component Stack]    
 (NOBRIDGE) WARN  Route "./(app)/(authenticated)/(tabs)/my-content.tsx" is missing the required default export. Ensure a React component is exported as default. [Component Stack]
 (NOBRIDGE) ERROR  ReanimatedError: [Reanimated] Native part of Reanimated doesn't seem to be initialized (Worklets).
See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#native-part-of-reanimated-doesnt-seem-to-be-initialized for more details. [Component Stack]  
---

The app opens on phone via dev build Expo. But it does not show home page contents and no courses.
Stopped and ran: Ran: npm start -- --clear
Checked the web app. That seems to work fine. Home page data is shown as well as two courses.

Tried something that Gem suggested wrt babel.config.js that did not work. Then expo prebuild aspect came up.
Restored babel.config.js to earlier contents.

So will now do clean rebuild:
1) Delete node_modules and package-lock.json
2) Delete .expo
3) npx expo prebuild --clean
4) npm install
5) npm start -- --clear
 
To do the above 22:06 08 August 2025

[Probably on 9 Aug. 2025]

I had forgotten about expo prebuild due to break for some time from React native! Now I will go through the earlier notes on SG edu app checking for install steps. If required, I can look at Launch Timestamp app too.

As per first notes post, npx expo prebuild has to be run after npm install. I think Gemini was suggesting before npm install. .... I could try running expo prebuild in the project as it is now without deleting and reinstalling node_modules.
Related extract from post:
Ran npm install. It gave some high vulnerabilities. Ran npm audit fix and now there are 0 vulnerabilities.
As per video https://youtu.be/fO3D8lNs10c?t=871 , I am next running:
npx expo prebuild
That succeeded.
Next:
npx expo run:android
(SG does it on Mac and uses run:ios)
--- end extract ---
====================
====================

16:24 12 August 2025

Tried: npx expo prebuild --clean
followed by: npm start -- --clear

Once again, faced same problem on phone (ReanimatedError)

It just struck me that the earlier reanimated error I was getting when running the dev build on phone was that phone lms app is an old debug-apk, IIRC. I need to install a debug-apk created from this project (which needs an Android app build).

Hmm.

Now, as advised by Gemini, I will go with the complete clean steps of:
1) Delete node_modules, package-lock.json, and the .expo folder.
2) Delete the native project folders: android and ios.
3) Run npx expo prebuild --clean.
4) Run npm install.
5) Run npx expo run:android.


For step 3, got this error:
lms> npx expo prebuild --clean
Need to install the following packages:
expo@53.0.20
Ok to proceed? (y) y

npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
! Git branch has uncommited file changes
› It's recommended to commit all changes before proceeding in case you want to revert generated changes.

√ Continue with uncommited changes? ... yes

√ Cleared android code
ConfigError: Cannot determine the project's Expo SDK version because the module `expo` is not installed. Install it with `npm install expo` and try again.
---------

So changing complete clean order to:
1) Delete node_modules, package-lock.json, and the .expo folder.
2) Delete the native project folders: android (and ios, if present).
3) Run npm install
4) Run npx expo prebuild --clean
5) Run npx expo run:android

Did Steps 1 and 2
Step 3 succeeded with message:
added 1256 packages, and audited 1257 packages in 3m

107 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
----

Step 4 succeeded. Console:
lms> npx expo prebuild --clean
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
! Git branch has uncommited file changes
› It's recommended to commit all changes before proceeding in case you want to revert generated changes.

√ Continue with uncommited changes? ... yes

√ Cleared android code
√ Created native directory
√ Updated package.json | no changes
√ Finished prebuild
----

The uncommitted change was related to package-lock.json deletion. I presume its OK to say yes to "Continue with uncommited changes?" in step 4.
Gemini confirmed that its OK to say Yes in above case.

Step 5 straight away jumped to initialization and configuration. (Downloading gradle stuff was not done again).
Got this error:
lms> npx expo run:android
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
› Opening emulator Medium_Phone_API_35
› Building app...
Configuration on demand is an incubating feature.

> Configure project :app
 ??  Applying gradle plugin 'expo-dev-launcher-gradle-plugin' (expo-dev-launcher@5.0.35)

FAILURE: Build failed with an exception.

* Where:
Build file 'C:\Users\ravi-user\NIBSProj\SG-EduApp\lms\node_modules\expo-application\android\build.gradle' line: 3

* What went wrong:
Plugin [id: 'expo-module-gradle-plugin'] was not found in any of the following sources:

- Gradle Core Plugins (not a core plugin. For more available plugins, please refer to https://docs.gradle.org/8.10.2/userguide/plugin_reference.html in the Gradle documentation.)
- Included Builds (None of the included builds contain this plugin)
- Plugin Repositories (plugin dependency must include a version number for this source)
==========

Then tried various stuff like:
1) npx expo install --fix

2) As per https://stackoverflow.com/questions/79601560/building-expo-managed-app-in-production-profile-getting-this-error :
My problem were 2 packages: expo-device and expo-secure-store. In package json I had versions "expo-device": "^7.0.2" and "expo-secure-store": "^14.0.1". Looking at the yarn.lock file I noticed it installed versions "expo-device": "7.1.2" and "expo-secure-store": "14.2.1". These new version of modules had gradle-plugin reference in build.gradle. I then removed "^" from package.json versions and did yarn install and then yarn.lock contained exact versions mentioned in package.json. This helped me run the project. I guess expo will publish expo-module-gradle-plugin in core module with version 53.
---------

So I removed all ^ in my package.json and retried.

None of them worked.

BTW Gemini had suggested making some changes to some gradle files but I did not want to do that at this stage.
...
The point is: what is leading to this change where the build seeks expo-module-gradle-plugin . I want to be able to trace that as that will give me more confidence in using expo for production apps.

How can I find which packages in my project are resulting in the build looking for expo-module-gradle-plugin
...
lms> npm list expo-modules-core
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
└─┬ expo@52.0.47
  └── expo-modules-core@2.2.3
...
npm list expo-module-gradle-plugin
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
└── (empty)
So how do I trace what package is trying to pull in expo-module-gradle-plugin
...
findstr /s /i "expo-module-gradle-plugin" .\node_modules\*.gradle
.\node_modules\expo-application\android\build.gradle:  id 'expo-module-gradle-plugin'
.\node_modules\expo-auth-session\node_modules\expo-constants\android\build.gradle:  id 'expo-module-gradle-plugin'
.\node_modules\expo-auth-session\node_modules\expo-crypto\android\build.gradle:  id 'expo-module-gradle-plugin'
.\node_modules\expo-auth-session\node_modules\expo-linking\android\build.gradle:  id 'expo-module-gradle-plugin'
.\node_modules\expo-auth-session\node_modules\expo-web-browser\android\build.gradle:  id 'expo-module-gradle-plugin'
...
Gem: The output confirms that the expo-module-gradle-plugin is being requested by the build.gradle files of multiple packages:
  • expo-application

  • expo-auth-session and its nested dependencies like expo-constantsexpo-cryptoexpo-linking, and expo-web-browser.

...
What beats me, is how did the same project get built successfully earlier? I did not make any changes like you have suggested now. That's what I want to know to build my confidence in expo app building process.
...
...\SG-EduApp\lms> npm list expo-auth-session
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
└─┬ @clerk/clerk-expo@2.10.0
  └── expo-auth-session@6.2.1
----
The above tells us that it is clerk-expo package that pulls in expo-auth-session.
...
npm list expo-constants
lms@1.0.0 {...}\SG-EduApp\lms
├─┬ @clerk/clerk-expo@2.10.0
│ └─┬ expo-auth-session@6.2.1
│   ├── expo-constants@17.1.7
│   └─┬ expo-linking@7.1.7
│     └── expo-constants@17.1.7 deduped
├── expo-constants@14.5.1
├─┬ expo-linking@7.0.5
│ └── expo-constants@17.0.8
├─┬ expo-router@4.0.21
│ └── expo-constants@14.5.1 deduped invalid: "~17.0.8" from node_modules/expo-router
└─┬ expo@52.0.46
  ├─┬ expo-asset@11.0.5
  │ └── expo-constants@17.0.8
  └── expo-constants@17.0.8
----
Above shows us that expo-constants is not only used directly in my app but is also pulled in by other packages. Further each other package that pulls in expo-constants may pull in a specific version of it.
...
I think I am getting close to the cause of the problem:
node_modules\@clerk\clerk-expo\package.json has:
  "devDependencies": {
    "@types/base-64": "^1.0.2",
    "expo-auth-session": "^5.4.0",
    "expo-local-authentication": "^13.8.0",
    "expo-secure-store": "^12.8.1",
    "expo-web-browser": "^12.8.2",
    "react-native": "^0.73.9",
    "@clerk/expo-passkeys": "0.2.10"
  },
  "peerDependencies": {
    "@clerk/expo-passkeys": ">=0.0.6",
    "expo-auth-session": ">=5",
    "expo-local-authentication": ">=13.5.0",
...
------------------------
and following:
npm list expo-constants
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
├─┬ @clerk/clerk-expo@2.10.0
│ └─┬ expo-auth-session@6.2.1
│   ├── expo-constants@17.1.7
│   └─┬ expo-linking@7.1.7
│     └── expo-constants@17.1.7 deduped
-----
shows that expo-auth-session@6.2.1 got picked up for installation by npm install.
whereas my previously working project's package-lock.json shows:
    "node_modules/expo-auth-session": {
      "version": "6.0.3",
      "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-6.0.3.tgz",
      "integrity": "sha512-s7LmmMPiiY1NXrlcXkc4+09Hlfw9X1CpaQOCDkwfQEodG1uCYGQi/WImTnDzw5YDkWI79uC8F1mB8EIerilkDA==",
=======================
So my earlier successful build pulled in expo-auth-session 6.0.3 whose dependencies may not be pulling in later versions of packages like expo-constants which in turn need expo-module-gradle-plugin.
...
Well, I think I will try a clean rebuild but without deletion of package-lock.json (which is that of the previous successful build some weeks back) and see whether that forces older versions of packages to be pulled in and which may resolve my problem.
...
I need to remove all the ^ and ~ in my package.json to ensure that it does not bring in newer versions when I do an npm install. Or is even that not necessary as package-lock.json's presence will ensure that npm install will install the older versions.
...
The presence of your saved package-lock.json file will ensure that npm install installs the older, working versions. Therefore, you don't need to manually remove the ^ and ~ symbols from your package.json file.
...

The statement that package-lock.json is the definitive source of truth is true, but only in the context of an unchanged package.json.

Here is the correct hierarchy and process:


The Hierarchy of npm install

  1. package.json is the source of truth for the desired state. It's your blueprint. When you make a manual change to a version number or a range (^ or ~), you are changing the desired state of your project.

  2. package-lock.json is the source of truth for the current, installed state. It's a logbook of exactly what npm installed last time. Its primary job is to ensure that future installs are exactly the same, as long as the package.json file hasn't changed.

When you run npm install, the process is as follows:

  • Check package.json for changes. npm first looks at your package.json file.

  • Compare to package-lock.json. It then checks the package-lock.json file.

  • If package.json is modified: If npm detects a change (like you changing a version number), it recognizes that the desired state has changed. It will then resolve the new dependencies, download them, and update the package-lock.json file to reflect this new, desired state.

  • If package.json is not modified: npm will use the package-lock.json file to install the exact same versions from the previous install, ignoring the version ranges in package.json.

...
[After I did the above approach of clean reinstall but with old package-lock.json:]

lms> findstr /s /i "expo-module-gradle-plugin" .\node_modules\*.gradle
lms> 
...
Crucially, the findstr command shows no results, meaning expo-module-gradle-plugin was not found in your node_modules directory after the install.
...
Earlier attempt showed that expo-constants@17.1.7 was being pulled in by expo-auth-session@6.2.1
But now:
lms> findstr /s /i "expo-module-gradle-plugin" .\node_modules\*.gradle
lms> npm list expo-auth-session
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
└─┬ @clerk/clerk-expo@2.10.0
  └── expo-auth-session@6.0.3
lms> npm list expo-constants
lms@1.0.0 C:\Users\ravi-user\NIBSProj\SG-EduApp\lms
├─┬ @clerk/clerk-expo@2.10.0
│ └─┬ expo-auth-session@6.0.3
│   └── expo-constants@17.0.8
├── expo-constants@14.5.1
├─┬ expo-linking@7.0.5
│ └── expo-constants@17.0.8
├─┬ expo-router@4.0.20
│ └── expo-constants@14.5.1 deduped
└─┬ expo@52.0.46
  ├─┬ expo-asset@11.0.5
  │ └── expo-constants@17.0.8
  └── expo-constants@17.0.8
lms> 
-----
Above info. shows now (older version) expo-constants@17.0.8 is being pulled in by (older version) expo-auth-session@6.0.3
These older version packages may not be needing expo-module-gradle-plugin
So a build app may not fail on expo-module-gradle-plugin
...
[On build app attempt:] The app build has moved past that (earlier) failure point. It is still building (83% config).
...
Got this error:
> Configure project :react-native-reanimated
Android gradle plugin: 8.6.0
Gradle: 8.10.2
> Task :expo-constants:compileDebugKotlin FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':expo-constants:compileDebugKotlin'.
> Inconsistent JVM-target compatibility detected for tasks 'compileDebugJavaWithJavac' (17) and 'compileDebugKotlin' (11).
  Consider using JVM Toolchain: https://kotl.in/gradle/jvm/toolchain
  Learn more about JVM-target validation: https://kotl.in/gradle/jvm/target-validation
...
Gemini suggested a build.gradle fix which I did not try out
...
Once again, I want to understand why the build fails now but succeeded earlier. I think that here expo prebuild may hold the key. It may not have the package-lock.json kind of facility and so it would be producing different output at different times as it changes.
...
I have in package.json under dependencies:
    "expo": "52.0.46",
But I don't have any expo entry in devDependencies
...

Having "expo": "52.0.46" in your dependencies block is perfectly fine. The key is that a specific version of the expo package is being installed locally in your node_modules folder.

You do not need to add another entry to devDependencies. When you run npx expo prebuildnpx will look for the expo executable in your local node_modules folder first, find version 52.0.46, and use that. This achieves the exact same result as my previous suggestion.

Your current setup is all you need to ensure that the expo CLI version is "frozen" for your project.
...
Oh! Then is the change in build process now that is triggering the build error when it was working around two months back?
...

Yes, the change in the build process itself is what's causing the new error. This is a crucial distinction.

The issue isn't with your code or dependencies, but with the build tools that are compiling your app. The Android Gradle Plugin and Gradle have both been updated since your last successful build.


What Changed in the Build Process

The error message from your output is a perfect example of this:

Android gradle plugin: 8.6.0 Gradle: 8.10.2

These are newer versions than what you were likely using two months ago. The new versions have stricter rules and different default configurations. In your case, the newer tools are enforcing a new rule that requires the JVM version for both the Java and Kotlin compilers to be consistent. Your old project configuration, which worked before, now violates this new rule.

Essentially, the build process has evolved. Your project, which was configured to work with the old process, now needs to be updated to comply with the new one. This is why manually setting the JVM target to 17 is the correct fix—it's a change that aligns your project with the new requirements of the updated build tools.
...
Yes, Gradle: 8.10.2 seems to be surely new as it got downloaded as part of the build app process now. Hmm. So can we freeze Android gradle plugin and Gradle versions for a build of an app.
...

Yes, you absolutely can and should freeze the Android Gradle Plugin (AGP) and Gradle versions. This is a crucial step for achieving a consistent and predictable build environment, especially in a fast-moving ecosystem like React Native and Expo.

How to Freeze Gradle and AGP

Freezing these versions ensures that your build process uses the exact same tools every time, preventing unexpected failures caused by updates. You'll need to modify two files in your android directory.


1. Freeze the Gradle Version

The Gradle version is set in the gradle-wrapper.properties file. This file tells your project which version of Gradle to download and use.

  1. Open the file at android/gradle/wrapper/gradle-wrapper.properties.

  2. Find the distributionUrl line and lock it to a specific version. Based on your output, you're using Gradle 8.10.2, which is compatible with AGP 8.6.0.

# Sun Aug 11 19:40:08 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

By specifying an exact version (8.10.2 instead of a range or a different version), you ensure that Gradle will always use this specific version.


2. Freeze the Android Gradle Plugin (AGP) Version

The AGP version is set in the top-level build.gradle file. This file configures the build tools themselves.

  1. Open the file at android/build.gradle.

  2. Locate the plugins block and set the AGP version to 8.6.0.

plugins {
    id "com.android.application" version "8.6.0" apply false
    id "com.android.library" version "8.6.0" apply false
    id "com.facebook.react" version "..." apply false
}
By making these two changes, you lock your native build tools to a known, working state. This means that even if a new version of Gradle or AGP is released, your project will continue to build with the versions you have specified, giving you a stable and repeatable build process.
...
Hmm. But I don't have the backup of this file in android directory for my successful build, as typically android directory of a React Native and Expo app is part of .gitignore, isn't it?
...
.. So if I modify the gradle build config files so that it uses Gradle 8.7 instead of 8.10.2, the build may not encounter the error I am facing now. Did I get that right?
...

Yes, you've got that right. That is the most likely solution to your immediate problem.

Reverting to Gradle 8.7 should fix the error. The reason is that the stricter rules that caused the Inconsistent JVM-target error were likely introduced in a newer version of Gradle or the Android Gradle Plugin. By using the version that was current when your app was first built, you are bypassing these new rules.

However, the most robust solution is to both revert Gradle and explicitly set the JVM target. This guarantees that your project's configuration is fully compatible with the older build tools.
...
It didn't work with only gradle-wrapper.properties having:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
(I skipped the JVM step, just for knowledge sake)
Then with same gradle-wrapper.properties (8.7) I ran:
npx expo-doctor - It reported some issues.
npx expo install --check - It made some changes.
npx expo run:android - That worked.
While I could not recreate the Android app without any changes to package.json (through expo-doctor and expo install --check), I was able to do a successful build of the app.
One learning is that for React Native and expo app, if I want to have a stable version with me which is same as released to others, and then do bug fixes on the stable version (and release fix/patch versions), I need to have source code + node_modules + android folder backed up. This full backup restored on same path as the original project, would probably run straight away and match what users have. Only source code backup may not created the same output app and so the app may change quite a bit from what is released to users.
...

Reproducing the node_modules folder with package-lock.json is easy, but the android folder is a generated artifact whose content is dependent on several factors.

The key to solving this is to make the generation of the android folder a deterministic process. To do this, you need to freeze all the variables that npx expo prebuild relies on.

Here's the comprehensive strategy to ensure a fully reproducible native build:

1. Freeze Your Dependencies (package-lock.json)

This is the part you've already mastered. The package-lock.json file guarantees that npm install (or preferably npm ci) will install the same exact dependency tree every time. This is the foundation of a stable node_modules folder.


2. Freeze the expo CLI Tool

As we discussed, the expo CLI itself can change, leading to different android folder outputs. By adding a specific version of the expo package to your package.json and using npx, you ensure that the prebuild command is always run with the same logic.

3. Freeze the Native Build Tools

The android directory contains configuration files that set the versions of Gradle and the Android Gradle Plugin (AGP). The solution is to manually set these to a known-good, compatible version for your Expo SDK.

  • android/gradle/wrapper/gradle-wrapper.properties: Set the distributionUrl to a specific Gradle version (e.g., 8.7).

  • android/build.gradle: Set the AGP version (e.g., 8.6.0).

...

Here is the corrected and more precise order of operations for a reproducible native build from scratch:


The Correct Workflow for a Reproducible Build

  1. Start from a clean slate: Delete your node_modules folder and your android directory.

  2. Restore locked dependencies: Run npm ci. This ensures you have the exact dependency tree that worked for you before, as defined by package-lock.json.

  3. Generate the native project: Run npx expo prebuild. This command will use your now-stable dependency tree to generate a new android directory.

  4. Freeze the native build tool versions: After prebuild has finished, you must manually edit the two files in the android directory to lock the specific Gradle and Android Gradle Plugin (AGP) versions. This overrides any defaults that prebuild might have used.

    • android/gradle/wrapper/gradle-wrapper.properties: Set the distributionUrl to your desired Gradle version.

    • android/build.gradle: Set the plugins to your desired AGP version.

  5. Build the app: Now you can run ./gradlew assembleDebug or npx expo run:android. The build will use the frozen tool versions you just set, leading to a predictable and consistent outcome.

...
By committing a working package-lock.json to your repository and following this workflow, you can confidently reproduce your android folder and your final app build without needing to back it up.
...
=========================
=========================

19:15 13 August 2025

As Android dev build app is built, to start it and even do non-native modifications and test, I can use:
npx expo start
OR
npm run start

In my front-end package.json I have:
  "scripts": {
    "start": "expo start",
----
So if I use npm run start, as per Gem, it will be equivalent to npx expo start.
----

Now trying out app:
1) Web app runs
2) App on Android emulator does not show screen data correctly. Console has this error:
DOM Bundling failed 6ms node_modules\expo\dom\entry.js (1 module)
Unable to resolve "./../../../../../../NotInBackupScript/SG-EduApp/lms/components/HomeBlock.tsx" from "node_modules\expo\dom\entry.js"   

Perhaps the new version of lms app did not get reinstalled. So manually uninstalled old lms app on emulator.
Then copied ..\SG-EduApp\lms\android\app\build\outputs\apk\debug\app-debug.apk to emulator which installed the app on it.

The Android app had to be run from Metro using 'a'. Then it opened properly. Running it directly on emulator was shown blank Expo screen and 'm' on Metro was not having any effect on it.
At times, the app shows a page/route not found or something error. Restarting it usually works.
Once again, I am getting the error:
DOM Bundling failed 19ms node_modules\expo\dom\entry.js (1 module)
Unable to resolve "./../../../../../../NotInBackupScript/SG-EduApp/lms/components/HomeBlock.tsx" from "node_modules\expo\dom\entry.js"
----

Hmm. How could that be? I rebuilt the app. in NIBS folder after cleanup of node_modules.
Perhaps some cache on the emulator device is coming into play.
Used 'adb reboot' command on a terminal to reboot the emulator device.
IIRC, even after reboot, the same issue was there.

Is it stale Metro cache that's the cause? Now running:
npx expo start --clear

That fixed it. Now the 'My Studio' page shows the image and text as expected on the Android app running on emulator.
---

Me: Clearning the cache fixed a DOM bundler problem I was facing as I had renamed the directory of the project and built it again. I am surprised who rebuild including npm install, expo prebuild, did not clear the Metro cache. Am I missing something here?

Gem: This is a great observation and a common point of confusion for many developers. You are not missing anything; this is how the various tools in the Expo ecosystem are designed to work.

Understanding the Cache

  • npm install and expo prebuild do not interact with the Metro bundler's cache.

  • npm install manages and caches Node.js packages in the ~/.npm or ~/.cache/npm directory, and it installs project dependencies into your local node_modules folder.

  • expo prebuild generates the native Android and iOS project files (android/ and ios/ folders) based on your app.json or app.config.jsIt doesn't cache anything related to the bundler itself.

  • The Metro bundler has its own separate cache. This cache stores information about your project's files, such as their module dependencies and compiled JavaScript output. Its purpose is to speed up subsequent builds by not re-parsing every file on every run.

Why the Cache Wasn't Cleared

When you renamed the project directory, you changed the file path to all your source code. Metro's cache, however, was still holding onto the old paths. Even though you ran npm install and expo prebuild (which handle different parts of the project setup), the Metro bundler's cache remained untouched. It only knew about the previous file structure and had no reason to invalidate its cache entries for a new, different directory name.

When you ran expo start --clear, you explicitly told Metro to discard its cache. This forced the bundler to re-evaluate your project from scratch, correctly identifying the new file paths and resolving your issue.


Me: Where is Metro's cache storage on a Windows system

Gem: On a Windows system, Metro's cache is typically stored in the temporary directory. You can find it at:

C:\Users\<YourUsername>\AppData\Local\Temp\metro-cache

You can also access this directory more easily by typing %localappdata%\Temp into your File Explorer's address bar, and then navigating to the metro-cache folder from there.


Me: Hmm. So unlike .expo folder which is in the project folder itself, Metro's cache is outside the project folder.

Gem: That's correct. Metro's cache is stored in a system-level temporary directory, while the .expo folder is specific to your project.


Metro Cache (metro-cache)

Metro's cache is global to your system. It's stored in a temporary folder like %localappdata%\Temp\metro-cache on Windows. This design allows the cache to be shared across different projects, improving performance when switching between them, as it can reuse cached information for common dependencies.


Expo Folder (.expo)

The .expo folder, on the other hand, is specific to an individual project. It contains temporary files and data related to your Expo app, such as the bundle's manifest and other project-specific build artifacts. Because this information is tied directly to a single project, it makes sense to keep it within the project directory itself.

============
My initial attempt at using the Android dev build app from my Samsung phone worked partially. The app could not get the data from the backend. It turned out that among the two IPs my PC listed, the 192.168.x.x IP was not the mobile hotspot IP but something else (perhaps Ethernet). That was accessible from Android emulator but not from mobile phone.

Changing the ip in .env of frontend to mobile hotspot related PC ip, fixed the issue. App on phone shows the data as expected.
The app on Android emulator also shows the data as expected.
So ip address (of backend) change in .env of lms followed by restart of app is enough for previous dev build app to work with changed ip address of backend on both emulator and phone.

=============

I noticed that the package.json of the project is the one I modified to remove all ^. While doing a fresh rebuild I did not restore the package.json file to what it was earlier. Did that trip up something resulting in the 2nd build error (JVM related)?

I think I should do a fresh build but this time with original package.json as well package-lock.json.

Copied existing full data of lms (4 to 5 GB) and renamed it as 20250813-Working-lms. [Took time - perhaps around 10 min]

Steps for clean rebuild with old package.json and package-lock.json:

1) Deleted node_modules, .expo and android directories.

2) Reverted package.json and package-lock.json to before commit (and so to successful build of some weeks back).

3) Restored locked dependencies: Ran npm ci. As per Gem, "This ensures you have the exact dependency tree that worked for you before, as defined by package-lock.json."

Console output:
added 1231 packages, and audited 1232 packages in 35s

109 packages are looking for funding
  run `npm fund` for details

4 vulnerabilities (3 low, 1 critical)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.
----
Ignoring vulnerabilities above 
...
lms> findstr /s /i "expo-module-gradle-plugin" .\node_modules\*.gradle
lms> 
---
So no expo-module-gradle-plugin issue.

4) Generated the native project: Ran npx expo prebuild --clean. As per Gem, "This command will use your now-stable dependency tree to generate a new android directory."
Console output:
lms> npx expo prebuild --clean
env: load .env
env: export EXPO_PUBLIC_STRAPI_API_URL EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
√ Cleared android code
√ Created native directory
√ Updated package.json | no changes
√ Finished prebuild
lms> 
----
5) Here I will go with what npx expo prebuild has set rather than choose AGP and gradle versions.

6) Built the app: npx expo run:android. 
[Did not do gradle download. So must be using already downloaded 8.10.2 or something]
BUILD SUCCESSFUL in 10m 37s
---

Uninstalled old app from emulator. Copied new app to it.
Opening app with 'a' - few retries needed - 'Unmatched Route' is one of the errors (seen earlier)
Finally (after 3 or 4 retries) - 'Unmatched Route' was shown briefly and then app showed expected image and data.

Web app is also working showing expected image app and data.

Last step is to try it out on phone. Have already uninstalled old app.
Installed the new app. Needed 1 or 2 retries but after that app showed expected image and data.

So the app is now working on web and on both Android platforms. 

I did not have to change anything at all for this rebuild. Only point was using old package-lock.json and old package.json for the clean rebuild.
I tripped up on Gemini advice to delete package-lock.json but that led me to learn more about package-lock.json role and also how to figure out some build issues.

Note that .env file in lms was already pointing to correct IP address of backend. That is the only possible change that would be needed for clean rebuild to work. Don't know if Grade and AGP build tool version freeze is necessary.
================
================

17:03 14 August 2025

I updated Gemini with an edited log of the above, covering the fresh build with old package.json and old package-lock.json.

Gem: That's a fantastic and detailed log. Thank you for sharing it. Your meticulous process has led to a crucial insight about the entire build chain.

Analysis of Your Findings

First, to answer your question: yes, modifying package.json to remove all ^ and ~ while using a newer, possibly inconsistent package-lock.json was a key factor in your second build error. This created an ambiguous state that the build tools couldn't properly resolve.

Your successful rebuild confirms this. By reverting to the original package.json and package-lock.json files from a working commit, you restored the entire project's state to a known-good configuration. This had several domino effects:

  • Dependency Resolution: Running npm ci with the original package.json and package-lock.json installed the exact, working versions of all dependencies (like expo-auth-session@6.0.3 and its compatible sub-dependencies), which resolved the expo-module-gradle-plugin issue.

  • Consistent prebuild Output: Since your package.json and dependency tree were now consistent, npx expo prebuild was able to correctly generate the native android directory without any errors or warnings.

  • New Gradle Version: This is the most important finding. The new Gradle version (likely 8.10.2) was not the root cause of your failures. When given a perfectly consistent and valid project configuration from your older package.json, the new build tools were able to compile the app successfully without any manual intervention.


The Final Conclusion

The core problem was not with any single component (Gradle, prebuild, or a specific package), but with the inconsistency between the project's various configuration files.

When all parts of your project are aligned—the package.jsonpackage-lock.json, and the automatically generated android directory—the build process works flawlessly, even with newer build tools. The manual fixes we discussed (freezing Gradle versions) are a way to force consistency when your project is in an unknown or broken state. Your solution of reverting to a fully consistent set of files from a known-good commit accomplished the same thing, but in a more comprehensive way.

Your successful build is proof that maintaining a consistent and stable project configuration is the most important factor for a reliable and reproducible build process.


Me: From android/gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
----
android/build.gradle does NOT have a plugin section like:
plugins {
    id "com.android.application" version "8.6.0" apply false
    id "com.android.library" version "8.6.0" apply false
    id "com.facebook.react" version "..." apply false
}
----
So how do I find out what is the AGP version used for the build? I have uploaded my build.gradle file (for this successful build). Please check it for AGP version used.

Gem: Based on the build.gradle file you provided, the Android Gradle Plugin (AGP) version is not explicitly defined in a plugins block. Instead, it's being determined by a different method.

Finding the AGP Version

The AGP version is being set in the

dependencies block of the buildscript section as a classpath dependency. The line you need to look at is:

classpath('com.android.tools.build:gradle')

The version is not directly specified here, which means it's likely being pulled from the default settings of the React Native Gradle plugin or another configuration file. However, you can infer the version from other details in your build files.

  • Your gradle-wrapper.properties file is using Gradle 8.10.2.

  • The

    buildscript section of your build.gradle file sets compileSdkVersion to 35.

Based on these details, the AGP version is almost certainly 8.6.0, as this is the version that aligns with Gradle 8.10.2 and a compileSdkVersion of 35. You can also confirm this by checking the build logs, which typically state the AGP version being used.


Me: Well, I actually don't need to know the AGP version. I need to know what lines I have to have in build.gradle for some future rebuild from sources only, to ensure it uses same AGP version as used for this successful build.

Gem: I'm glad you brought that up. You don't need to know the specific AGP version to ensure a consistent build. Instead, you need to add the correct configuration to lock in the version that the system is already using successfully.

To ensure your future rebuilds use the same AGP version, you'll add a plugins block to your top-level android/build.gradle file. This is the standard way to declare and freeze the AGP version.

The Lines to Add

You should add the following plugins block to your android/build.gradle file, right at the top, to explicitly declare the AGP version.

plugins {
    id "com.android.application" version "8.6.0" apply false
    id "com.android.library" version "8.6.0" apply false
}

Reasoning:

The configuration you have in your buildscript block is implicitly pulling a compatible AGP version, but it doesn't lock it down. By adding this plugins block, you are now explicitly telling Gradle to use version 8.6.0, which is the version that aligns with your current successful build using Gradle 8.10.2 and compileSdkVersion 35. This ensures that any future build, even with a different version of npx expo prebuild, will use the same AGP version and result in a consistent outcome.


Me: Hmm. I don't want to add some new lines to existing build.gradle. I just want to remember some existing lines in build.gradle that will implicitly ensure that same AGP version is used. If that is not possible, can you specify the exact lines in current build.gradle that are related to determining roughly which AGP version will be used (even if not precisely). Then I can make a note of those lines for any future reproducible app rebuild from sources.

Gem: It's not possible to guarantee the exact same Android Gradle Plugin (AGP) version using only the existing lines in your build.gradle file. The lines you currently have rely on a more implicit system, which can change based on the versions of other packages and the Expo CLI.

However, you can make a note of the following lines that collectively represent the build environment that worked for you. When you generate a new android directory, these lines should be configured to match what you have now to ensure a similar outcome.


Key Lines for Future Reference

Here are the lines that are most relevant to determining the AGP version implicitly:

  1. AGP Classpath: This line is in the dependencies block of your buildscript section and brings in the AGP, even without a specified version. The specific version is determined by the com.facebook.react:react-native-gradle-plugin and its own dependencies.

    classpath('com.android.tools.build:gradle')

  2. compileSdkVersion: This line sets the compilation level for your Android app. The AGP version is tightly coupled to this setting, so a consistent compileSdkVersion will require a compatible AGP version.

    compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')

  3. Kotlin and NDK Versions: These lines are also in the buildscript section and define key tool versions that must be compatible with the AGP.

    kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25' ndkVersion = "26.1.10909125"

  4. Gradle Wrapper Version: This line is in your gradle-wrapper.properties file and specifies the exact version of Gradle being used. The AGP version must be compatible with it.

    distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

By keeping these lines consistent across your builds, you are doing your best to ensure the same AGP version is used implicitly. However, for true reproducibility, the explicit plugins block remains the most reliable method.


Me: Here is the note I prepared to save in my repo:
Gradle and some other build tool versions that are relevant to AGP (Android Gradle Plugin) version, used in successful build on 13 Aug. 2025
From android/gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
----
android/build.gradle does NOT have a plugin section that explicitly defines the AGP version. But, as per Gemini, the lines that are most relevant to determining the AGP version implicitly are:
[In buildscript.dependencies block, AGP classpath] 
        classpath('com.android.tools.build:gradle')
        classpath('com.facebook.react:react-native-gradle-plugin')
[In buildscript.ext block] 
        compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
        kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
        ndkVersion = "26.1.10909125"
----
---- end note ----
I will also save the entire gradle-wrapper.properties and build.gradle files (they are small files) in my repo (with a commit). The objective being to have a record of the android build key configuration used for successful build just in case that is needed for some future build from sources (repo).
...
Gem said above is OK.
...
I wanted to additionally note the specific AGP version used which Gem said is usually mentioned in the build log. But by default the build log is shown on console and my console history no longer shows me the build log.
...
On command for showing build output on console as well as saving it in a log file, Gem said:
npx expo run:android 2>&1 | tee build_log.txt

This command works as follows:

  • npx expo run:android: This runs the build and sends its output to standard output (stdout).

  • 2>&1: This redirects standard error (stderr) to stdout, so all output, including errors, is handled together.

  • | tee build_log.txt: This takes the combined output and pipes it into the tee command. tee then displays the output on your screen and saves a copy of it to the file build_log.txt.

=================

Comments