I would say that it depends on which asset is loading and when it is needed.
The Call of Duty Mobile is a good example.
When you first opened the game, you will synchronously load the necessary data for the basic display of items(weapons, characters), as soon as this is completed, you can play, but all game skins are not loaded, therefore skins are displayed by default, that is, there can be 100 players on the scene and everyone will be look the same, while you are playing, extra skins are loaded, these are asynchronous operations.
Regarding which path to choose to work with the result, it again depends on the project, what architecture you have, there is no one ideal solution. If you need to do some action once after loading the result but do not need to work with memory management, then this event, if you need to load an asset and, for example, make 100 instances and sometime process the result again, then it is better to work with a AsyncOperationHandle.
To use an async model (Built-in .NET) or coroutines, again depends on the project. If your class does not inherit from an MonoBehaviour
, then you can only use the Built-in async model. (or create an additional script with methods for launching a coroutine that will be attached to the GameObject on the scene, but it will rather be a singleton, and this is not always welcome)
Therefore it all depends on the specifics of the project.
By the way, I am currently developing a convenient Addressables assets management tool, maybe you will find it useful. It implements control scenarios based on different models (Sync, Async (.NET / UniTask), Coroutine), also it has results caching and automatic asset release.