How to: Manage Chart Lifecycles

This section describes how chart life cycles are managed in Android and what you have to do, particularly when using ChartView.

About Chart Lifecycles

Charts are Android Views, but they have an unusually large amount of state (axis ranges, styles etc.) which may change during the chart’s lifetime, and are sometimes backed by active data adapters. They also use Open GL ES 2.0 for rendering. Both of these factors impose some demands on how their lifecyles are managed in Android.

Persisting Charts Over Activity Restarts

An important feature of Android is that it responds to device configuration changes (most commonly, screen rotation) by tearing down your Activities and their associated view hierarchies and recreating them.

If your chart is entirely static and can be completely recreated from scratch in its original form, this is not a problem.

However, if your chart has any user interaction, incoming data, or any other kind of state change after its initial creation, you need to ensure that its state is maintained over the configuration change.

Pause and Resume Lifecycle Events

The Open GL rendering views need to know when the Activity is paused and resumed, in order to manage themselves (and their associated threads).

Using ChartFragment

ChartFragment takes care of both these issues for you. It internally uses the setRetainInstance(true) API to ensure that it survives the configuration change. It also provides implementations of onPause and onResume which forward to the equivalents in the GL views.

We strongly recommend that you use ChartFragment for these reasons.

Using ChartView

In versions of Android prior to V4.2 it was not permitted to nest Fragments within other Fragments. In some scenarios this may prevent the use of ChartFragment, and so we provide ChartView as a fallback. However, it then becomes your responsibility to manage the 2 issues described above.

This is illustrated in the example below, which takes the Custom DataAdapter Sample How to: Create a custom DataAdapter and modifies it to use ChartView rather than ChartFragment.

To handle the retention of the chart across configuration changes make the ChartView a member of the Activity class and use the onRetainNonConfigurationInstance and getLastNonConfigurationInstance API methods to hold on to it. These methods have been deprecated in Android, with the recommendation that you use Fragment.setRetainInstance as ChartFragment in fact does - this section of the guide is of course dealing with the situation where that is not possible.

Java

private ChartView chartView = null;

public Object onRetainNonConfigurationInstance () {
    // Tell Android to hold on to the ChartView
    return chartView;
}

C#

private ChartView chartView = null;

public override Java.Lang.Object OnRetainNonConfigurationInstance () {
    // Tell Android to hold on to the ChartView
    return chartView;
}

In the Activity’s onCreate method, you expect that a new ChartView has been created from the layout XML. You now look to see if there is an ‘old’ ChartView. If you have one swap it in, in place of the ‘new’ one. If you don’t have one go through the full chart setup.

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_custom_data_adapter);

    // If the activity is newly created
    if (chartView == null) {

        chartView = (ChartView) this.findViewById(R.id.chart);

        ChartView oldChartView = null;
        @SuppressWarnings("deprecation")
        Object o = getLastNonConfigurationInstance();
        if (o != null && o instanceof ChartView) {
            oldChartView = (ChartView) o;
        }

        // If this is the activity's first existence
        if (oldChartView == null) {
            // Get the a reference to the ShinobiChart
            ShinobiChart shinobiChart = chartView.getShinobiChart();
            shinobiChart.setTitle("Custom Data Adapter");

            // Create X and Y axes and add to the chart
            NumberAxis xAxis = new NumberAxis();
            shinobiChart.setXAxis(xAxis);
            xAxis.getStyle().setInterSeriesPadding(0.2f);

            NumberAxis yAxis = new NumberAxis();
            shinobiChart.setYAxis(yAxis);

            // Create our custom DataAdapter
            @SuppressWarnings("rawtypes")
            DataAdapter dataAdapter = new CustomDataAdapter<Double, Double>();

            // Create a ColumnSeries and give it the data adapter
            ColumnSeries series = new ColumnSeries();
            series.setDataAdapter(dataAdapter);
            shinobiChart.addSeries(series);
        }
        // If the activity had a previous existence
        else {
            // Remove the new ChartView from its parent
            ViewGroup parent = ((ViewGroup) chartView.getParent());
            parent.removeView(chartView);
            // Add the old Chartview to the parent, and replace the local reference
            parent.addView(oldChartView);
            chartView = oldChartView;
        }

        run();
    }
}

C#

protected override void OnCreate (Bundle bundle)
{
    base.OnCreate (bundle);

    // Set our view from the "main" layout resource
    SetContentView (Resource.Layout.Main);

    // If the activity is newly created
    if (chartView == null) {

        chartView = (ChartView) this.FindViewById(Resource.Id.chart);

        ChartView oldChartView = LastNonConfigurationInstance as ChartView;

        // If this is the activity's first existence
        if (oldChartView == null) {
            // Get the a reference to the ShinobiChart
            IShinobiChart shinobiChart = chartView.ShinobiChart;
            shinobiChart.Title = "Custom Data Adapter (ChartView)";

            // Create X and Y axes and add to the chart
            NumberAxis xAxis = new NumberAxis();
            shinobiChart.XAxis = xAxis;
            xAxis.Style.InterSeriesPadding = 0.2f;

            NumberAxis yAxis = new NumberAxis();
            shinobiChart.YAxis = yAxis;

            // Create our custom DataAdapter
            DataAdapter dataAdapter = new CustomDataAdapter();

            // Create a ColumnSeries and give it the data adapter
            ColumnSeries series = new ColumnSeries();
            series.DataAdapter = dataAdapter;
            shinobiChart.AddSeries(series);
        }
        // If the activity had a previous existence
        else {
            // Remove the new ChartView from its parent
            ViewGroup parent = ((ViewGroup) chartView.Parent);
            parent.RemoveView(chartView);
            // Add the old Chartview to the parent, and replace the local reference
            parent.AddView(oldChartView);
            chartView = oldChartView;
        }

        Run();
    }
}

Normally you expect to have no ‘old’ ChartView when you start the app, but on screen rotations you expect to find one.

It’s also necessary to detach the old ChartView from its parent when the activity is destroyed, to avoid an IllegalStateException when you reattach it.

Java

@Override
public void onDestroy() {
    super.onDestroy();
    // Remove the old ChartView from its parent
    ViewGroup parent = (ViewGroup) chartView.getParent();
    parent.removeView(chartView);
}

C#

protected override void OnDestroy() {
    base.OnDestroy();
    // Remove the old ChartView from its parent
    ViewGroup parent = (ViewGroup) chartView.Parent;
    parent.RemoveView(chartView);
}

The sharp-eyed will spot the run() call at the end of the large code snippet above. In the modified sample you have given responsibility for pushing data into the dataAdapter to the Activity class, as you no longer have the UpdatingChartFragment class from the original sample. This is implementation detail and has no bearing on the subject of this guide.

Finally you override the onPause and onResume to forward these events from the Activity to the ChartView (and incidentally pause and resume the runnable pushing data into the dataAdapter).

Java

@Override
public void onPause() {
    super.onPause();
    if (chartView != null) {
        chartView.removeCallbacks(this);
        // Ensure the GL views get to hear about the pause/resume events
        chartView.onPause();
    }
}

@Override
public void onResume() {
    super.onResume();
    if (chartView != null) {
        // Ensure the GL views get to hear about the pause/resume events
        chartView.onResume();
        run();
    }
}

C#

protected override void OnPause() {
    base.OnPause();
    if (chartView != null) {
        chartView.RemoveCallbacks(this);
        // Ensure the GL views get to hear about the pause/resume events
        chartView.OnPause();
    }
}

protected override void OnResume() {
    base.OnResume();
        if (chartView != null) {
        // Ensure the GL views get to hear about the pause/resume events
        chartView.OnResume();
        Run();
    }
}

This is, of course, quite a simple scenario, and your implementation will depend heavily on your application architecture. Getting this right can be tricky and demands a clear understanding of Android Activity life cycles. We strongly recommend that you use ChartFragment wherever possible.

See related code sample: Custom DataAdapter ChartView Sample, in the samples/custom-data-adapter-chartview folder of your product download (Xamarin.Android/samples/CustomDataAdapterChartView if you’re using Xamarin.Android).