Is there a better way to do a PopToRoot async without leaking GRefs? - android

According to this bug: bugzilla.xamarin.com/show_bug.cgi?id=52597 the PopToRoot of the Navigation object still leak GRefs (in fact JNI Global References)
In one application we use Listview really intensively and to avoid crash I had to implement a workaround using two methods :
/// <summary>
/// Workaround to minimize the quantity of GREFs leaked by Xamarin.Forms JMA 17.02.2017
/// </summary>
/// <param name="animate">if set to <c>true</c> [animate].</param>
public async Task PopToRoot(bool animate)
{
var nav = Application.Current.MainPage.Navigation;
if(animate == false) //simulate animate = false
for (int i = 1; i < nav.NavigationStack.Count; i++)
if(nav.NavigationStack[i] is BaseView)
{
//hide the animation
(nav.NavigationStack[i] as BaseView).IsVisible = false;
//hide the toolbar title change
(nav.NavigationStack[i] as BaseView).Title = (nav.NavigationStack[nav.NavigationStack.Count - 1] as BaseView).Title;
}
await _PopToRootInner(true);
}
/// <summary>
/// Workaround to minimize the quantity of GREFs leaked by Xamarin.Forms JMA 17.02.2017
/// </summary>
/// <param name="animate">if set to <c>true</c> [animate].</param>
private async Task _PopToRootInner(bool animate)
{
var nav = Application.Current.MainPage.Navigation;
await PopAsync(animate);
if (nav.NavigationStack.Count > 1 && nav.NavigationStack[nav.NavigationStack.Count - 1] is BaseView)
await (nav.NavigationStack[nav.NavigationStack.Count - 1] as BaseView)._PopToRootInner(animate);
}
My question is : Do you know any better method to do a PopToRoot without animation and without leaking GRefs ?
And Also, if you know how to correct the bug, I am of course interested !

Related

PopToRoot while retap active tab in TabbedPage in MAUI Android

How can I setup a PopToRoot action while tapping active tab on Android devices. On iOS it works fine but on android nothing happened if i clicked active tab again.
In Xamarin using shell i use a code :
[assembly: ExportRenderer(typeof(Shell), typeof(JknShellRenderer))]
namespace MyApp.Droid.Renderers
{
/// <summary>
/// The JknShellRenderer is necessary in order to replace the ShellItemRenderer with your own.
/// </summary>
class JknShellRenderer : ShellRenderer
{
public JknShellRenderer(Context context) : base(context)
{
}
protected override IShellItemRenderer CreateShellItemRenderer(ShellItem shellItem)
{
return new JknShellItemRenderer(this);
}
}
/// <summary>
/// This renderer is necessary to allow us to handle the TabReselected (current tab clicked) event as it is not implemented by default on Android
/// and the only way is to go through a renderer.
/// </summary>
public class JknShellItemRenderer : ShellItemRenderer
{
public JknShellItemRenderer(IShellContext shellContext) : base(shellContext)
{
}
/// <summary>
/// Pops to root when the selected tab is pressed.
/// </summary>
/// <param name="shellSection"></param>
protected override void OnTabReselected(ShellSection shellSection)
{
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
{
await shellSection?.Navigation.PopToRootAsync();
});
}
}
}
but on MAUI I'm not using a Shell...
What the way to make it in MAUI TabbedPage ?

How to implement back button handling on Android with Redux

I'm trying to implement back button handling on Android using CoRedux for my Redux store. I did find one way to do it, but I am hoping there is a more elegant solution because mine seems like a hack.
Problem
At the heart of the problem is the fact returning to an Android Fragment is not the same as rendering that Fragment for the first time.
The first time a user visits the Fragment, I render it with the FragmentManager as a transaction, adding a back stack entry for the "main" screen
fragmentManager?.beginTransaction()
?.add(R.id.myFragmentContainer, MyFragment1())
?.addToBackStack("main")?.commit()
When the user returns to that fragment from another fragment, the way to get back to it is to pop the back stack:
fragmentManager?.popBackStack()
This seems to conflict with Redux principles wherein the state should be enough to render the UI but in this case the path TO the state also matters.
Hack Solution
I'm hoping someone can improve on this solution, but I managed to solve this problem by introducing some state that resides outside of Redux, a boolean called skipRendering. You could call this "ephemeral" state perhaps. Initialized to false, skipRendering gets set to true when the user taps the back button:
fun popBackStack() {
fragmentManager?.popBackStack()
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
skipRendering = true
}
Dispatching the back button to the redux store rewinds the redux state to the prior state as follows:
return when (action) {
// ...
ReduxAction.BackButton -> {
state.pastState
?: throw IllegalStateException("More back taps processed than past state frames")
}
}
For what it's worth, pastState gets populated by the reducer whenever the user requests to visit a fragment from which the user can subsequently tap back.
return when (action) {
// ...
ReduxAction.ShowMyFragment1 -> {
state.copy(pastState = state, screenDisplayed = C)
}
}
Finally, the render skips processing if skipRendering since the necessary work of calling fragmentManager?.popBackStack() was handled before dispatching the BackButton action.
I suspect there is a better solution which uses Redux constructs for example a side effect. But I'm stuck figuring out a way to solve this more elegantly.
To solve this problem, I decided to accept that the conflict cannot be resolved directly. The conflict is between Redux and Android's native back button handling because Redux needs to be master of the state but Android holds the back stack information. Recognizing that these two don't mix well, I decided to ditch Android's back stack handling and implement it entirely on my Redux store.
data class LLReduxState(
// ...
val screenBackStack: List<ScreenDisplayed> = listOf(ScreenDisplayed.MainScreen)
)
sealed class ScreenDisplayed {
object MainScreen : ScreenDisplayed()
object AScreen : ScreenDisplayed()
object BScreen : ScreenDisplayed()
object CScreen : ScreenDisplayed()
}
Here's what the reducer looks like:
private fun reducer(state: LLReduxState, action: ReduxAction): LLReduxState {
return when (action) {
// ...
ReduxAction.BackButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.addAll(state.screenBackStack)
it.removeAt(0)
})
}
ReduxAction.AButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.AScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.BButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.BScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.CButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.CScreen)
it.addAll(state.screenBackStack)
})
}
}
}
In my fragment, the Activity can call this API I exposed when the Activity's onBackPressed() gets called by the operating system:
fun popBackStack() {
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
}
Lastly, the Fragment renders as follows:
private fun render(state: LLReduxState) {
// ...
if (ScreenDisplayed.AScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, AFragment())
?.commit()
}
if (ScreenDisplayed.BScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, BFragment())
?.commit()
}
if (ScreenDisplayed.CScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, CFragment())
?.commit()
}
}
This solution works perfectly for back button handling because it applies Redux in the way it was meant to be applied. As evidence, I was able to write automation tests which mock the back stack as follows by setting the initial state to one with the deepest back stack:
LLReduxState(
screenBackStack = listOf(
ScreenDisplayed.CScreen,
ScreenDisplayed.BScreen,
ScreenDisplayed.AScreen,
ScreenDisplayed.MainScreen
)
)
I've left some details out which are specific to CoRedux.

Recursive Future calls with Dart

I have this sort of pseudo-code in a Widget state (it uses a flutter_staggered_grid_view to display an infinite number variable size cards, cards being loaded page by page):
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_onAfterLayout);
super.initState();
}
_onAfterLayout(_) {
if (_needNextPage()) {
_getNextPage();
_fillPages();
}
}
// a function that fills the screen with cards, calling getNextPage every 500 ms until the screen is full
_fillPages() {
Future.delayed(const Duration(milliseconds: 500), () {
if (_needNextPage()) {
_getNextPage();
_fillPages(); // go recursive
}
});
}
// a function that checks if a page should be loaded
bool _needNextPage() {
...
}
// a function that loads a page from network
_getNextPage() {
if (loading) return;
loading = true;
op = CancelableOperation.fromFuture(Context.search(context, results.input)).then((res) {
loading = false;
setState(() {
// add what has been loaded to the current result list, causes rebuilt, etc.
});
});
}
This code functionally works, but when I run it in an Android Nexus 10 virtual device with a small page size (it has a big screen, so it needs lots of request to fill an entire page), the whole app just crashes rapidly without any log, information nor stacktrace, and I was wondering if it could be due to my code...
The recursive _fillPages smells bad to me, maybe there are better ways, I know there's no tail recursion optimization in Dart so I was wondering if this could cause issues?

Memory used by pushing a lot of pages on Xamarin.Forms' NavigationPage

I have a Xamarin.Forms NavigationPage and I noticed that after pushing quite a lot of pages (say 20), the app starts being laggy and might freeze at some point. I'm guessing it must be using a lot of memory (I should really check in Android's monitoring tools now that I thought about it).
Is there another way I can provide a history functionality (so, the user can always press back to go to where they were reading before) that doesn't eat up all the memory? I know it's possible because it's done in other apps.
For solutions with multiple pages and large navigation stack You could use PagesFactory:
public static class PagesFactory
{
static readonly Dictionary<Type, Page> pages = new Dictionary<Type, Page>();
static NavigationPage navigation;
public static NavigationPage GetNavigation()
{
if (navigation == null)
{
navigation = new NavigationPage(PagesFactory.GetPage<Views.MainMenuView>());
}
return navigation;
}
public static T GetPage<T>(bool cachePages = true) where T : Page
{
Type pageType = typeof(T);
if (cachePages)
{
if (!pages.ContainsKey(pageType))
{
Page page = (Page)Activator.CreateInstance(pageType);
pages.Add(pageType, page);
}
return pages[pageType] as T;
}
else
{
return Activator.CreateInstance(pageType) as T;
}
}
public static async Task PushAsync<T>() where T : Page
{
await GetNavigation().PushAsync(GetPage<T>());
}
public static async Task PopAsync()
{
await GetNavigation().PopAsync();
}
public static async Task PopToRootAsync()
{
await GetNavigation().PopToRootAsync();
}
public static async Task PopThenPushAsync<T>() where T : Page
{
await GetNavigation().PopAsync();
await GetNavigation().PushAsync(GetPage<T>());
}
}

Bug after Samsung update | Monogame | Detecting viewport.height as devices width

UPDATE
This issue have also been discussed in: https://github.com/mono/MonoGame/issues/2492
The problem is if the app is only allowed to run in Landscape orientation, not when you use Portrait or both.
Got a android game coded in monogame, where I got a GraphicsDeviceManager which are set to FullScreen=true.
I use the GraphicsDevice.Viewport.Height and the GraphicsDevice.Viewport.Width to determine the resolution of the device.
This was working very good until I got an samsung update and had to turn FastDev off.
The big mysterious problem:
When I debug with the pc, the height and width of the viewport is set correct. But when i unplug and play the app after 1-2 times the viewport.Height becomes the devices width, and the viewport.Width becomes the device height, which completely makes the game unplayable.
Its very hard to find the solution, since this is never happening when i debug and got the cable in from pc to the device.
Anyone got any ideas what it can be?
I can now confirm that it is because of the samsung update
I made worlds simplest android app to test it, just got a mainframe with a background image called "bg" and a spritefront printing out the viewport.Width and viewport.Height.
Here's the code:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace TestRes
{
/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Rectangle _mainFrame;
Texture2D _background;
string text;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.IsFullScreen = true;
//graphics.PreferredBackBufferWidth = 800;
// graphics.PreferredBackBufferHeight = 480;
graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft;
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting
/// This is where it can query for any required services and load any non-
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
text = "";
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
_background = Content.Load<Texture2D>("Bilder/bg");
_mainFrame = new Rectangle(0, 0, this.GraphicsDevice.Viewport.Width, this.GraphicsDevice.Viewport.Height);
// TODO: use this.Content to load your game content here
font = Content.Load<SpriteFont>("spriteFont1");
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
{
Exit();
}
text = "ScreenWidth: " + this.GraphicsDevice.Viewport.Width + ", screenheight: " + this.GraphicsDevice.Viewport.Height;
// TODO: Add your update logic here
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(_background, _mainFrame, Color.White);
spriteBatch.DrawString(font, text, new Vector2(16, 1000), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
When deploying from VS everything is good, the viewport.width is the actual device width and the viewport.height is the actual device height. But when trying to deploy it from the Samsung Galaxy S4 active (sometimes you need to try 2-3 times) then all of a sudden Viewport.Height is the device Width and the other way around, which makes the background picture just cover a bit of the screen.
Have taken pictures to show it:
Checked in version 3.6 , and the bug is fixed.

Categories

Resources