Skip to content

6. Using custom views instead of fragments

Gabor Varadi edited this page Jun 1, 2020 · 3 revisions

In order to create a view-based setup, using the DefaultStateChanger and Navigator is the easiest way to start out with.

Here is a step-by-step guide to using views.

Steps

Creating a custom viewgroup

To create a custom viewgroup, you need to create a layout file as you generally do, for example layout/hello_world_view.xml.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/hello_world_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        android:layout_centerInParent="true" />
</RelativeLayout>

But you also create a corresponding class for it that extends your root viewgroup:

public class HelloWorldView extends RelativeLayout {
    public HelloWorldView(@NonNull Context context) {
        super(context);
        init(context);
    }

    public HelloWorldView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public HelloWorldView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @TargetApi(21)
    public HelloWorldView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        if(!isInEditMode()) {
            // ... get key, inject from dagger component, etc.
        }
    }

    private HelloWorldViewBinding binding;

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        binding = HelloWorldViewBinding.bind(this);
    }
}

And most importantly, once you've created this custom viewgroup, you want to replace the root in your layout XML file.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

to

<your.packages.HelloWorldView xmlns:android="http://schemas.android.com/apk/res/android"
   ...>
</your.packages.HelloWorldView>

This means when your layout XML is inflated, it'll create your custom viewgroup, where you can handle its views' events.

Creating a key, and associating it with a custom viewgroup

Creating a key

Keys are immutable value objects that represent your state in your application (where you are and where you've been).

Generally this should be Parcelable (or a KeyParceler must be specified to make it be Parcelable).

If you use simple-stack in Java, then your keys will typically look somewhat like this:

@AutoValue
public abstract class HelloWorldKey extends BaseKey {
    public static HelloWorldKey create() {
        return new AutoValue_HelloWorldKey();
    }

    @Override
    public int layout() {
        return R.layout.hello_world_view;
    }
}

Where BaseKey is

public abstract class BaseKey implements DefaultViewKey, Parcelable {
    @Override
    public ViewChangeHandler viewChangeHandler() {
        return new SegueViewChangeHandler();
    }
}

Setting up auto-value

For AutoValue to work, you need to add it as a compileOnly and annotationProcessor dependency. The samples use auto-parcel to make them Parcelable, but with Kotlin, you can use @Parcelize data class.

dependencies {
    ....
    provided "com.google.auto.value:auto-value:1.4.1"
    annotationProcessor "com.google.auto.value:auto-value:1.4.1"
}

Then it'll just work!

For additional parameters in the key, you just for example want to add a public abstract String param(); method, as you normally would with auto-value.

Accessing a key inside a custom viewgroup

Considering you generally add additional parameters needed by your view to the key (think of it like a typed Intent), the DefaultStateChanger inflates the view using stateChange.createContext() (which creates a KeyContextWrapper) as its context, which allows you to use Backstack.getKey(context) to obtain the key.

For example,

public class HelloWorldView extends RelativeLayout {
    // ...
   
    HelloWorldKey helloWorldKey;

    private void init(Context context) {
        if(!isInEditMode()) {
            helloWorldKey = Backstack.getKey(context);
        }
    }

Installing the navigator for handling backstack

In your Activity, you generally want to do the following, or something similar:

public class MainActivity
        extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Navigator.install(this, findViewById(R.id.root), History.single(HelloWorldKey.create()));
    }
}

Navigation

Navigating between screens with Navigator

If you're using Navigator, then you can easily access the backstack with Navigator.getBackstack(context).

binding.helloButton.setOnClickListener((view) -> {
    Navigator.getBackstack(view.getContext()).goTo(OtherKey.create());
});
Clone this wiki locally