-
Notifications
You must be signed in to change notification settings - Fork 75
6. Using custom views instead of fragments
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.
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.
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();
}
}
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.
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);
}
}
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()));
}
}
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());
});
By default, the library provides the following view change handlers (for both state change directions):
- AnimatorViewChangeHandler: base class for animations based on
AnimatorSet
- FadeViewChangeHandler: a basic fade animation between two views
- NoOpViewChangeHandler: just swap the two views out
- SegueViewChangeHandler: left-right and right-left animation
Additional custom animations are possible by implementing the ViewChangeHandler
interface.
However, this is used by DefaultStateChanger
, which means that using it is entirely optional.
In Simple-Stack, the behavior of DefaultStateChanger
is the following:
-
it persists the state of the previous view (both hierarchy state, and by
Bundleable
interface if implemented) -
it inflates the specified layout, then restores the state of the new view (both hierarchy state, and by
Bundleable
interface if implemented) -
and executes a "view change" in-between to animate this transition using the specified "view change handler".
Of course, for this to work, and in order to use DefaultStateChanger
, your state keys must implement DefaultViewKey
, which specifies the layout resource id, and the view change handler.
The default state changer uses a KeyContextWrapper
as the base context of your view, thus allowing you to obtain the key associated with your view directly via Backstack.getKey(getContext())
.
By default, DefaultViewKey
s provide the layout to inflate, and the view change handler used during forward and backward animation.
In the following example, this is an auto-value generated immutable class that is parcelable, and also properly implements equals()
and hashCode()
.
@AutoValue
public abstract class FirstKey
implements DefaultViewKey, Parcelable {
public static FirstKey create() {
return new AutoValue_FirstKey();
}
@Override
public int layout() {
return R.layout.first_view;
}
@Override
public ViewChangeHandler viewChangeHandler() {
return new SegueViewChangeHandler();
}
}