sockeqwe / mosby

A Model-View-Presenter / Model-View-Intent library for modern Android apps
http://hannesdorfmann.com/mosby/
Apache License 2.0
5.49k stars 841 forks source link

Feature suggestion - templates for adding MVI based feature #308

Closed MFlisar closed 6 years ago

MFlisar commented 6 years ago

Whenever I add a feature to my app, I see myself copying over 4 classes, which basically are all the same.

I always create 4 classes like following:

My basic copy & paste source looks like following:

class CategoryInteractor {

}

class CategoryPresenter(var initialState: CategoryState = CategoryState()) : MviBasePresenter<MviCategorySelectorActivity, CategoryState>() {

    private val mCategoryInteractor: CategoryInteractor

    init {
        mCategoryInteractor = CategoryInteractor()
    }

    override fun bindIntents() {
        val observables = ArrayList<Observable<CategoryState.CategoryPartialState>>()

        // TODO
        // observables.add(...);

        val allIntents = Observable.merge(observables)
        val stateObservable = allIntents.scan(initialState, { previousState, partialChanges -> this.viewStateReducer(previousState, partialChanges) }).distinctUntilChanged()
                .doOnNext { newViewState -> L.d("MviCategoryActivity ReducedState: %s", newViewState.toString()) }
        subscribeViewState(stateObservable) { obj, viewState -> obj.render(viewState) }
    }

    private fun viewStateReducer(previousState: CategoryState, partialChanges: CategoryState.CategoryPartialState): CategoryState {
        return partialChanges.reduce(previousState)
    }
}

@Parcelize
open class CategoryState(var error: Throwable? = null, var category: String? = null) : Parcelable {

    override fun toString(): String {
        return "CategoryState(error=$error, category=$category)"
    }

    fun copy(error: Throwable? = this.error,
             category: String? = this.category) = CategoryState(error, category)

    // -----------------
    // Partial States
    // -----------------

    sealed class CategoryPartialState() {
        abstract fun reduce(state: CategoryState): CategoryState

        abstract class SimpleStateChangePartialState() : CategoryPartialState() {
            override fun reduce(state: CategoryState): CategoryState {
                return state.copy()
            }
        }
    }
}

class MviCategoryActivity : BaseMviActivity<MviSelectorActivity, CategoryState, CategoryPresenter>() {

    // -------------------
    // companion
    // -------------------

    companion object {

    }

    // -------------------
    // lifecycle + view events
    // -------------------

    // -------------------
    // MVI - Presenter + Observables
    // -------------------

    override fun createPresenter(): CategoryPresenter {
        var presenter = CategoryPresenter()
        lastState?.let { presenter.initialState = lastState!! }
        return presenter
    }

    // -------------------
    // MVI - render
    // -------------------

    fun render(state: CategoryState) {
        lastState = state
    }
}

It must not be exactly like my template, but it would be a nice android studio extension to be able to add default templates where you only enter "Category" in my case and the template will create 4 files like the ones above or similar.

You probably have some template you always reuse, how do you handle this? Do you always create your files manually?

larten commented 6 years ago

Android Studio supports own code templates by default. You can find a lot of articles about the creation. I have a few base classes based on mosby library and I can create the fragment, presenter, viewstate classes only with set the feature name.

dimsuz commented 6 years ago

The problem with this is that every project/team will have its own set of such templates. So I think it's not something you can generalize and distribute with a library. You could make another artifact, but then again - 1 out of 5 users will use it, and 4 of them will create something different.

larten commented 6 years ago

You can create base classes and the team/projects can it use as a submodule or library. With generics and abstract methods/variables it's possible and reduce boilerplate code.

jhowens89 commented 6 years ago

@MFlisar, I went through a very similar line of thinking even as far as considering templates. I ended writing a generator with KotlinPoet and it has worked out fantastically. Here's an example of what my generator script looks like:

val destinationData = DestinationData("match_play.leaderboard.xxx")
         val breakingNewsBanner = MviUnitInput.New("Break News Banner",
                 layoutType = LayoutType.LINEAR)
         val liveBanner = MviUnitInput.Existing("P Cup Live Banner",
                 packageName = "com.tour.pgatour.dual_team.scoring.live_banner")
         val matchPlayBanner = MviUnitInput.New("Match Play Banner",
                 layoutType = LayoutType.RELATIVE)

         val tournamentHeader = MviUnitInput.Existing("P Cup Tournament Header",
                 packageName = "com.tour.pgatour.dual_team.scoring.tournament_model")

         val weatherBroadcast = MviUnitInput.Existing("Weather And Broadcast",
                 packageName = "com.tour.pgatour.dual_team.scoring.weather_broadcast")

         val matchPlayRoundSelector = MviUnitInput.New("Match Play Round Selector",
                 layoutType = LayoutType.LINEAR)

         val roundPlayMatchSummaries = MviUnitInput.New("Round Play Match Summaries",
                 layoutType = LayoutType.RELATIVE)

         val knockOutMatchSummaries = MviUnitInput.New("Knockout Match Summaries",
                 layoutType = LayoutType.RELATIVE)

         val feature = FeatureData(featureName = "Match Play Leaderboard",
                 topComponentConfig = TopLevelOptionConfig(
                         uiOption = UiOption.FRAGMENT,
                         mviUnits = listOf(breakingNewsBanner,
                                 liveBanner,
                                 matchPlayBanner,
                                 tournamentHeader,
                                 weatherBroadcast,
                                 matchPlayRoundSelector),
                         refreshActionBar = true,
                         showInterstitial = ShowInterstitial.Yes("PAGE_LEADERBOARD".toConstantsClassName())),
                 childComponents = listOf(
                         ChildOptionConfig(listOf(
                                 roundPlayMatchSummaries
                         ),
                                 childFeatureName = "Round Play Match Summaries",
                                 indexed = true),
                         ChildOptionConfig(listOf(
                                 knockOutMatchSummaries
                         ),
                                 childFeatureName = "Knockout Match Summaries",
                                 indexed = false)),
                 bundleArgs = listOf(TourCodeArg, TournamentIdArg))

         FeatureGenerator(destinationData, feature).generateFeature()

This will build 5 MVI units, reference 3 existing ones, build 3 fragments, and set up a heavy amount of dependency injection for the the whole screen. I would highly recommend the approach. My coworkers use it as well and it greatly increased productivity. Here's how the actual generator portion of the module are laid out:

screen shot 2018-03-28 at 4 02 51 pm

dimsuz commented 6 years ago

@jhowens89 ooh, a very interesting approach! How does one actually executes this generator? Is it some gradle task in the same project?

sockeqwe commented 6 years ago

Pretty cool stuff, thanks for sharing. However, I would like to not add this to Mosby repo itself because I think its too much team specific, but I gladly add a link to your template generator in readme.

sevar83 commented 6 years ago

@jhowens89, Hi, may I try your generator as I'm setting up a new project these days?