Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement/navigation architecture #73

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'com.justpinch.androidanalyzer'

android {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<activity
android:name=".ui.activity.MainActivity"
android:theme="@style/AppTheme.Splash"
android:windowSoftInputMode="adjustPan"
>
<nav-graph android:value="@navigation/bottom_nav_graph" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.architectcoders.equipocinco.common

import android.os.Bundle
import androidx.fragment.app.Fragment
import com.architectcoders.equipocinco.R
import com.architectcoders.equipocinco.ui.activity.MainActivity
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavLogger
import com.ncapdevi.fragnav.FragNavSwitchController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.ncapdevi.fragnav.tabhistory.UniqueTabHistoryStrategy
import kotlinx.android.synthetic.main.activity_main.*

class FragmentFrameHelper(private val activity: MainActivity) {
companion object {
const val INDEX_POPULAR = FragNavController.TAB1
const val INDEX_TOP_RATED = FragNavController.TAB2
const val INDEX_FAVOURITE = FragNavController.TAB3
}

private val fragNavController: FragNavController =
FragNavController(activity.supportFragmentManager, R.id.navHostFragment)


fun setupNavController(savedInstanceState: Bundle?) {
fragNavController.apply {
rootFragmentListener = activity
createEager = true
fragNavLogger = object : FragNavLogger {
override fun error(message: String, throwable: Throwable) {
}
}

defaultTransactionOptions = FragNavTransactionOptions.newBuilder().customAnimations(
R.anim.slide_in_from_right,
R.anim.slide_out_to_left,
R.anim.slide_in_from_left,
R.anim.slide_out_to_right
).build()
fragmentHideStrategy = FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH
Copy link
Collaborator Author

@gabrielpozo gabrielpozo Feb 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aqui puedes personalizar la estrategia del stack de fragments, ahora tenemos DETACH_ON_NAVIGATE_HIDE_ON_SWITCH que basicamente significa q en nuestro caso siempre tendria 3 vistas en memorias a la vez, no importa en que nivel de profundidad estes. Por ejemplo: si estas en detailMovie del TAB de popularMovies, tendrías en memoria estas 3, DetailScreen(de popularMovie), TopRatedScreen y FavouriteScreen , y si le das hacia atras desde DetailScreen, tendrias ahora estas vistas PopularMovie, TopRatedScreen y FavouriteScreen en memoria

Copy link
Collaborator Author

@gabrielpozo gabrielpozo Feb 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cuando vas en niveles dentro de un mismo TAB, por ej. desde PopularMovies a detail, se hace un detach del fragment de modo que se destruye la View del fragment de PopularMocies pero queda la instancia del objeto en memoria, y en consecuencia el ViewModel sobrevive tambien conteniendo en sus LiveDatas los ultimos valores. Por eso tuve que hacer un wrapper del object Navigation en un Event en el onSelectedMovie y preguntar si ya se habia consumido anteriormente, de otro modo me volveria de nuevo al detailscreen y no me dejaria ir nunca hacia atras

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brutal! Gracias por la explicación!


navigationStrategy = UniqueTabHistoryStrategy(object : FragNavSwitchController {
override fun switchTab(index: Int, transactionOptions: FragNavTransactionOptions?) {
activity.bottom_navigation_view.selectTabAtPosition(index)
}
})
}

fragNavController.initialize(INDEX_POPULAR, savedInstanceState)
val initial = savedInstanceState == null
if (initial) {
activity.bottom_navigation_view.selectTabAtPosition(INDEX_POPULAR)
}

activity.bottom_navigation_view.setOnTabSelectListener({ tabId ->
when (tabId) {
R.id.menu_popular -> fragNavController.switchTab(INDEX_POPULAR)
R.id.menu_top_rated -> fragNavController.switchTab(INDEX_TOP_RATED)
R.id.menu_favourites -> fragNavController.switchTab(INDEX_FAVOURITE)
}
}, initial)

activity.bottom_navigation_view.setOnTabReselectListener { fragNavController.clearStack() }


}

fun pushFragment(fragment: Fragment) {
fragNavController.pushFragment(fragment)
}

fun onSaveInstanceState(outState: Bundle) {
fragNavController.onSaveInstanceState(outState)
}

fun popFragmentNot(): Boolean = fragNavController.popFragment().not()

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.architectcoders.equipocinco.extensions

import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import com.architectcoders.equipocinco.MovieApplication
import com.architectcoders.equipocinco.ui.fragment.detail.DetailMovieFragment

@Suppress("UNCHECKED_CAST")
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline factory: () -> T): T {
Expand All @@ -18,4 +20,6 @@ inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline factory: ()
}

val Context.app: MovieApplication
get() = applicationContext as MovieApplication
get() = applicationContext as MovieApplication


Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.architectcoders.equipocinco.extensions

import android.os.Bundle
import com.architectcoders.equipocinco.ui.fragment.detail.DetailMovieFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.FavouriteMoviesFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.PopularMoviesFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.TopRatedMoviesFragment

fun PopularMoviesFragment.Companion.newInstance(): PopularMoviesFragment = PopularMoviesFragment()

fun TopRatedMoviesFragment.Companion.newInstance(): TopRatedMoviesFragment = TopRatedMoviesFragment()

fun FavouriteMoviesFragment.Companion.newInstance(): FavouriteMoviesFragment = FavouriteMoviesFragment()

//Detail Movie instance
fun DetailMovieFragment.Companion.newInstance(id: Int): DetailMovieFragment {
val args = Bundle()
args.putInt(MOVIE_ID_KEY, id)
val fragment = DetailMovieFragment()
fragment.arguments = args
return fragment

}




Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
package com.architectcoders.equipocinco.ui.activity

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.Navigation
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.setupWithNavController
import androidx.fragment.app.Fragment
import com.architectcoders.equipocinco.R
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

import com.architectcoders.equipocinco.common.FragmentFrameHelper
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_FAVOURITE
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_POPULAR
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_TOP_RATED
import com.architectcoders.equipocinco.extensions.newInstance
import com.architectcoders.equipocinco.ui.fragment.BaseFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.FavouriteMoviesFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.PopularMoviesFragment
import com.architectcoders.equipocinco.ui.fragment.master.child.TopRatedMoviesFragment
import com.ncapdevi.fragnav.FragNavController

class MainActivity : AppCompatActivity(), FragNavController.RootFragmentListener,
BaseFragment.FragmentNavigation {

private val fragmentHelper = FragmentFrameHelper(this)
private lateinit var activity: MainActivity
override val numberOfRootFragments: Int = 3


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme)
setContentView(R.layout.activity_main)
fragmentHelper.setupNavController(savedInstanceState)
activity = this@MainActivity
setupBottomNavigation()
}

private fun setupBottomNavigation() {
bottom_navigation_view.setupWithNavController(findNavController(R.id.navHostFragment))

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
fragmentHelper.onSaveInstanceState(outState)
}

override fun onSupportNavigateUp() =
Navigation.findNavController(this, R.id.navHostFragment).navigateUp()
}
override fun onBackPressed() {
if (fragmentHelper.popFragmentNot()) {
super.onBackPressed()
}
}

override fun getRootFragment(index: Int): Fragment {
when (index) {
INDEX_POPULAR -> { return PopularMoviesFragment.newInstance() }
INDEX_TOP_RATED -> return TopRatedMoviesFragment.newInstance()
INDEX_FAVOURITE -> return FavouriteMoviesFragment.newInstance()
}
throw IllegalStateException("Need to send an index that we know")
}

override fun pushFragment(fragment: Fragment, sharedElementList: List<Pair<View, String>>?) {
fragmentHelper.pushFragment(fragment)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.architectcoders.equipocinco.ui.fragment

import android.content.Context
import android.view.View
import androidx.fragment.app.Fragment


abstract class BaseFragment : Fragment() {

lateinit var mFragmentNavigation: FragmentNavigation

override fun onAttach(context: Context) {
super.onAttach(context)
if (context is FragmentNavigation) {
mFragmentNavigation = context
}
}

interface FragmentNavigation {
fun pushFragment(fragment: Fragment, sharedElementList: List<Pair<View, String>>? = null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import com.architectcoders.equipocinco.R
import com.architectcoders.equipocinco.databinding.FragmentDetailMovieBinding
import com.architectcoders.equipocinco.di.modules.DetailMovieComponent
import com.architectcoders.equipocinco.di.modules.DetailMovieModule
import com.architectcoders.equipocinco.extensions.app
import com.architectcoders.equipocinco.extensions.getViewModel
import com.architectcoders.equipocinco.ui.fragment.BaseFragment
import com.architectcoders.presentation.viewmodels.DetailMovieViewModel
import kotlinx.android.synthetic.main.fragment_detail_movie.*

class DetailMovieFragment : Fragment() {
class DetailMovieFragment : BaseFragment() {

companion object {
const val MOVIE_ID_KEY = "DetailMovieFragment::id"
}
companion object { const val MOVIE_ID_KEY = "DetailMovieFragment::id" }

private lateinit var component: DetailMovieComponent
private val viewModel: DetailMovieViewModel by lazy { getViewModel { component.detailViewModel } }
Expand Down Expand Up @@ -54,3 +52,4 @@ class DetailMovieFragment : Fragment() {
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.architectcoders.domain.model.Movie
import com.architectcoders.equipocinco.R
Expand All @@ -18,27 +14,27 @@ import com.architectcoders.equipocinco.di.modules.MoviesComponent
import com.architectcoders.equipocinco.di.modules.MoviesModule
import com.architectcoders.equipocinco.extensions.app
import com.architectcoders.equipocinco.extensions.getViewModel
import com.architectcoders.equipocinco.extensions.newInstance
import com.architectcoders.equipocinco.framework.SearchManager
import com.architectcoders.equipocinco.ui.adapter.MovieAdapter
import com.architectcoders.equipocinco.ui.fragment.BaseFragment
import com.architectcoders.equipocinco.ui.fragment.detail.DetailMovieFragment
import com.architectcoders.generic.framework.extension.isFilled
import com.architectcoders.generic.framework.extension.view.setVisibleOrGone
import com.architectcoders.presentation.common.Event
import com.architectcoders.presentation.viewmodels.MovieViewModel
import kotlinx.android.synthetic.main.fragment_movies.*
import kotlinx.android.synthetic.main.progress_bar.*
import kotlinx.android.synthetic.main.search.*

abstract class MoviesFragment : Fragment() {
abstract class MoviesFragment : BaseFragment() {

private lateinit var navController: NavController
private lateinit var coarsePermissionRequester: PermissionRequester

private lateinit var component: MoviesComponent
private var adapter: MovieAdapter? = null

protected val viewModel: MovieViewModel by lazy { getViewModel { component.movieViewModel } }

private var adapter: MovieAdapter? = null

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -47,7 +43,6 @@ abstract class MoviesFragment : Fragment() {
return inflater.inflate(R.layout.fragment_movies, container, false)
}


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Expand All @@ -56,18 +51,22 @@ abstract class MoviesFragment : Fragment() {
} ?: throw Exception("Invalid Activity")


navController = view.findNavController()

coarsePermissionRequester =
PermissionRequester(activity, Manifest.permission.ACCESS_COARSE_LOCATION)

coarsePermissionRequester.request {
viewModel.model.observe(viewLifecycleOwner, Observer(::updateUI))
}

navigationObserver()
initClSearch()
}


private fun navigationObserver() {
viewModel.modelNavigation.observe(viewLifecycleOwner, Observer(::navigationResult))
}

private fun updateUI(model: MovieViewModel.UiModel) {
when (model) {
is MovieViewModel.UiModel.Loading -> pb.show()
Expand All @@ -76,22 +75,20 @@ abstract class MoviesFragment : Fragment() {
}
}

abstract fun onRequestMovies()

private fun updateData(movies: List<Movie>) {
initAdapter(movies)
}

private fun navigationResult(navigationModel: Event<MovieViewModel.NavigationModel>) {
navigationModel.getContentIfNotHandled()?.let { navModel ->
mFragmentNavigation.pushFragment(DetailMovieFragment.newInstance(navModel.movie.id))
}
}

private fun initAdapter(items: List<Movie>) {
rv?.let {
rv.layoutManager = GridLayoutManager(activity, 3)
adapter =
MovieAdapter(items.toMutableList()) {
navController.navigate(
R.id.action_moviesFragment_to_detailMovieFragment,
bundleOf(DetailMovieFragment.MOVIE_ID_KEY to it.id)
)
}
adapter = MovieAdapter(items.toMutableList(), viewModel::onSelectedMovie)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aqui es donde me referia que no rastreabamos la accion del onClick en una peli. Ahora si que le pasamos al viewModel la accion(onSelectedMovie) y de esta manera ya podemos testear este logica-flow

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mola!

rv.adapter = adapter
pb.hide()
}
Expand All @@ -114,4 +111,7 @@ abstract class MoviesFragment : Fragment() {
private fun searchMovies(query: String) {
viewModel.onSearchMovies(query)
}

abstract fun onRequestMovies()

}
Loading