Skip to content

gfreivasc/crane

Repository files navigation

Crane

A fully programatic, type-safe navigation solution for Android development, focused on multi-modular navigation.

Installing

Crane artifacts are hosted through JitPack (and there's still no release available currently). You'll need to add jitpack to your build repositories.

// Your root "build.gradle.kts"
allprojects {
    repositories {
        // ...
        maven { setUrl("https://jitpack.io") }
    }
}

And finally include Crane artifacts to your project (for enabling KSP, see this guide)

// Any project module "build.gradle.kts"
// For version, see JitPack badge
dependencies {
    val craneVersion = "1.0.0-alpha01"
    implementation("com.gabrielfv.crane:crane:$craneVersion")
}

How it works

Crane is an engine that wires navigation based on Route instances to the underlying native android navigation sctucture. Currently supporting Fragment navigation, it uses FragmentActivity's supportFragmentManager to properly handle backstack and fragment transitions, while providing an easy to use API surface that allows

  • Push/Pop functionality through Route instances that can hold a Fragment parameters.
  • Mechanism for a Fragment to return results down the stack.
  • A Route-Fragment relationship that allows to easy multi-modular navigation.
  • Capability to easily pop an specific sub-set of Fragment instances off the stack.
  • Resilient structure that can survive configuration changes and process death.
    • Test this with adb shell am kill <app-package> command with app on background.

Setting up your navigation

Here's an example of a fragment routed by Crane:

// i.e. ":navigation" module
@Parcelize
data class HomeRoute(val title: String) : Route(HomeFragment::class)

// i.e. ":features:home" module
class HomeFragment : Fragment() {
    private val params: HomeRoute by params()

    // ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById(R.id.title).text = params.title
    }
}

Now to set up your navigation you will need to create an activity that will hold everything for us

class NavRootActivity : AppCompatActivity {
    private val crane: Crane = Crane.create()  // Or on your Dependency Graph

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crane.init(this, android.R.id.content, ARoute())

        // Allow crane to restore any state it might have saved
        if (savedInstanceState != null) {
            crane.restoreSavedState(savedInstanceState)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // Allow Crane to save what it needs to
        crane.saveInstanceState(outState)
    }

    override fun onBackPressed() {
        // Delegate back press to Crane
        if (!crane.pop()) super.onBackPressed()
    }
}

Wrapping up

You will also need to make the same Crane instance available across your app, so that fragments can use that to navigate to other fragments. Ideally this should be handled by your project's dependency inversion solution, but a singleton accessor is planned. So you'll need:

  • A root activity with a FragmentManager.
  • A fragment that will represent your navigation home.
  • A Route to that fragment.
  • (Optional) Something to hold the single Crane instance and make it available to the rest of the project.

Navigation Affinity

Closing a sub-set of views in a stack at once is a common requirement in android projects. While regular solutions can be tricky, Crane leverages FragmentManager under the affinity abstraction. Basically what it means is that once a fragment is pushed with an affinity, every fragment that is pushed after it belongs to the same affinity, until a new affinity is set. When we wish to pop a sub-set of fragments from the stack, we're speaking of popping an affinity. This will make Crane resume to the fragment previous to the one that set the popped affinity. The root fragment/route will always have an affinity associated with it regardless of wether it was set to do it or not, popping this affinity will close the application.

Take for example "post new picture" feature which consists of:

Gallery -> Edit Image -> Caption & Confirm

After we confirm our action of posting a new picture, we don't want to go back to either "Gallery" (or maybe we want?) nor "Edit Image" (this indeed). So we can define that "Gallery" is what would set our affinity here.

// GalleryRoute.kt
@Parcelize
class GalleryRoute : AffinityRoute(GalleryFragment::class)

// Any fragment
crane.push(GalleryRoute()) // Crane will know to set an Affinity

// ConfirmPicturePostFragment.kt
class ConfirmPicturePostFragment() : Fragment {

    // ...
    fun finish(postUrl: String) {
        crane.popAffinity()
        // We could also...
        crane.push(PostSuccessFeedbackRoute(postUrl))
    }
}

In this situation, we decided to add a little feedback, and as we move to "Success Feedback" we've already closed the previously active affinity, and things will work just fine.

Returning results

Crane also offers a mechanism to pass results to anyone in the stack, even pass ahead, but please don't (or do?). It's as simple as it is to navigate, the only requirement is that the result should be Parcelable.

// ProfileRegistrationResult.kt
@Parcelize
data class ProfileRegistrationResult(val success: Boolean) : Parcelable

// Any fragment (possibly "ConfirmProfileRegistrationFragment")
crane.pushResult(ProfileRegistrationResult(true))

// Any other fragment
val result = crane.fetchResult<ProfileRegistrationResult>()

This current implementation comes with a limitation: Only one result of each type is allowed at a time, and it gets erased once it's fetched. This will probably change in the future, likely along with an Api change.

Roadmap

  • Allow nesting.
  • Refactor result mechanism.

License

Copyright 2021 Gabriel Freitas Vasconcelos

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

A fully programatic, type-safe navigation solution for Android development, focused on multi-modular navigation.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages