├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── FragNavDemo.gif
├── README.md
├── UniqueHistory.gif
├── UnlimitedHistory.gif
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── ncapdevi
│ │ │ └── sample
│ │ │ ├── activities
│ │ │ ├── BottomTabsActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── NavDrawerActivity.kt
│ │ │ └── fragments
│ │ │ ├── BaseFragment.kt
│ │ │ ├── FavoritesFragment.kt
│ │ │ ├── FoodFragment.kt
│ │ │ ├── FriendsFragment.kt
│ │ │ ├── NearbyFragment.kt
│ │ │ └── RecentsFragment.kt
│ └── res
│ │ ├── anim
│ │ ├── slide_in_from_left.xml
│ │ ├── slide_in_from_right.xml
│ │ ├── slide_out_to_left.xml
│ │ └── slide_out_to_right.xml
│ │ ├── drawable-hdpi
│ │ ├── ic_favorites.png
│ │ ├── ic_friends.png
│ │ ├── ic_nearby.png
│ │ ├── ic_recents.png
│ │ └── ic_restaurants.png
│ │ ├── drawable-mdpi
│ │ ├── ic_favorites.png
│ │ ├── ic_friends.png
│ │ ├── ic_nearby.png
│ │ ├── ic_recents.png
│ │ └── ic_restaurants.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_favorites.png
│ │ ├── ic_friends.png
│ │ ├── ic_nearby.png
│ │ ├── ic_recents.png
│ │ └── ic_restaurants.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_favorites.png
│ │ ├── ic_friends.png
│ │ ├── ic_nearby.png
│ │ ├── ic_recents.png
│ │ └── ic_restaurants.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_favorites.png
│ │ ├── ic_friends.png
│ │ ├── ic_nearby.png
│ │ ├── ic_recents.png
│ │ └── ic_restaurants.png
│ │ ├── drawable
│ │ └── side_nav_bar.xml
│ │ ├── layout
│ │ ├── activity_bottom_tabs.xml
│ │ ├── activity_main.xml
│ │ ├── activity_nav_drawer.xml
│ │ ├── app_bar_nav_drawer.xml
│ │ ├── content_nav_drawer.xml
│ │ ├── fragment_main.xml
│ │ └── nav_header_nav_drawer.xml
│ │ ├── menu
│ │ └── activity_nav_drawer_drawer.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values-v21
│ │ └── styles.xml
│ │ ├── values-w820dp
│ │ └── dimens.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── menu_bottombar.xml
│ └── test
│ └── java
│ └── com
│ └── ncapdevi
│ └── sample
│ └── AppUnitTests.java
├── build.gradle
├── frag-nav
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── ncapdevi
│ │ │ └── fragnav
│ │ │ ├── FragNavController.kt
│ │ │ ├── FragNavLogger.kt
│ │ │ ├── FragNavPopController.kt
│ │ │ ├── FragNavSwitchController.kt
│ │ │ ├── FragNavTransactionOptions.kt
│ │ │ └── tabhistory
│ │ │ ├── BaseFragNavTabHistoryController.kt
│ │ │ ├── CollectionFragNavTabHistoryController.kt
│ │ │ ├── CurrentTabHistoryController.kt
│ │ │ ├── FragNavTabHistoryController.kt
│ │ │ ├── NavigationStrategy.kt
│ │ │ ├── UniqueTabHistoryController.kt
│ │ │ └── UnlimitedTabHistoryController.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ ├── java
│ └── com
│ │ └── ncapdevi
│ │ └── fragnav
│ │ ├── FragNavControllerRaceConditionSpec.kt
│ │ ├── FragNavControllerTest.kt
│ │ ├── FragNavTransactionOptionsTest.kt
│ │ └── tabhistory
│ │ ├── CurrentTabHistoryControllerTest.kt
│ │ ├── UniqueTabHistoryControllerTest.kt
│ │ └── UnlimitedTabHistoryControllerTest.kt
│ └── resources
│ └── mockito-extensions
│ └── org.mockito.plugins.MockMaker
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .idea
8 |
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 | before_cache:
3 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
4 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
5 | cache:
6 | directories:
7 | - $HOME/.gradle/caches/
8 | - $HOME/.gradle/wrapper/
9 | - $HOME/.android/build-cache
10 |
11 | android:
12 | components:
13 | # use the latest revision of Android SDK Tools
14 | - tools
15 | - platform-tools
16 | - tools
17 | # The BuildTools version used by your project
18 | - build-tools-27.0.3
19 | # The SDK version used to compile your project
20 | - android-27
21 | # Additional components
22 | - extra-google-m2repository
23 | - extra-android-m2repository
24 | - extra-android-support
25 |
26 | licenses:
27 | - 'android-sdk-preview-license-.+'
28 | - 'android-sdk-license-.+'
29 | - 'google-gdk-license-.+'
30 |
31 | jdk:
32 | - oraclejdk8
33 |
34 | notifications:
35 | email: false
36 |
37 | install:
38 | # List and delete unnecessary components to free space
39 | - sdkmanager --list || true
40 | - sdkmanager --uninstall "system-images;android-15;default;armeabi-v7a"
41 | - sdkmanager --uninstall "system-images;android-16;default;armeabi-v7a"
42 | - sdkmanager --uninstall "system-images;android-17;default;armeabi-v7a"
43 | - sdkmanager --uninstall "system-images;android-18;default;armeabi-v7a"
44 | - sdkmanager --uninstall "system-images;android-19;default;armeabi-v7a"
45 | - sdkmanager --uninstall "system-images;android-21;default;armeabi-v7a"
46 | - sdkmanager --uninstall "extras;google;google_play_services"
47 | - sdkmanager --uninstall "extras;android;support"
48 | - sdkmanager --uninstall "platforms;android-10"
49 | - sdkmanager --uninstall "platforms;android-15"
50 | - sdkmanager --uninstall "platforms;android-16"
51 | - sdkmanager --uninstall "platforms;android-17"
52 | - sdkmanager --uninstall "platforms;android-18"
53 | - sdkmanager --uninstall "platforms;android-19"
54 | - sdkmanager --uninstall "platforms;android-20"
55 | - sdkmanager --uninstall "platforms;android-21"
56 | - sdkmanager --uninstall "platforms;android-22"
57 | - sdkmanager --uninstall "platforms;android-23"
58 | - sdkmanager --uninstall "platforms;android-24"
59 | - sdkmanager --uninstall "platforms;android-25"
60 | - sdkmanager --uninstall "platforms;android-26"
61 | - sdkmanager --uninstall "build-tools;25.0.2"
62 | # Update sdk tools to latest version and install/update components
63 | - echo yes | sdkmanager "tools"
64 | - echo yes | sdkmanager "platforms;android-27" # Latest platform required by SDK tools
65 | - echo yes | sdkmanager "extras;android;m2repository"
66 | - echo yes | sdkmanager "extras;google;m2repository"
67 |
68 | script:
69 | - ./gradlew jacocoTestReportDebug coveralls
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | All changelogs are in the release section
2 |
3 | https://github.com/ncapdevi/FragNav/releases
4 |
--------------------------------------------------------------------------------
/FragNavDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/FragNavDemo.gif
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/ncapdevi/FragNav)
2 |
3 | # FragNav
4 | Android library for managing multiple stacks of fragments (e.g., [Bottom Navigation ](https://www.google.com/design/spec/components/bottom-navigation.html), [Navigation Drawer](https://www.google.com/design/spec/patterns/navigation-drawer.html)). This library does NOT include the UI for bottom tab bar layout. For that, I recommend either [BottomBar](https://github.com/roughike/BottomBar) (which is the library shown in the demo) or [AHBottomNavigation](https://github.com/aurelhubert/ahbottomnavigation). This library helps maintain order after pushing onto and popping from multiple stacks(tabs). It also helps with switching between desired tabs and clearing the stacks.
5 |
6 |
7 |
8 |
9 | ## Donations
10 | Did I help you out, save you some time, make your life easier? Oh, cool. Want to say thanks, buy me a coffee or a beer? HEY THANKS! I appreciate it.
11 |
12 | [Cash App](https://cash.me/$NicCapdevila)
13 |
14 | [Paypal](https://paypal.me/ncapdevi)
15 |
16 | [Venmo](http://www.venmo.com/code?user_id=1774909453762560167)
17 |
18 | ## Restrictions
19 | Fragments are maintained in a stack per Android's guideline https://developer.android.com/guide/navigation/navigation-principles#navigation_state_is_represented_as_a_stack_of_destinations . A lot of questions get asked about how to maintain only one instance of a fragment, or to pull out a fragment in the middle of the stack. That is outside Android navigation guidelines, and also this library. You may want to rethink your UX.
20 |
21 |
22 | ## Sample
23 | With [Material Design Bottom Navigation pattern](https://www.google.com/design/spec/components/bottom-navigation.html), and other tabbed navigation, managing multiple stacks of fragments can be a real headache. The example file shows best practice for navigating deep within a tab stack.
24 |
25 | ## Gradle
26 |
27 | ```groovy
28 | implementation 'com.ncapdevi:frag-nav:3.2.0' //or or `compile` if on older gradle version
29 | ```
30 |
31 | ## How do I implement it?
32 |
33 | ### Initialize using a builder and one of two methods
34 | ```kotlin
35 | private val fragNavController: FragNavController = FragNavController(supportFragmentManager, R.id.container)
36 | ```
37 | #### 1.
38 | Create a list of fragments and pass them in
39 | ```kotlin
40 | val fragments = listOf(
41 | RecentsFragment.newInstance(0),
42 | FavoritesFragment.newInstance(0),
43 | NearbyFragment.newInstance(0),
44 | FriendsFragment.newInstance(0),
45 | FoodFragment.newInstance(0),
46 | RecentsFragment.newInstance(0),
47 | FavoritesFragment.newInstance(0),
48 | NearbyFragment.newInstance(0),
49 | FriendsFragment.newInstance(0),
50 | FoodFragment.newInstance(0),
51 | RecentsFragment.newInstance(0),
52 | FavoritesFragment.newInstance(0))
53 |
54 | fragNavController.rootFragments = fragments
55 | ```
56 |
57 | #### 2.
58 |
59 |
60 | Allow for dynamically creating the base class by implementing the NavListener in your class and overriding the getRootFragment method
61 |
62 | ```java
63 | public class YourActivity extends AppCompatActivity implements FragNavController.RootFragmentListener {
64 | ```
65 |
66 | ```java
67 | fragNavController.rootFragmentListener = this
68 | ```
69 |
70 | ```kotlin
71 |
72 | override val numberOfRootFragments: Int = 5
73 |
74 | override fun getRootFragment(index: Int): Fragment {
75 | when (index) {
76 | INDEX_RECENTS -> return RecentsFragment.newInstance(0)
77 | INDEX_FAVORITES -> return FavoritesFragment.newInstance(0)
78 | INDEX_NEARBY -> return NearbyFragment.newInstance(0)
79 | INDEX_FRIENDS -> return FriendsFragment.newInstance(0)
80 | INDEX_FOOD -> return FoodFragment.newInstance(0)
81 | }
82 | throw IllegalStateException("Need to send an index that we know")
83 | }
84 |
85 | ```
86 |
87 | #### 3.
88 | ```java
89 | fragNavController.initialize(FragNavController.TAB3, savedInstanceState)
90 | ```
91 |
92 | ### SaveInstanceState
93 |
94 | Send in the supportFragment Manager, a list of base fragments, the container that you'll be using to display fragments.
95 | After that, you have four main functions that you can use
96 | In your activity, you'll also want to override your onSaveInstanceState like so
97 |
98 | ```kotlin
99 | override fun onSaveInstanceState(outState: Bundle?) {
100 | super.onSaveInstanceState(outState)
101 | fragNavController.onSaveInstanceState(outState!!)
102 |
103 | }
104 | ```
105 |
106 | ### Switch tabs
107 | Tab switching is indexed to try to prevent you from sending in wrong indices. It also will throw an error if you try to switch to a tab you haven't defined a base fragment for.
108 |
109 | ```java
110 | fragNavController.switchTab(NavController.TAB1);
111 | ```
112 |
113 | ### Push a fragment
114 | You can only push onto the currently selected index
115 | ```java
116 | fragNavController.pushFragment(FoodFragment.newInstance())
117 | ```
118 |
119 | ### Pop a fragment
120 | You can only pop from the currently selected index. This can throw an UnsupportedOperationException if trying to pop the root fragment
121 | ```java
122 | fragNavController.popFragment();
123 | ```
124 | ### Pop multiple fragments
125 | You can pop multiple fragments at once, with the same rules as above applying. If the pop depth is deeper than possible, it will stop when it gets to the root fragment
126 | ```java
127 | fragNavController.popFragments(3);
128 | ```
129 | ### Replacing a fragment
130 | You can only replace onto the currently selected index
131 | ```java
132 | fragNavController.replaceFragment(Fragment fragment);
133 | ```
134 | ### You can also clear the stack to bring you back to the base fragment
135 | ```java
136 | fragNavController.clearStack();
137 | ```
138 | ### You can also navigate your DialogFragments using
139 | ```java
140 | showDialogFragment(dialogFragment);
141 | clearDialogFragment();
142 | getCurrentDialogFrag()
143 | ```
144 |
145 | ### Transaction Options
146 | All of the above transactions can also be done with defined transaction options.
147 | The FragNavTransactionOptions have a builder that can be used.
148 | ```kotlin
149 | class FragNavTransactionOptions private constructor(builder: Builder) {
150 | val sharedElements: List> = builder.sharedElements
151 | @FragNavController.Transit
152 | val transition = builder.transition
153 | @AnimRes
154 | val enterAnimation = builder.enterAnimation
155 | @AnimRes
156 | val exitAnimation = builder.exitAnimation
157 | @AnimRes
158 | val popEnterAnimation = builder.popEnterAnimation
159 | @AnimRes
160 | val popExitAnimation = builder.popExitAnimation
161 | @StyleRes
162 | val transitionStyle = builder.transitionStyle
163 | val breadCrumbTitle: String? = builder.breadCrumbTitle
164 | val breadCrumbShortTitle: String? = builder.breadCrumbShortTitle
165 | val allowStateLoss: Boolean = builder.allowStateLoss
166 |
167 | ```
168 |
169 | ### Get informed of fragment transactions
170 | Have your activity implement FragNavController.TransactionListener
171 | and you will have methods that inform you of tab switches or fragment transactions
172 |
173 | A sample application is in the repo if you need to see how it works.
174 |
175 | ### Fragment Transitions
176 |
177 | Can be set using the transactionOptions
178 |
179 |
180 |
181 | ## Restoring Fragment State
182 | Fragments transitions in this library use attach()/detch() (http://daniel-codes.blogspot.com/2012/06/fragment-transactions-reference.html). This is a delibrate choice in order to maintain the fragment's lifecycle, as well as being optimal for memory. This means that fragments will go through their proper lifecycle https://developer.android.com/guide/components/fragments.html#Lifecycle . This lifecycle includes going through `OnCreateView` which means that if you want to maintain view states, that is outside the scope of this library, and is up to the indiviudal fragment. There are plenty of resources out there that will help you design your fragments in such a way that their view state can be restored https://inthecheesefactory.com/blog/fragment-state-saving-best-practices/en and there are libraries that can help restore other states https://github.com/frankiesardo/icepick
183 |
184 | ## Special Use Cases
185 |
186 | ### History & Back navigation between tabs
187 |
188 | The reason behind this feature is that many of the "big" apps out there has a fairly similar approach for handling back navigation. When the user starts to tap the back button the current tab's fragments are being thrown away (FragNav default configuration does this too). The more interesting part comes when the user reaches the "root" fragment of the current tab. At this point there are several approaches that we can choose:
189 |
190 | - Nothing happens on further back button taps - **This is the default**
191 | - FragNav tracks "Tab History" and send a tab switch signal and we navigate back in history to the previous tab.
192 |
193 | To use the history keeping mode you'll have to add extra parameters to the builder:
194 |
195 | ```java
196 | mNavController = FragNavController.newBuilder(...)
197 | ...
198 | .switchController(FragNavTabHistoryController.UNLIMITED_TAB_HISTORY, new FragNavSwitchController() {
199 | @Override
200 | public void switchTab(int index, @Nullable FragNavTransactionOptions transactionOptions) {
201 | bottomBar.selectTabAtPosition(index);
202 | }
203 | })
204 | .build();
205 | ```
206 |
207 | Here first we have to choose between two flavors (see below for details), then we'll have to provide a callback that handles the tab switch trigger (This is required so that our UI element that also contain the state of the selected tab can update itself - aka switching the tabs always triggered by the application never by FragNav).
208 |
209 | | UNLIMITED_TAB_HISTORY | UNIQUE_TAB_HISTORY |
210 | | :--------------------------------------: | :----------------------------------: |
211 | |  |  |
212 |
213 | ### Show & Hide modes for fragment "replacement"
214 |
215 | While having a good architecture and caching most of the data that is presented on a page makes attaching / detaching the fragments when switching pretty seamless there may be some cases where even a small glitch or slowdown can be bothering for the user. Let's assume a virtualized list with couple of hundred items, even if the attach is pretty fast and the data is available rebuilding all the cell items for the list is not immediate and user might see some loading / white screen. To optimize the experience we introduced 3 different possibility:
216 |
217 | - Using attach and detach for both opening new fragments on the current stack and switching between tabs - **This is the default** - *DETACH*
218 |
219 | - Using attach and detach for opening new fragments on the current stack and using show and hide for switching between tabs - *DETACH_ON_NAVIGATE_HIDE_ON_SWITCH*
220 |
221 | Having this setting we have a good balance between memory consumption and user experience. (we have at most as many fragment UI in the memory as the number of our tabs)
222 |
223 | - Using Fragment show and hide for both opening new fragments on the current stack and switching between tabs - *HIDE*
224 |
225 | This gives the best performance keeping all fragments in the memory so we won't have to wait for the rebuilding of them. However with many tabs and deep navigation stacks this can lead easily to memory consumption issues.
226 |
227 | **WARNING** - Keep in mind that using **show and hide does not trigger the usual lifecycle events** of the fragments so app developer has to manually take care of handling state which is usually done in the Fragment onPause/Stop and onResume/Start methods.
228 |
229 | ```java
230 | mNavController = FragNavController.newBuilder(...)
231 | ...
232 | .fragmentHideStrategy(FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH)
233 | .eager(true)
234 | .build();
235 | ```
236 |
237 | There is also a possibility to automatically add and inflate all the root fragments right after creation (This makes sense only using *HIDE* and *DETACH_ON_NAVIGATE_HIDE_ON_SWITCH* modes). To have this you should set "eager" mode to true on the builder (Default is false).
238 |
239 | ## Apps Using FragNav
240 |
241 | Feel free to send me a pull request with your app and I'll link you here:
242 |
243 | | Logo | Name | Play Store |
244 | | ---------------------------------------- | ---------- | ---------------------------------------- |
245 | |
| Rockbot DJ |
|
246 | |
| Rockbot Remote |
|
247 | |
| Skyscanner |
|
248 | |
| Fonic / Fonic Mobile |
|
249 | |
| Just Expenses |
|
250 |
251 | ## Contributions
252 | If you have any problems, feel free to create an issue or pull request.
253 |
254 | The sample app in the repository uses [BottomBar](https://github.com/roughike/BottomBar) library.
255 |
256 |
257 |
258 | ## License
259 |
260 | ```
261 | FragNav Android Fragment Navigation Library
262 | Copyright (c) 2016 Nic Capdevila (http://github.com/ncapdevi).
263 |
264 | Licensed under the Apache License, Version 2.0 (the "License");
265 | you may not use this file except in compliance with the License.
266 | You may obtain a copy of the License at
267 |
268 | http://www.apache.org/licenses/LICENSE-2.0
269 |
270 | Unless required by applicable law or agreed to in writing, software
271 | distributed under the License is distributed on an "AS IS" BASIS,
272 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
273 | See the License for the specific language governing permissions and
274 | limitations under the License.
275 | ```
276 |
--------------------------------------------------------------------------------
/UniqueHistory.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/UniqueHistory.gif
--------------------------------------------------------------------------------
/UnlimitedHistory.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/UnlimitedHistory.gif
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion rootProject.ext.compileSdkVersion
6 | buildToolsVersion rootProject.ext.buildToolsVersion
7 | lintOptions {
8 | abortOnError false
9 | }
10 | defaultConfig {
11 | applicationId "com.ncapdevi.sample"
12 | minSdkVersion rootProject.ext.minSdkVersion
13 | targetSdkVersion rootProject.ext.compileSdkVersion
14 | versionCode 1
15 | versionName "1.0"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 | repositories {
25 | jcenter()
26 | }
27 |
28 | dependencies {
29 | implementation project(':frag-nav')
30 | implementation "com.google.android.material:material:1.0.0"
31 | implementation "androidx.appcompat:appcompat:1.0.2"
32 | implementation "com.roughike:bottom-bar:2.3.1"
33 | testImplementation "junit:junit:$rootProject.ext.junitVersion"
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
35 | }
36 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/niccapdevila/.android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/activities/BottomTabsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.activities
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.util.Log
7 | import android.view.MenuItem
8 | import android.view.View
9 | import com.ncapdevi.fragnav.FragNavController
10 | import com.ncapdevi.fragnav.FragNavLogger
11 | import com.ncapdevi.fragnav.FragNavSwitchController
12 | import com.ncapdevi.fragnav.FragNavTransactionOptions
13 | import com.ncapdevi.fragnav.tabhistory.UniqueTabHistoryStrategy
14 | import com.ncapdevi.sample.R
15 | import com.ncapdevi.sample.fragments.*
16 | import com.roughike.bottombar.BottomBar
17 |
18 | class BottomTabsActivity : AppCompatActivity(), BaseFragment.FragmentNavigation, FragNavController.TransactionListener, FragNavController.RootFragmentListener {
19 | override val numberOfRootFragments: Int = 5
20 |
21 | private val fragNavController: FragNavController = FragNavController(supportFragmentManager, R.id.container)
22 |
23 | private lateinit var bottomBar: BottomBar
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_bottom_tabs)
28 |
29 | bottomBar = findViewById(R.id.bottomBar)
30 |
31 | fragNavController.apply {
32 | transactionListener = this@BottomTabsActivity
33 | rootFragmentListener = this@BottomTabsActivity
34 | createEager = true
35 | fragNavLogger = object : FragNavLogger {
36 | override fun error(message: String, throwable: Throwable) {
37 | Log.e(TAG, message, throwable)
38 | }
39 | }
40 |
41 | 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()
42 | fragmentHideStrategy = FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH
43 |
44 | navigationStrategy = UniqueTabHistoryStrategy(object : FragNavSwitchController {
45 | override fun switchTab(index: Int, transactionOptions: FragNavTransactionOptions?) {
46 | bottomBar.selectTabAtPosition(index)
47 | }
48 | })
49 | }
50 |
51 | fragNavController.initialize(INDEX_NEARBY, savedInstanceState)
52 |
53 | val initial = savedInstanceState == null
54 | if (initial) {
55 | bottomBar.selectTabAtPosition(INDEX_NEARBY)
56 | }
57 |
58 | bottomBar.setOnTabSelectListener({ tabId ->
59 | when (tabId) {
60 | R.id.bb_menu_recents -> fragNavController.switchTab(INDEX_RECENTS)
61 | R.id.bb_menu_favorites -> fragNavController.switchTab(INDEX_FAVORITES)
62 | R.id.bb_menu_nearby -> fragNavController.switchTab(INDEX_NEARBY)
63 | R.id.bb_menu_friends -> fragNavController.switchTab(INDEX_FRIENDS)
64 | R.id.bb_menu_food -> fragNavController.switchTab(INDEX_FOOD)
65 | }
66 | }, initial)
67 |
68 | bottomBar.setOnTabReselectListener { fragNavController.clearStack() }
69 | }
70 |
71 | override fun onBackPressed() {
72 | if (fragNavController.popFragment().not()) {
73 | super.onBackPressed()
74 | }
75 | }
76 |
77 | override fun onSaveInstanceState(outState: Bundle?) {
78 | super.onSaveInstanceState(outState)
79 | fragNavController.onSaveInstanceState(outState!!)
80 |
81 | }
82 |
83 | override fun pushFragment(fragment: Fragment, sharedElementList: List>?) {
84 | val options = FragNavTransactionOptions.newBuilder()
85 | options.reordering = true
86 | sharedElementList?.let {
87 | it.forEach { pair ->
88 | options.addSharedElement(pair)
89 | }
90 | }
91 | fragNavController.pushFragment(fragment, options.build())
92 |
93 | }
94 |
95 | override fun onTabTransaction(fragment: Fragment?, index: Int) {
96 | // If we have a backstack, show the back button
97 | supportActionBar?.setDisplayHomeAsUpEnabled(fragNavController.isRootFragment.not())
98 |
99 | }
100 |
101 |
102 | override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) {
103 | //do fragmentty stuff. Maybe change title, I'm not going to tell you how to live your life
104 | // If we have a backstack, show the back button
105 | supportActionBar?.setDisplayHomeAsUpEnabled(fragNavController.isRootFragment.not())
106 |
107 | }
108 |
109 | override fun getRootFragment(index: Int): Fragment {
110 | when (index) {
111 | INDEX_RECENTS -> return RecentsFragment.newInstance(0)
112 | INDEX_FAVORITES -> return FavoritesFragment.newInstance(0)
113 | INDEX_NEARBY -> return NearbyFragment.newInstance(0)
114 | INDEX_FRIENDS -> return FriendsFragment.newInstance(0)
115 | INDEX_FOOD -> return FoodFragment.newInstance(0)
116 | }
117 | throw IllegalStateException("Need to send an index that we know")
118 | }
119 |
120 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
121 | when (item.itemId) {
122 | android.R.id.home -> fragNavController.popFragment()
123 | }
124 | return true
125 | }
126 |
127 | companion object {
128 | private val TAG = BottomTabsActivity::class.java.simpleName
129 | }
130 | }
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/activities/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.activities
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.view.View
7 | import android.widget.Button
8 | import com.ncapdevi.fragnav.FragNavController
9 |
10 | import com.ncapdevi.sample.R
11 |
12 |
13 | //Better convention to properly name the indices what they are in your app
14 | const val INDEX_RECENTS = FragNavController.TAB1
15 | const val INDEX_FAVORITES = FragNavController.TAB2
16 | const val INDEX_NEARBY = FragNavController.TAB3
17 | const val INDEX_FRIENDS = FragNavController.TAB4
18 | const val INDEX_FOOD = FragNavController.TAB5
19 | const val INDEX_RECENTS2 = FragNavController.TAB6
20 | const val INDEX_FAVORITES2 = FragNavController.TAB7
21 | const val INDEX_NEARBY2 = FragNavController.TAB8
22 | const val INDEX_FRIENDS2 = FragNavController.TAB9
23 | const val INDEX_FOOD2 = FragNavController.TAB10
24 | const val INDEX_RECENTS3 = FragNavController.TAB11
25 | const val INDEX_FAVORITES3 = FragNavController.TAB12
26 |
27 | class MainActivity : AppCompatActivity() {
28 |
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | setContentView(com.ncapdevi.sample.R.layout.activity_main)
33 |
34 | val btnBottomTabs = findViewById(R.id.btnBottomTabs) as Button
35 | btnBottomTabs.setOnClickListener { startActivity(Intent(this@MainActivity, BottomTabsActivity::class.java)) }
36 |
37 | val btnNavDrawer = findViewById(R.id.btnNavDrawer) as Button
38 | btnNavDrawer.setOnClickListener { startActivity(Intent(this@MainActivity, NavDrawerActivity::class.java)) }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/activities/NavDrawerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.activities
2 |
3 | import android.os.Bundle
4 | import com.google.android.material.navigation.NavigationView
5 | import androidx.fragment.app.Fragment
6 | import androidx.core.view.GravityCompat
7 | import androidx.drawerlayout.widget.DrawerLayout
8 | import androidx.appcompat.app.ActionBarDrawerToggle
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.appcompat.widget.Toolbar
11 | import android.view.MenuItem
12 | import android.view.View
13 | import com.ncapdevi.fragnav.FragNavController
14 | import com.ncapdevi.fragnav.FragNavTransactionOptions
15 | import com.ncapdevi.sample.R
16 | import com.ncapdevi.sample.fragments.*
17 |
18 |
19 | class NavDrawerActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, BaseFragment.FragmentNavigation {
20 | //Better convention to properly name the indices what they are in your app
21 |
22 |
23 | private var fragNavController: FragNavController = FragNavController(supportFragmentManager, R.id.container)
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_nav_drawer)
28 | val toolbar = findViewById(R.id.toolbar)
29 | setSupportActionBar(toolbar)
30 |
31 | val drawer = findViewById(R.id.drawer_layout)
32 | val toggle = ActionBarDrawerToggle(
33 | this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
34 | drawer.addDrawerListener(toggle)
35 |
36 | toggle.syncState()
37 |
38 | val navigationView = findViewById(R.id.nav_view)
39 | navigationView.setNavigationItemSelectedListener(this)
40 |
41 | val fragments = listOf(
42 | RecentsFragment.newInstance(0),
43 | FavoritesFragment.newInstance(0),
44 | NearbyFragment.newInstance(0),
45 | FriendsFragment.newInstance(0),
46 | FoodFragment.newInstance(0),
47 | RecentsFragment.newInstance(0),
48 | FavoritesFragment.newInstance(0),
49 | NearbyFragment.newInstance(0),
50 | FriendsFragment.newInstance(0),
51 | FoodFragment.newInstance(0),
52 | RecentsFragment.newInstance(0),
53 | FavoritesFragment.newInstance(0))
54 |
55 | fragNavController.apply {
56 | rootFragments = fragments
57 | 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()
58 | }
59 |
60 | fragNavController.initialize(INDEX_RECENTS,savedInstanceState)
61 |
62 |
63 | }
64 |
65 | override fun onBackPressed() {
66 | val drawer = findViewById(R.id.drawer_layout)
67 | when {
68 | drawer.isDrawerOpen(GravityCompat.START) -> drawer.closeDrawer(GravityCompat.START)
69 | fragNavController.isRootFragment.not() -> fragNavController.popFragment()
70 | else -> super.onBackPressed()
71 | }
72 | }
73 |
74 | override fun onSaveInstanceState(outState: Bundle?) {
75 | super.onSaveInstanceState(outState)
76 | fragNavController.onSaveInstanceState(outState)
77 | }
78 |
79 | override fun onNavigationItemSelected(item: MenuItem): Boolean {
80 | when (item.itemId) {
81 | R.id.bb_menu_recents -> fragNavController.switchTab(INDEX_RECENTS)
82 | R.id.bb_menu_favorites -> fragNavController.switchTab(INDEX_FAVORITES)
83 | R.id.bb_menu_nearby -> fragNavController.switchTab(INDEX_NEARBY)
84 | R.id.bb_menu_friends -> fragNavController.switchTab(INDEX_FRIENDS)
85 | R.id.bb_menu_food -> fragNavController.switchTab(INDEX_FOOD)
86 | R.id.bb_menu_recents2 -> fragNavController.switchTab(INDEX_RECENTS2)
87 | R.id.bb_menu_favorites2 -> fragNavController.switchTab(INDEX_FAVORITES2)
88 | R.id.bb_menu_nearby2 -> fragNavController.switchTab(INDEX_NEARBY2)
89 | R.id.bb_menu_friends2 -> fragNavController.switchTab(INDEX_FRIENDS2)
90 | R.id.bb_menu_food2 -> fragNavController.switchTab(INDEX_FOOD2)
91 | R.id.bb_menu_recents3 -> fragNavController.switchTab(INDEX_RECENTS3)
92 | R.id.bb_menu_favorites3 -> fragNavController.switchTab(INDEX_FAVORITES3)
93 | }
94 | val drawer = findViewById(R.id.drawer_layout)
95 | drawer.closeDrawer(GravityCompat.START)
96 | return true
97 | }
98 |
99 | override fun pushFragment(fragment: Fragment, sharedList: List>?) {
100 | fragNavController.pushFragment(fragment)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.Button
10 |
11 | import com.ncapdevi.sample.R
12 |
13 | /**
14 | * Created by niccapdevila on 3/26/16.
15 | */
16 | open class BaseFragment : Fragment() {
17 |
18 | lateinit var btn: Button
19 | lateinit var mFragmentNavigation: FragmentNavigation
20 | internal var mInt = 0
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | val args = arguments
25 | if (args != null) {
26 | mInt = args.getInt(ARGS_INSTANCE)
27 | }
28 | }
29 |
30 |
31 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
32 | return inflater.inflate(R.layout.fragment_main, container, false)?.apply {
33 | btn = findViewById(R.id.button)
34 | }
35 | }
36 |
37 | override fun onAttach(context: Context?) {
38 | super.onAttach(context)
39 | if (context is FragmentNavigation) {
40 | mFragmentNavigation = context
41 | }
42 | }
43 |
44 | interface FragmentNavigation {
45 | fun pushFragment(fragment: Fragment, sharedElementList: List>?= null)
46 | }
47 |
48 | companion object {
49 | const val ARGS_INSTANCE = "com.ncapdevi.sample.argsInstance"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/FavoritesFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.transition.TransitionInflater
7 | import android.view.View
8 | import android.widget.CheckBox
9 | import android.widget.EditText
10 | import com.ncapdevi.sample.R
11 |
12 | /**
13 | * Created by niccapdevila on 3/26/16.
14 | */
15 | class FavoritesFragment : BaseFragment() {
16 |
17 | @SuppressLint("SetTextI18n")
18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
19 | super.onViewCreated(view, savedInstanceState)
20 |
21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
22 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
23 | sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(
24 | android.R.transition.fade
25 | )
26 | }
27 | }
28 |
29 | val et = view.findViewById(R.id.edit_text)
30 | val cb = view.findViewById(R.id.checkbox)
31 |
32 | val list = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
33 | listOf>(
34 | Pair(et, et.transitionName),
35 | Pair(cb, cb.transitionName)
36 | )
37 | } else {
38 | listOf()
39 | }
40 |
41 | btn.setOnClickListener {
42 | mFragmentNavigation.pushFragment(newInstance(mInt + 1), list)
43 | }
44 | btn.text = """${javaClass.simpleName} $mInt"""
45 | }
46 |
47 | companion object {
48 |
49 | fun newInstance(instance: Int): FavoritesFragment {
50 | val args = Bundle()
51 | args.putInt(BaseFragment.ARGS_INSTANCE, instance)
52 | val fragment = FavoritesFragment()
53 | fragment.arguments = args
54 | return fragment
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/FoodFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.View
6 |
7 | /**
8 | * Created by niccapdevila on 3/26/16.
9 | */
10 | class FoodFragment : BaseFragment() {
11 |
12 | @SuppressLint("SetTextI18n")
13 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
14 | super.onViewCreated(view, savedInstanceState)
15 | btn.setOnClickListener {
16 | mFragmentNavigation.pushFragment(FoodFragment.newInstance(mInt + 1))
17 | }
18 | btn.text = """${javaClass.simpleName} $mInt"""
19 | }
20 |
21 | companion object {
22 |
23 | fun newInstance(instance: Int): FoodFragment {
24 | val args = Bundle()
25 | args.putInt(BaseFragment.ARGS_INSTANCE, instance)
26 | val fragment = FoodFragment()
27 | fragment.arguments = args
28 | return fragment
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/FriendsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.View
6 |
7 | /**
8 | * Created by niccapdevila on 3/26/16.
9 | */
10 | class FriendsFragment : BaseFragment() {
11 |
12 |
13 | @SuppressLint("SetTextI18n")
14 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
15 | super.onViewCreated(view, savedInstanceState)
16 | btn.setOnClickListener {
17 | mFragmentNavigation.pushFragment(FriendsFragment.newInstance(mInt + 1))
18 | }
19 | btn.text = """${javaClass.simpleName} $mInt"""
20 | }
21 |
22 | companion object {
23 |
24 | fun newInstance(instance: Int): FriendsFragment {
25 | val args = Bundle()
26 | args.putInt(BaseFragment.ARGS_INSTANCE, instance)
27 | val fragment = FriendsFragment()
28 | fragment.arguments = args
29 | return fragment
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/NearbyFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.View
6 |
7 | /**
8 | * Created by niccapdevila on 3/26/16.
9 | */
10 | class NearbyFragment : BaseFragment() {
11 |
12 | @SuppressLint("SetTextI18n")
13 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
14 | super.onViewCreated(view, savedInstanceState)
15 | btn.setOnClickListener {
16 | mFragmentNavigation.pushFragment(NearbyFragment.newInstance(mInt + 1))
17 | }
18 | btn.text = """${javaClass.simpleName} $mInt"""
19 | }
20 |
21 | companion object {
22 |
23 | fun newInstance(instance: Int): NearbyFragment {
24 | val args = Bundle()
25 | args.putInt(BaseFragment.ARGS_INSTANCE, instance)
26 | val fragment = NearbyFragment()
27 | fragment.arguments = args
28 | return fragment
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ncapdevi/sample/fragments/RecentsFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample.fragments
2 |
3 | import android.annotation.SuppressLint
4 | import android.os.Bundle
5 | import android.view.View
6 |
7 | /**
8 | * Created by niccapdevila on 3/26/16.
9 | */
10 | class RecentsFragment : BaseFragment() {
11 |
12 |
13 | @SuppressLint("SetTextI18n")
14 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
15 | super.onViewCreated(view, savedInstanceState)
16 | btn.setOnClickListener {
17 | mFragmentNavigation.pushFragment(RecentsFragment.newInstance(mInt + 1))
18 | }
19 | btn.text = """${javaClass.simpleName} $mInt"""
20 | }
21 |
22 | companion object {
23 |
24 | fun newInstance(instance: Int): RecentsFragment {
25 | val args = Bundle()
26 | args.putInt(BaseFragment.ARGS_INSTANCE, instance)
27 | val fragment = RecentsFragment()
28 | fragment.arguments = args
29 | return fragment
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_from_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_from_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_to_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_to_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-hdpi/ic_favorites.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-hdpi/ic_friends.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_nearby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-hdpi/ic_nearby.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_recents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-hdpi/ic_recents.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_restaurants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-hdpi/ic_restaurants.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-mdpi/ic_favorites.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-mdpi/ic_friends.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_nearby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-mdpi/ic_nearby.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_recents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-mdpi/ic_recents.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_restaurants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-mdpi/ic_restaurants.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xhdpi/ic_favorites.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xhdpi/ic_friends.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_nearby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xhdpi/ic_nearby.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_recents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xhdpi/ic_recents.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_restaurants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xhdpi/ic_restaurants.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxhdpi/ic_favorites.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxhdpi/ic_friends.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_nearby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxhdpi/ic_nearby.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_recents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxhdpi/ic_recents.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_restaurants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxhdpi/ic_restaurants.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxxhdpi/ic_favorites.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_friends.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxxhdpi/ic_friends.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_nearby.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxxhdpi/ic_nearby.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_recents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxxhdpi/ic_recents.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_restaurants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/drawable-xxxhdpi/ic_restaurants.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/side_nav_bar.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_bottom_tabs.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_nav_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_bar_nav_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_nav_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
22 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/nav_header_nav_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
22 |
23 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/activity_nav_drawer_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 16dp
7 | 120dp
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FragNav
3 |
4 | Open navigation drawer
5 | Close navigation drawer
6 |
7 | NavDrawerActivity
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/menu_bottombar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
13 |
18 |
23 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/test/java/com/ncapdevi/sample/AppUnitTests.java:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.sample;
2 |
3 | /**
4 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
5 | */
6 | public class AppUnitTests {
7 |
8 | //TODO Add Unit Tests
9 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.31'
3 | ext.spek_version = '2.0.0-rc.1'
4 | repositories {
5 | jcenter()
6 | google()
7 |
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.4.1'
11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
12 | classpath "de.mannodermaus.gradle.plugins:android-junit5:1.3.1.1"
13 | classpath "com.vanniktech:gradle-android-junit-jacoco-plugin:0.13.0"
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | mavenCentral()
22 | maven { url "http://dl.bintray.com/jetbrains/spek" }
23 | }
24 | }
25 | repositories {
26 | google()
27 | jcenter()
28 | mavenCentral()
29 | }
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
34 | ext {
35 | // App dependencies
36 |
37 | junitVersion = '4.12'
38 | buildToolsVersion = '28.0.3'
39 | minSdkVersion = 14
40 | targetSdkVersion = 28
41 | compileSdkVersion = 28
42 | }
--------------------------------------------------------------------------------
/frag-nav/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | pom.xml
3 | .idea
--------------------------------------------------------------------------------
/frag-nav/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.jfrog.bintray" version "1.8.4"
3 | id "com.github.dcendents.android-maven" version "2.1"
4 | id 'com.github.kt3k.coveralls' version '2.8.2'
5 | }
6 |
7 | apply plugin: 'com.android.library'
8 | apply plugin: 'kotlin-android'
9 | apply plugin: 'maven'
10 | apply plugin: 'com.github.kt3k.coveralls'
11 | apply plugin: "de.mannodermaus.android-junit5"
12 | apply plugin: "com.vanniktech.android.junit.jacoco"
13 |
14 | ext {
15 | libraryVersionCode = 30
16 | libraryVersionName = '3.3.0'
17 |
18 | //Bintray and Maven
19 | bintrayRepo = 'maven'
20 | bintrayName = 'frag-nav'
21 |
22 | publishedGroupId = 'com.ncapdevi'
23 | libraryName = 'FragNav'
24 | artifact = 'frag-nav'
25 |
26 | libraryDescription = 'A library to help manage multiple fragment stacks'
27 |
28 | siteUrl = 'https://github.com/ncapdevi/FragNav'
29 | gitUrl = 'https://github.com/ncapdevi/FragNav.git'
30 |
31 | developerId = 'ncapdevi'
32 | developerName = 'Nic Capdevila'
33 | developerEmail = 'ncapdevi@gmail.com'
34 |
35 | licenseName = 'The Apache Software License, Version 2.0'
36 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
37 | allLicenses = ["Apache-2.0"]
38 | }
39 |
40 |
41 | android {
42 | compileSdkVersion rootProject.ext.compileSdkVersion
43 | buildToolsVersion rootProject.ext.buildToolsVersion
44 |
45 | lintOptions {
46 | abortOnError false
47 | }
48 | defaultConfig {
49 | minSdkVersion rootProject.ext.minSdkVersion
50 | targetSdkVersion rootProject.ext.compileSdkVersion
51 | versionCode libraryVersionCode
52 | versionName libraryVersionName
53 | }
54 | buildTypes {
55 | release {
56 | minifyEnabled false
57 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
58 | }
59 | debug {
60 | testCoverageEnabled true
61 | }
62 | }
63 |
64 | testOptions {
65 | unitTests {
66 | includeAndroidResources = true
67 | }
68 | junitPlatform {
69 | filters {
70 | engines {
71 | include 'spek2'
72 | }
73 | }
74 | }
75 | }
76 | }
77 |
78 | dependencies {
79 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
80 |
81 | implementation "androidx.fragment:fragment:1.0.0"
82 | implementation "androidx.annotation:annotation:1.0.2"
83 |
84 | testImplementation "junit:junit:$rootProject.ext.junitVersion"
85 | testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.0.0"
86 | testImplementation "org.robolectric:robolectric:4.0.2"
87 |
88 | testImplementation 'org.amshove.kluent:kluent-android:1.42'
89 |
90 | // Spek
91 |
92 | testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version"
93 | testImplementation "org.spekframework.spek2:spek-runner-junit5:$spek_version"
94 | testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
95 | testImplementation('org.jetbrains.spek:spek-api:1.1.5') {
96 | exclude group: 'org.jetbrains.kotlin'
97 | }
98 | testImplementation('org.jetbrains.spek:spek-junit-platform-engine:1.1.5') {
99 | exclude group: 'org.junit.platform'
100 | exclude group: 'org.jetbrains.kotlin'
101 | }
102 |
103 |
104 | }
105 |
106 | group = publishedGroupId // Maven Group ID for the artifact
107 |
108 | install {
109 | repositories.mavenInstaller {
110 | // This generates POM.xml with proper parameters
111 | pom {
112 | project {
113 | packaging 'aar'
114 | groupId publishedGroupId
115 | artifactId artifact
116 |
117 | // Add your description here
118 | name libraryName
119 | description libraryDescription
120 | url siteUrl
121 |
122 | // Set your license
123 | licenses {
124 | license {
125 | name licenseName
126 | url licenseUrl
127 | }
128 | }
129 | developers {
130 | developer {
131 | id developerId
132 | name developerName
133 | email developerEmail
134 | }
135 | }
136 | scm {
137 | connection gitUrl
138 | developerConnection gitUrl
139 | url siteUrl
140 |
141 | }
142 | }
143 | }
144 | }
145 | }
146 |
147 |
148 | version = libraryVersionName
149 |
150 | if (project.hasProperty("android")) { // Android libraries
151 | task sourcesJar(type: Jar) {
152 | classifier = 'sources'
153 | from android.sourceSets.main.java.srcDirs
154 | }
155 |
156 | /* task javadoc(type: Javadoc) {
157 | source = android.sourceSets.main.java.srcDirs
158 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
159 | }*/
160 | } else { // Java libraries
161 | task sourcesJar(type: Jar, dependsOn: classes) {
162 | classifier = 'sources'
163 | from sourceSets.main.allSource
164 | }
165 | }
166 |
167 | /*task javadocJar(type: Jar, dependsOn: javadoc) {
168 | classifier = 'javadoc'
169 | from javadoc.destinationDir
170 | }*/
171 |
172 | artifacts {
173 | // archives javadocJar
174 | archives sourcesJar
175 | }
176 |
177 | // Bintray
178 | bintray {
179 | Properties properties = new Properties()
180 | if (project.rootProject.file('local.properties').exists()) {
181 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
182 |
183 | user = properties.getProperty("bintray.user")
184 | key = properties.getProperty("bintray.apikey")
185 |
186 | configurations = ['archives']
187 | pkg {
188 | repo = bintrayRepo
189 | name = bintrayName
190 | desc = libraryDescription
191 | websiteUrl = siteUrl
192 | vcsUrl = gitUrl
193 | licenses = allLicenses
194 | publish = true
195 | publicDownloadNumbers = true
196 | version {
197 | desc = libraryDescription
198 | }
199 | }
200 | }
201 | }
202 |
203 | coveralls {
204 | jacocoReportPath = "${buildDir}/reports/jacoco/debug/jacoco.xml"
205 | }
206 |
207 | tasks.coveralls {
208 | dependsOn 'jacocoTestReportDebug'
209 | onlyIf { System.env.'CI' }
210 | }
211 | task createPom {
212 | pom {
213 | project {
214 | packaging 'aar'
215 |
216 | name project.name
217 | description libraryDescription
218 | url siteUrl
219 | inceptionYear '2016'
220 |
221 | licenses {
222 | license {
223 | name 'The Apache Software License, Version 2.0'
224 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
225 | }
226 | }
227 | scm {
228 | connection gitUrl
229 | developerConnection gitUrl
230 | url siteUrl
231 | }
232 | developers {
233 | developer {
234 | id 'ncapdevi'
235 | name 'Nic Capdevila'
236 | email 'ncapdevi@gmail.com'
237 | }
238 | }
239 | }
240 | }.writeTo("$buildDir/poms/pom-default.xml").writeTo("pom.xml")
241 | }
242 | build.dependsOn createPom
243 | repositories {
244 | mavenCentral()
245 | }
--------------------------------------------------------------------------------
/frag-nav/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/niccapdevila/.android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/frag-nav/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/FragNavController.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("MemberVisibilityCanBePrivate")
2 |
3 | package com.ncapdevi.fragnav
4 |
5 | import android.annotation.SuppressLint
6 | import android.os.Bundle
7 | import androidx.annotation.CheckResult
8 | import androidx.annotation.IdRes
9 | import androidx.annotation.IntDef
10 | import androidx.fragment.app.DialogFragment
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.FragmentManager
13 | import androidx.fragment.app.FragmentTransaction
14 | import com.ncapdevi.fragnav.tabhistory.*
15 | import org.json.JSONArray
16 | import java.lang.ref.WeakReference
17 | import java.util.*
18 |
19 | @Suppress("unused")
20 | /**
21 | * The class is used to manage navigation through multiple stacks of fragments, as well as coordinate
22 | * fragments that may appear on screen
23 | *
24 | *
25 | * https://github.com/ncapdevi/FragNav
26 | * Nic Capdevila
27 | * Nic.Capdevila@gmail.com
28 | *
29 | *
30 | * Originally Created March 2016
31 | */
32 | class FragNavController constructor(private val fragmentManger: FragmentManager, @IdRes private val containerId: Int) {
33 |
34 | //region Public properties
35 | var rootFragments: List? = null
36 | set(value) {
37 | if (value != null) {
38 | if (rootFragmentListener != null) {
39 | throw IllegalStateException("Root fragments and root fragment listener can not be set the same time")
40 | }
41 |
42 | if (value.size > FragNavController.MAX_NUM_TABS) {
43 | throw IllegalArgumentException("Number of root fragments cannot be greater than " + FragNavController.MAX_NUM_TABS)
44 | }
45 | }
46 |
47 | field = value
48 | }
49 | var defaultTransactionOptions: FragNavTransactionOptions? = null
50 | var fragNavLogger: FragNavLogger? = null
51 | var rootFragmentListener: RootFragmentListener? = null
52 |
53 | var transactionListener: TransactionListener? = null
54 | var navigationStrategy: NavigationStrategy = CurrentTabStrategy()
55 | set(value) {
56 | field = value
57 | fragNavTabHistoryController = when (value) {
58 | is UniqueTabHistoryStrategy -> UniqueTabHistoryController(DefaultFragNavPopController(), value.fragNavSwitchController)
59 | is UnlimitedTabHistoryStrategy -> UnlimitedTabHistoryController(DefaultFragNavPopController(), value.fragNavSwitchController)
60 | else -> CurrentTabHistoryController(DefaultFragNavPopController())
61 | }
62 |
63 | }
64 |
65 | var fragmentHideStrategy = FragNavController.DETACH
66 | var createEager = false
67 |
68 | @TabIndex
69 | @get:CheckResult
70 | @get:TabIndex
71 | var currentStackIndex: Int = FragNavController.TAB1
72 | private set
73 | //endregion
74 |
75 | //region Private properties
76 | private val fragmentStacksTags: MutableList> = ArrayList()
77 | private var tagCount: Int = 0
78 | private var mCurrentFrag: Fragment? = null
79 | private var mCurrentDialogFrag: DialogFragment? = null
80 |
81 | private var executingTransaction: Boolean = false
82 | private var fragNavTabHistoryController: FragNavTabHistoryController = CurrentTabHistoryController(DefaultFragNavPopController())
83 | private val fragmentCache = mutableMapOf>()
84 | //endregion
85 |
86 |
87 | //region Public helper functions
88 |
89 | /**
90 | * Helper function to attempt to get current fragment
91 | *
92 | * @return Fragment the current frag to be returned
93 | */
94 | val currentFrag: Fragment?
95 | get() {
96 | //Attempt to used stored current fragment
97 | if (mCurrentFrag?.isAdded == true && mCurrentFrag?.isDetached?.not() == true) {
98 | return mCurrentFrag
99 | } else if (currentStackIndex == NO_TAB) {
100 | return null
101 | } else if (fragmentStacksTags.isEmpty()) {
102 | return null
103 | }
104 | //if not, try to pull it from the stack
105 | val fragmentStack = fragmentStacksTags[currentStackIndex]
106 | if (!fragmentStack.isEmpty()) {
107 | val fragmentByTag = getFragment(fragmentStack.peek())
108 | if (fragmentByTag != null) {
109 | mCurrentFrag = fragmentByTag
110 | }
111 | }
112 | return mCurrentFrag
113 | }
114 |
115 | /**
116 | * @return Current DialogFragment being displayed. Null if none
117 | */
118 | val currentDialogFrag: DialogFragment?
119 | @CheckResult
120 | get() {
121 | if (mCurrentDialogFrag != null) {
122 | return mCurrentDialogFrag
123 | } else {
124 | //Else try to find one in the FragmentManager
125 | val fragmentManager: FragmentManager = getFragmentManagerForDialog()
126 | mCurrentDialogFrag = fragmentManager.fragments.firstOrNull { it is DialogFragment } as DialogFragment?
127 | }
128 | return mCurrentDialogFrag
129 | }
130 |
131 |
132 | /**
133 | * Get the number of fragment stacks
134 | *
135 | * @return the number of fragment stacks
136 | */
137 | val size: Int
138 | @CheckResult
139 | get() = fragmentStacksTags.size
140 |
141 | /**
142 | * Get a copy of the current stack that is being displayed
143 | *
144 | * @return Current stack
145 | */
146 | val currentStack: Stack?
147 | @CheckResult
148 | get() = getStack(currentStackIndex)
149 |
150 | /**
151 | * @return If true, you are at the bottom of the stack
152 | * (Consider using replaceFragment if you need to change the root fragment for some reason)
153 | * else you can popFragment as needed as your are not at the root
154 | */
155 | val isRootFragment: Boolean
156 | @CheckResult
157 | get() = fragmentStacksTags.getOrNull(currentStackIndex)?.size == 1
158 |
159 |
160 | /**
161 | * Helper function to get whether the fragmentManger has gone through a stateSave, if this is true, you probably want to commit allowing state loss
162 | *
163 | * @return if fragmentManger isStateSaved
164 | */
165 | val isStateSaved: Boolean
166 | get() = fragmentManger.isStateSaved
167 |
168 |
169 | /**
170 | * Helper function to make sure that we are starting with a clean slate and to perform our first fragment interaction.
171 | *
172 | * @param index the tab index to initialize to
173 | */
174 |
175 |
176 | fun initialize(@TabIndex index: Int = TAB1, savedInstanceState: Bundle? = null) {
177 | if (rootFragmentListener == null && rootFragments == null) {
178 | throw IndexOutOfBoundsException("Either a root fragment(s) needs to be set, or a fragment listener")
179 | } else if (rootFragmentListener != null && rootFragments != null) {
180 | throw java.lang.IllegalStateException("Shouldn't have both a rootFragmentListener and rootFragments set, this is clearly a mistake")
181 | }
182 |
183 | val numberOfTabs: Int = rootFragmentListener?.numberOfRootFragments ?: rootFragments?.size
184 | ?: 0
185 |
186 | //Attempt to restore from bundle, if not, initialize
187 | if (!restoreFromBundle(savedInstanceState)) {
188 | fragmentStacksTags.clear()
189 | for (i in 0 until numberOfTabs) {
190 | fragmentStacksTags.add(Stack())
191 | }
192 |
193 |
194 | currentStackIndex = index
195 | if (currentStackIndex > fragmentStacksTags.size) {
196 | throw IndexOutOfBoundsException("Starting index cannot be larger than the number of stacks")
197 | }
198 | fragNavTabHistoryController.switchTab(index)
199 |
200 | currentStackIndex = index
201 | clearFragmentManager()
202 | clearDialogFragment()
203 |
204 | if (index == NO_TAB) {
205 | return
206 | }
207 |
208 | val ft = createTransactionWithOptions(defaultTransactionOptions, false, false)
209 |
210 | val lowerBound = if (createEager) 0 else index
211 | val upperBound = if (createEager) fragmentStacksTags.size else index + 1
212 | for (i in lowerBound until upperBound) {
213 | currentStackIndex = i
214 | val fragment = getRootFragment(i)
215 | val fragmentTag = generateTag(fragment)
216 | fragmentStacksTags[currentStackIndex].push(fragmentTag)
217 | ft.addSafe(containerId, fragment, fragmentTag)
218 | if (i != index) {
219 | when {
220 | shouldDetachAttachOnSwitch() -> ft.detach(fragment)
221 | shouldRemoveAttachOnSwitch() -> ft.remove(fragment)
222 | else -> ft.hide(fragment)
223 | }
224 | } else {
225 | mCurrentFrag = fragment
226 | }
227 | }
228 | currentStackIndex = index
229 |
230 | commitTransaction(ft, defaultTransactionOptions)
231 |
232 | transactionListener?.onTabTransaction(currentFrag, currentStackIndex)
233 | } else {
234 | fragNavTabHistoryController.restoreFromBundle(savedInstanceState)
235 | }
236 | }
237 |
238 |
239 | //endregion
240 |
241 | //region Transactions
242 |
243 | /**
244 | * Function used to switch to the specified fragment stack
245 | *
246 | * @param index The given index to switch to
247 | * @param transactionOptions Transaction options to be displayed
248 | * @throws IndexOutOfBoundsException Thrown if trying to switch to an index outside given range
249 | */
250 | @Throws(IndexOutOfBoundsException::class)
251 | @JvmOverloads
252 | fun switchTab(@TabIndex index: Int, transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions) {
253 | switchTabInternal(index, transactionOptions)
254 | }
255 |
256 | @Throws(IndexOutOfBoundsException::class)
257 | private fun switchTabInternal(@TabIndex index: Int, transactionOptions: FragNavTransactionOptions?) {
258 | //Check to make sure the tab is within range
259 | if (index >= fragmentStacksTags.size) {
260 | throw IndexOutOfBoundsException("Can't switch to a tab that hasn't been initialized, " +
261 | "Index : " + index + ", current stack size : " + fragmentStacksTags.size +
262 | ". Make sure to create all of the tabs you need in the Constructor or provide a way for them to be created via RootFragmentListener.")
263 | }
264 | if (currentStackIndex != index) {
265 | val ft = createTransactionWithOptions(transactionOptions, index < currentStackIndex)
266 | removeCurrentFragment(ft, shouldDetachAttachOnSwitch(), shouldRemoveAttachOnSwitch())
267 |
268 | currentStackIndex = index
269 |
270 | fragNavTabHistoryController.switchTab(index)
271 |
272 | var fragment: Fragment? = null
273 | if (index == NO_TAB) {
274 | commitTransaction(ft, transactionOptions)
275 | } else {
276 | //Attempt to reattach previous fragment
277 | fragment = addPreviousFragment(ft, shouldDetachAttachOnSwitch() || shouldRemoveAttachOnSwitch())
278 | commitTransaction(ft, transactionOptions)
279 | }
280 | mCurrentFrag = fragment
281 | transactionListener?.onTabTransaction(currentFrag, currentStackIndex)
282 | }
283 | }
284 |
285 | /**
286 | * Push a fragment onto the current stack
287 | *
288 | * @param fragment The fragment that is to be pushed
289 | * @param transactionOptions Transaction options to be displayed
290 | */
291 | @JvmOverloads
292 | fun pushFragment(fragment: Fragment?, transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions) {
293 | if (fragment != null && currentStackIndex != NO_TAB) {
294 | val ft = createTransactionWithOptions(transactionOptions, false)
295 |
296 | removeCurrentFragment(ft, shouldDetachAttachOnPushPop(), shouldRemoveAttachOnSwitch())
297 |
298 | val fragmentTag = generateTag(fragment)
299 | fragmentStacksTags[currentStackIndex].push(fragmentTag)
300 | ft.addSafe(containerId, fragment, fragmentTag)
301 |
302 | commitTransaction(ft, transactionOptions)
303 |
304 | mCurrentFrag = fragment
305 | transactionListener?.onFragmentTransaction(currentFrag, TransactionType.PUSH)
306 | }
307 | }
308 |
309 | /**
310 | * Pop the current fragment from the current tab
311 | *
312 | * @param transactionOptions Transaction options to be displayed
313 | */
314 | @Throws(UnsupportedOperationException::class)
315 | @JvmOverloads
316 | fun popFragment(transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions): Boolean {
317 | return popFragments(1, transactionOptions)
318 | }
319 |
320 | /**
321 | * Pop the current stack until a given tag is found. If the tag is not found, the stack will popFragment until it is at
322 | * the root fragment
323 | *
324 | * @param transactionOptions Transaction options to be displayed
325 | * @return true if any any fragment has been popped
326 | */
327 | @Throws(UnsupportedOperationException::class)
328 | fun popFragments(popDepth: Int, transactionOptions: FragNavTransactionOptions?): Boolean {
329 | return fragNavTabHistoryController.popFragments(popDepth, transactionOptions)
330 | }
331 |
332 | @Throws(UnsupportedOperationException::class)
333 | private fun tryPopFragmentsFromCurrentStack(popDepth: Int, transactionOptions: FragNavTransactionOptions?): Int {
334 | if (navigationStrategy is CurrentTabStrategy && isRootFragment) {
335 | throw UnsupportedOperationException(
336 | "You can not popFragment the rootFragment. If you need to change this fragment, use replaceFragment(fragment)")
337 | } else if (popDepth < 1) {
338 | throw UnsupportedOperationException("popFragments parameter needs to be greater than 0")
339 | } else if (currentStackIndex == NO_TAB) {
340 | throw UnsupportedOperationException("You can not pop fragments when no tab is selected")
341 | }
342 |
343 | //If our popDepth is big enough that it would just clear the stack, then call that.
344 | val currentStack = fragmentStacksTags[currentStackIndex]
345 | val poppableSize = currentStack.size - 1
346 | if (popDepth >= poppableSize) {
347 | clearStack(transactionOptions)
348 | return poppableSize
349 | }
350 |
351 | val ft = createTransactionWithOptions(transactionOptions, true)
352 |
353 | //Pop the number of the fragments on the stack and remove them from the FragmentManager
354 | for (i in 0 until popDepth) {
355 | val fragment = getFragment(currentStack.pop())
356 | if (fragment != null) {
357 | ft.removeSafe(fragment)
358 | }
359 | }
360 |
361 | // Attempt to reattach previous fragment
362 | val fragment = addPreviousFragment(ft, shouldDetachAttachOnPushPop())
363 |
364 | commitTransaction(ft, transactionOptions)
365 | mCurrentFrag = fragment
366 | transactionListener?.onFragmentTransaction(currentFrag, TransactionType.POP)
367 | return popDepth
368 | }
369 |
370 | /**
371 | * Pop the current fragment from the current tab
372 | */
373 | @Throws(UnsupportedOperationException::class)
374 | fun popFragments(popDepth: Int) {
375 | popFragments(popDepth, defaultTransactionOptions)
376 | }
377 |
378 | /**
379 | * Clears the current tab's stack to get to just the bottom Fragment. This will reveal the root fragment
380 | *
381 | * @param transactionOptions Transaction options to be displayed
382 | */
383 | @JvmOverloads
384 | fun clearStack(transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions) {
385 | clearStack(currentStackIndex,transactionOptions)
386 | }
387 |
388 | /**
389 | * Clears the passed tab's stack to get to just the bottom Fragment. This will reveal the root fragment
390 | *
391 | * @param tabIndex Index of tab that needs to be cleared
392 | * @param transactionOptions Transaction options to be displayed
393 | */
394 | @JvmOverloads
395 | fun clearStack(tabIndex: Int, transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions) {
396 | if (tabIndex == NO_TAB) {
397 | return
398 | }
399 |
400 | //Grab Current stack
401 | val fragmentStack = fragmentStacksTags[tabIndex]
402 |
403 | // Only need to start popping and reattach if the stack is greater than 1
404 | if (fragmentStack.size > 1) {
405 | //Only animate if we're clearing the current stack
406 | val shouldAnimate = tabIndex == currentStackIndex
407 | val ft = createTransactionWithOptions(transactionOptions,true, shouldAnimate)
408 |
409 | //Pop all of the fragments on the stack and remove them from the FragmentManager
410 | while (fragmentStack.size > 1) {
411 | val fragment = getFragment(fragmentStack.pop())
412 | if (fragment != null) {
413 | ft.removeSafe(fragment)
414 | }
415 | }
416 |
417 | // Attempt to reattach previous fragment
418 | val fragment = addPreviousFragment(ft, shouldDetachAttachOnPushPop())
419 |
420 | commitTransaction(ft, transactionOptions)
421 | mCurrentFrag = fragment
422 | transactionListener?.onFragmentTransaction(currentFrag, TransactionType.POP)
423 | }
424 | }
425 |
426 | /**
427 | * Replace the current fragment
428 | *
429 | * @param fragment the fragment to be shown instead
430 | * @param transactionOptions Transaction options to be displayed
431 | */
432 | @JvmOverloads
433 | fun replaceFragment(fragment: Fragment, transactionOptions: FragNavTransactionOptions? = defaultTransactionOptions) {
434 | val poppingFrag = currentFrag
435 |
436 | if (poppingFrag != null) {
437 | val ft = createTransactionWithOptions(transactionOptions, false)
438 |
439 | //overly cautious fragment popFragment
440 |
441 | val fragmentTag = generateTag(fragment)
442 | ft.replace(containerId, fragment, fragmentTag)
443 | commitTransaction(ft, transactionOptions)
444 |
445 | fragmentStacksTags[currentStackIndex].apply {
446 | if (isNotEmpty()) {
447 | pop()
448 | }
449 | push(fragmentTag)
450 | }
451 | mCurrentFrag = fragment
452 |
453 | transactionListener?.onFragmentTransaction(currentFrag, TransactionType.REPLACE)
454 | }
455 | }
456 |
457 | /**
458 | * Clear any DialogFragments that may be shown
459 | */
460 | @Suppress("MemberVisibilityCanBePrivate")
461 | fun clearDialogFragment() {
462 | val currentDialogFrag = mCurrentDialogFrag
463 | if (currentDialogFrag != null) {
464 | currentDialogFrag.dismiss()
465 | mCurrentDialogFrag = null
466 | } else {
467 | val fragmentManager: FragmentManager = getFragmentManagerForDialog()
468 | fragmentManager.fragments.forEach {
469 | if (it is DialogFragment) {
470 | it.dismiss()
471 | }
472 | }
473 | }
474 | }
475 |
476 | /**
477 | * Display a DialogFragment on the screen
478 | *
479 | * @param dialogFragment The Fragment to be Displayed
480 | */
481 | fun showDialogFragment(dialogFragment: DialogFragment?) {
482 | //Clear any current dialog fragments
483 | clearDialogFragment()
484 |
485 | if (dialogFragment != null) {
486 | val fragmentManager: FragmentManager = getFragmentManagerForDialog()
487 | mCurrentDialogFrag = dialogFragment
488 | try {
489 | dialogFragment.show(fragmentManager, dialogFragment.javaClass.name)
490 | } catch (e: IllegalStateException) {
491 | logError("Could not show dialog", e)
492 | // Activity was likely destroyed before we had a chance to show, nothing can be done here.
493 | }
494 | }
495 | }
496 |
497 | //endregion
498 |
499 | //region Private helper functions
500 |
501 | /**
502 | * Helper function to get the root fragment for a given index. This is done by either passing them in the constructor, or dynamically via NavListener.
503 | *
504 | * @param index The tab index to get this fragment from
505 | * @return The root fragment at this index
506 | * @throws IllegalStateException This will be thrown if we can't find a rootFragment for this index. Either because you didn't provide it in the
507 | * constructor, or because your RootFragmentListener.getRootFragment(index) isn't returning a fragment for this index.
508 | */
509 | @CheckResult
510 | @Throws(IllegalStateException::class)
511 | private fun getRootFragment(index: Int): Fragment {
512 | var fragment: Fragment? = null
513 |
514 | if (fragment == null) {
515 | fragment = rootFragmentListener?.getRootFragment(index)
516 | }
517 |
518 | if (fragment == null) {
519 | fragment = rootFragments?.getOrNull(index)
520 | }
521 |
522 |
523 | if (fragment == null) {
524 | throw IllegalStateException("Either you haven't past in a fragment at this index in your constructor, or you haven't " + "provided a way to create it while via your RootFragmentListener.getRootFragment(index)")
525 | }
526 |
527 | return fragment
528 | }
529 |
530 | /**
531 | * Adds fragment to the fragment transaction, also add it to local cache so we can obtain it even before transaction has been committed.
532 | */
533 | private fun FragmentTransaction.addSafe(containerViewId: Int, fragment: Fragment, tag: String) {
534 | fragmentCache[tag] = WeakReference(fragment)
535 | add(containerViewId, fragment, tag)
536 | }
537 |
538 | /**
539 | * Remove the fragment from transaction and also from cache if found.
540 | */
541 | private fun FragmentTransaction.removeSafe(fragment: Fragment) {
542 | val tag = fragment.tag
543 | if (tag != null) {
544 | fragmentCache.remove(tag)
545 | }
546 | remove(fragment)
547 | }
548 |
549 | /**
550 | * Will attempt to reattach a previous fragment or fragments in fragment stack until it succeeds or replace with root fragment.
551 | *
552 | * @param ft current fragment transaction
553 | * @return Fragment if we were able to find and reattach it
554 | */
555 | private fun addPreviousFragment(ft: FragmentTransaction, isAttach: Boolean): Fragment {
556 | val fragmentStack = fragmentStacksTags[currentStackIndex]
557 | var currentFragment: Fragment? = null
558 | var currentTag: String? = null
559 | var index = 0
560 | val initialSize = fragmentStack.size
561 | while (currentFragment == null && fragmentStack.isNotEmpty()) {
562 | index++
563 | currentTag = fragmentStack.pop()
564 | currentFragment = getFragment(currentTag)
565 | }
566 | return if (currentFragment != null) {
567 | if (index > 1) {
568 | val message = "Could not restore top fragment on current stack"
569 | logError(message, IllegalStateException(message))
570 | }
571 | fragmentStack.push(currentTag)
572 | if (isAttach) {
573 | ft.attach(currentFragment)
574 | } else {
575 | ft.show(currentFragment)
576 | }
577 | currentFragment
578 | } else {
579 | if (initialSize > 0) {
580 | val message = "Could not restore any fragment on current stack, adding new root fragment"
581 | logError(message, IllegalStateException(message))
582 | }
583 | val rootFragment = getRootFragment(currentStackIndex)
584 | val rootTag = generateTag(rootFragment)
585 | fragmentStack.push(rootTag)
586 | ft.addSafe(containerId, rootFragment, rootTag)
587 | rootFragment
588 | }
589 | }
590 |
591 | /**
592 | * Attempts to detach any current fragment if it exists, and if none is found, returns.
593 | *
594 | * @param ft the current transaction being performed
595 | */
596 | private fun removeCurrentFragment(ft: FragmentTransaction, isDetach: Boolean, isRemove: Boolean) {
597 | currentFrag?.let {
598 | when {
599 | isDetach -> ft.detach(it)
600 | isRemove -> ft.remove(it)
601 | else -> ft.hide(it)
602 | }
603 | }
604 | }
605 |
606 | /**
607 | * Create a unique fragment tag so that we can grab the fragment later from the FragmentManger
608 | *
609 | * @param fragment The fragment that we're creating a unique tag for
610 | * @return a unique tag using the fragment's class name
611 | */
612 | @CheckResult
613 | private fun generateTag(fragment: Fragment): String {
614 | return fragment.javaClass.name + ++tagCount
615 | }
616 |
617 | private fun getFragment(tag: String): Fragment? {
618 | val weakReference = fragmentCache[tag]
619 | if (weakReference != null) {
620 | val fragment = weakReference.get()
621 | if (fragment != null) {
622 | return fragment
623 | }
624 | fragmentCache.remove(tag)
625 | }
626 | return fragmentManger.findFragmentByTag(tag)
627 | }
628 |
629 |
630 | /**
631 | * Private helper function to clear out the fragment manager on initialization. All fragment management should be done via FragNav.
632 | */
633 | private fun clearFragmentManager() {
634 | val currentFragments = fragmentManger.fragments.filterNotNull()
635 | if (currentFragments.isNotEmpty()) {
636 | with(createTransactionWithOptions(defaultTransactionOptions, false)) {
637 | currentFragments.forEach { removeSafe(it) }
638 | commitTransaction(this, defaultTransactionOptions)
639 | }
640 | }
641 | }
642 |
643 | /**
644 | * Setup a fragment transaction with the given option
645 | *
646 | * @param transactionOptions The options that will be set for this transaction
647 | */
648 | @SuppressLint("CommitTransaction")
649 | @CheckResult
650 | private fun createTransactionWithOptions(transactionOptions: FragNavTransactionOptions?, isPopping: Boolean, animated: Boolean = true): FragmentTransaction {
651 | return fragmentManger.beginTransaction().apply {
652 | transactionOptions?.also { options ->
653 | // Not using standard pop support since we handle backstack manually
654 | if (animated) {
655 | if (isPopping) {
656 | setCustomAnimations(
657 | transactionOptions.popEnterAnimation,
658 | transactionOptions.popExitAnimation
659 | )
660 | } else {
661 | setCustomAnimations(
662 | transactionOptions.enterAnimation,
663 | transactionOptions.exitAnimation
664 | )
665 | }
666 | }
667 |
668 | setTransitionStyle(options.transitionStyle)
669 |
670 | setTransition(options.transition)
671 |
672 | options.sharedElements.forEach { sharedElement ->
673 | sharedElement.first?.let {
674 | sharedElement.second?.let { it1 ->
675 | addSharedElement(
676 | it,
677 | it1
678 | )
679 | }
680 | }
681 | }
682 |
683 | when {
684 | options.breadCrumbTitle != null -> setBreadCrumbTitle(options.breadCrumbTitle)
685 | options.breadCrumbShortTitle != null -> setBreadCrumbShortTitle(options.breadCrumbShortTitle)
686 | }
687 |
688 | setReorderingAllowed(options.reordering)
689 | }
690 | }
691 | }
692 |
693 | /**
694 | * Helper function to commit fragment transaction with transaction option - allowStateLoss
695 | *
696 | * @param fragmentTransaction
697 | * @param transactionOptions
698 | */
699 | private fun commitTransaction(fragmentTransaction: FragmentTransaction, transactionOptions: FragNavTransactionOptions?) {
700 | if (transactionOptions?.allowStateLoss == true) {
701 | fragmentTransaction.commitAllowingStateLoss()
702 | } else {
703 | fragmentTransaction.commit()
704 | }
705 | }
706 |
707 | private fun logError(message: String, throwable: Throwable) {
708 | fragNavLogger?.error(message, throwable)
709 | }
710 |
711 | private fun shouldDetachAttachOnPushPop(): Boolean {
712 | return fragmentHideStrategy != HIDE
713 | }
714 |
715 | private fun shouldDetachAttachOnSwitch(): Boolean {
716 | return fragmentHideStrategy == DETACH
717 | }
718 |
719 | private fun shouldRemoveAttachOnSwitch(): Boolean {
720 | return fragmentHideStrategy == REMOVE
721 | }
722 |
723 | /**
724 | * Get a copy of the stack at a given index
725 | *
726 | * @return requested stack
727 | */
728 | @CheckResult
729 | @Throws(IndexOutOfBoundsException::class)
730 | fun getStack(@TabIndex index: Int): Stack? {
731 | if (index == NO_TAB) {
732 | return null
733 | }
734 | return fragmentStacksTags[index].mapNotNullTo(Stack()) { s -> getFragment(s) }
735 | }
736 |
737 | /**
738 | * Use this if you need to make sure that pending transactions occur immediately. This call is safe to
739 | * call as often as you want as there's a check to prevent multiple executePendingTransactions at once
740 | */
741 | fun executePendingTransactions() {
742 | if (!executingTransaction) {
743 | executingTransaction = true
744 | fragmentManger.executePendingTransactions()
745 | executingTransaction = false
746 | }
747 | }
748 |
749 | fun getFragmentManagerForDialog(): FragmentManager {
750 | val currentFrag = this.currentFrag
751 | return if (currentFrag?.isAdded == true) {
752 | currentFrag.childFragmentManager
753 | } else {
754 | this.fragmentManger
755 | }
756 | }
757 |
758 | //endregion
759 |
760 | //region SavedInstanceState
761 |
762 | /**
763 | * Call this in your Activity's onSaveInstanceState(Bundle outState) method to save the instance's state.
764 | *
765 | * @param outState The Bundle to save state information to
766 | */
767 | fun onSaveInstanceState(outState: Bundle?) {
768 | if (outState == null) {
769 | return
770 | }
771 | // Write tag count
772 | outState.putInt(EXTRA_TAG_COUNT, tagCount)
773 |
774 | // Write select tab
775 | outState.putInt(EXTRA_SELECTED_TAB_INDEX, currentStackIndex)
776 |
777 | // Write current fragment
778 | val currentFrag = currentFrag
779 | if (currentFrag != null) {
780 | outState.putString(EXTRA_CURRENT_FRAGMENT, currentFrag.tag)
781 | }
782 |
783 |
784 | // Write tag stacks
785 |
786 | try {
787 | val stackArrays = JSONArray()
788 | fragmentStacksTags.forEach { stack ->
789 | val stackArray = JSONArray()
790 | stack.forEach { stackArray.put(it) }
791 | stackArrays.put(stackArray)
792 | }
793 | outState.putString(EXTRA_FRAGMENT_STACK, stackArrays.toString())
794 |
795 | } catch (t: Throwable) {
796 | logError("Could not save fragment stack", t)
797 | // Nothing we can do
798 | }
799 |
800 | fragNavTabHistoryController.onSaveInstanceState(outState)
801 | }
802 |
803 | /**
804 | * Restores this instance to the state specified by the contents of savedInstanceState
805 | *
806 | * @param savedInstanceState The bundle to restore from
807 | * @return true if successful, false if not
808 | */
809 | private fun restoreFromBundle(savedInstanceState: Bundle?): Boolean {
810 | if (savedInstanceState == null) {
811 | return false
812 | }
813 |
814 | // Restore tag count
815 | tagCount = savedInstanceState.getInt(EXTRA_TAG_COUNT, 0)
816 |
817 | // Restore current fragment
818 | val tag = savedInstanceState.getString(EXTRA_CURRENT_FRAGMENT)
819 | if (tag !=null) {
820 | mCurrentFrag = getFragment(tag)
821 | }
822 |
823 | // Restore fragment stacks
824 | try {
825 | val stackArrays = JSONArray(savedInstanceState.getString(EXTRA_FRAGMENT_STACK))
826 |
827 | for (x in 0 until stackArrays.length()) {
828 | val stackArray = stackArrays.getJSONArray(x)
829 | val stack = Stack()
830 | (0 until stackArray.length())
831 | .map { stackArray.getString(it) }
832 | .filter { !it.isNullOrEmpty() && !"null".equals(it, ignoreCase = true) }
833 | .mapNotNullTo(stack) { it }
834 |
835 | fragmentStacksTags.add(stack)
836 | }
837 | // Restore selected tab if we have one
838 | val selectedTabIndex = savedInstanceState.getInt(EXTRA_SELECTED_TAB_INDEX)
839 | if (selectedTabIndex in 0..(MAX_NUM_TABS - 1)) {
840 | // Shortcut for switchTab. We already restored fragment, so just notify history controller
841 | // We cannot use switchTab, because switchTab removes fragment, but we don't want it
842 | currentStackIndex = selectedTabIndex
843 | fragNavTabHistoryController.switchTab(selectedTabIndex)
844 |
845 | transactionListener?.onTabTransaction(mCurrentFrag, selectedTabIndex)
846 | }
847 |
848 | //Successfully restored state
849 | return true
850 | } catch (ex: Throwable) {
851 | tagCount = 0
852 | mCurrentFrag = null
853 | fragmentStacksTags.clear()
854 | logError("Could not restore fragment state", ex)
855 | return false
856 | }
857 |
858 | }
859 | //endregion
860 |
861 | enum class TransactionType {
862 | PUSH,
863 | POP,
864 | REPLACE
865 | }
866 |
867 | //Declare the TabIndex annotation
868 | @IntDef(NO_TAB, TAB1, TAB2, TAB3, TAB4, TAB5, TAB6, TAB7, TAB8, TAB9, TAB10, TAB11, TAB12, TAB13, TAB14, TAB15, TAB16, TAB17, TAB18, TAB19, TAB20)
869 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
870 | annotation class TabIndex
871 |
872 |
873 | // Declare Transit Styles
874 | @IntDef(FragmentTransaction.TRANSIT_NONE, FragmentTransaction.TRANSIT_FRAGMENT_OPEN, FragmentTransaction.TRANSIT_FRAGMENT_CLOSE, FragmentTransaction.TRANSIT_FRAGMENT_FADE)
875 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
876 | internal annotation class Transit
877 |
878 | /**
879 | * Define what happens when we try to pop on a tab where root fragment is at the top
880 | */
881 | @IntDef(DETACH, HIDE, REMOVE, DETACH_ON_NAVIGATE_HIDE_ON_SWITCH)
882 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
883 | annotation class FragmentHideStrategy
884 |
885 | interface RootFragmentListener {
886 | val numberOfRootFragments: Int
887 | /**
888 | * Dynamically create the Fragment that will go on the bottom of the stack
889 | *
890 | * @param index the index that the root of the stack Fragment needs to go
891 | * @return the new Fragment
892 | */
893 | fun getRootFragment(index: Int): Fragment
894 | }
895 |
896 | interface TransactionListener {
897 |
898 | fun onTabTransaction(fragment: Fragment?, index: Int)
899 |
900 | fun onFragmentTransaction(fragment: Fragment?, transactionType: TransactionType)
901 | }
902 |
903 | inner class DefaultFragNavPopController : com.ncapdevi.fragnav.FragNavPopController {
904 | @Throws(UnsupportedOperationException::class)
905 | override fun tryPopFragments(popDepth: Int, transactionOptions: FragNavTransactionOptions?): Int {
906 | return this@FragNavController.tryPopFragmentsFromCurrentStack(popDepth, transactionOptions)
907 | }
908 | }
909 |
910 | companion object {
911 | // Declare the constants. A maximum of 5 tabs is recommended for bottom navigation, this is per Material Design's Bottom Navigation's design spec.
912 | const val NO_TAB = -1
913 | const val TAB1 = 0
914 | const val TAB2 = 1
915 | const val TAB3 = 2
916 | const val TAB4 = 3
917 | const val TAB5 = 4
918 | const val TAB6 = 5
919 | const val TAB7 = 6
920 | const val TAB8 = 7
921 | const val TAB9 = 8
922 | const val TAB10 = 9
923 | const val TAB11 = 10
924 | const val TAB12 = 11
925 | const val TAB13 = 12
926 | const val TAB14 = 13
927 | const val TAB15 = 14
928 | const val TAB16 = 15
929 | const val TAB17 = 16
930 | const val TAB18 = 17
931 | const val TAB19 = 18
932 | const val TAB20 = 19
933 |
934 | internal const val MAX_NUM_TABS = 20
935 |
936 | // Extras used to store savedInstanceState
937 | private val EXTRA_TAG_COUNT = FragNavController::class.java.name + ":EXTRA_TAG_COUNT"
938 | private val EXTRA_SELECTED_TAB_INDEX = FragNavController::class.java.name + ":EXTRA_SELECTED_TAB_INDEX"
939 | private val EXTRA_CURRENT_FRAGMENT = FragNavController::class.java.name + ":EXTRA_CURRENT_FRAGMENT"
940 | private val EXTRA_FRAGMENT_STACK = FragNavController::class.java.name + ":EXTRA_FRAGMENT_STACK"
941 |
942 |
943 | /**
944 | * Using attach and detach methods of Fragment transaction to switch between fragments
945 | */
946 | const val DETACH = 0
947 |
948 | /**
949 | * Using show and hide methods of Fragment transaction to switch between fragments
950 | */
951 | const val HIDE = 1
952 |
953 | /**
954 | * Using attach and detach methods of Fragment transaction to navigate between fragments on the current tab but
955 | * using show and hide methods to switch between tabs
956 | */
957 | const val DETACH_ON_NAVIGATE_HIDE_ON_SWITCH = 2
958 |
959 | /**
960 | * Using create + attach and remove methods of Fragment transaction to switch between fragments
961 | */
962 | const val REMOVE = 3
963 | }
964 | }
965 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/FragNavLogger.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | interface FragNavLogger {
4 | fun error(message: String, throwable: Throwable)
5 | }
6 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/FragNavPopController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | interface FragNavPopController {
4 | @Throws(UnsupportedOperationException::class)
5 | fun tryPopFragments(popDepth: Int, transactionOptions: FragNavTransactionOptions?): Int
6 | }
7 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/FragNavSwitchController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | interface FragNavSwitchController {
4 | fun switchTab(@FragNavController.TabIndex index: Int, transactionOptions: FragNavTransactionOptions?)
5 | }
6 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/FragNavTransactionOptions.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | import androidx.annotation.AnimRes
4 | import androidx.annotation.StyleRes
5 | import androidx.fragment.app.FragmentTransaction
6 | import android.view.View
7 |
8 | @Suppress("MemberVisibilityCanBePrivate", "unused")
9 | class FragNavTransactionOptions private constructor(builder: Builder) {
10 | val sharedElements: List> = builder.sharedElements
11 | @FragNavController.Transit
12 | val transition = builder.transition
13 | @AnimRes
14 | val enterAnimation = builder.enterAnimation
15 | @AnimRes
16 | val exitAnimation = builder.exitAnimation
17 | @AnimRes
18 | val popEnterAnimation = builder.popEnterAnimation
19 | @AnimRes
20 | val popExitAnimation = builder.popExitAnimation
21 | @StyleRes
22 | val transitionStyle = builder.transitionStyle
23 | val breadCrumbTitle: String? = builder.breadCrumbTitle
24 | val breadCrumbShortTitle: String? = builder.breadCrumbShortTitle
25 | val allowStateLoss: Boolean = builder.allowStateLoss
26 | val reordering: Boolean = builder.reordering
27 |
28 | class Builder {
29 | var sharedElements: MutableList> = mutableListOf()
30 | var transition: Int = FragmentTransaction.TRANSIT_NONE
31 | var enterAnimation: Int = 0
32 | var exitAnimation: Int = 0
33 | var transitionStyle: Int = 0
34 | var popEnterAnimation: Int = 0
35 | var popExitAnimation: Int = 0
36 | var breadCrumbTitle: String? = null
37 | var breadCrumbShortTitle: String? = null
38 | var allowStateLoss = false
39 | var reordering = false
40 |
41 | fun addSharedElement(element: Pair): Builder {
42 | sharedElements.add(element)
43 | return this
44 | }
45 |
46 | fun sharedElements(elements: MutableList>): Builder {
47 | sharedElements = elements
48 | return this
49 | }
50 |
51 | fun transition(@FragNavController.Transit transition: Int): Builder {
52 | this.transition = transition
53 | return this
54 | }
55 |
56 | fun customAnimations(@AnimRes enterAnimation: Int, @AnimRes exitAnimation: Int): Builder {
57 | this.enterAnimation = enterAnimation
58 | this.exitAnimation = exitAnimation
59 | return this
60 | }
61 |
62 | fun customAnimations(@AnimRes enterAnimation: Int, @AnimRes exitAnimation: Int, @AnimRes popEnterAnimation: Int, @AnimRes popExitAnimation: Int): Builder {
63 | this.popEnterAnimation = popEnterAnimation
64 | this.popExitAnimation = popExitAnimation
65 | return customAnimations(enterAnimation, exitAnimation)
66 | }
67 |
68 |
69 | fun transitionStyle(@StyleRes transitionStyle: Int): Builder {
70 | this.transitionStyle = transitionStyle
71 | return this
72 | }
73 |
74 | fun breadCrumbTitle(breadCrumbTitle: String): Builder {
75 | this.breadCrumbTitle = breadCrumbTitle
76 | return this
77 | }
78 |
79 | fun breadCrumbShortTitle(breadCrumbShortTitle: String): Builder {
80 | this.breadCrumbShortTitle = breadCrumbShortTitle
81 | return this
82 | }
83 |
84 | fun allowStateLoss(allow: Boolean): Builder {
85 | allowStateLoss = allow
86 | return this
87 | }
88 |
89 | fun allowReordering(reorder: Boolean): Builder {
90 | reordering = reorder
91 | return this
92 | }
93 |
94 | fun build(): FragNavTransactionOptions {
95 | return FragNavTransactionOptions(this)
96 | }
97 | }
98 |
99 | companion object {
100 | fun newBuilder(): Builder {
101 | return Builder()
102 | }
103 | }
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/BaseFragNavTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import com.ncapdevi.fragnav.FragNavPopController
4 |
5 | abstract class BaseFragNavTabHistoryController(val fragNavPopController: FragNavPopController) : FragNavTabHistoryController
6 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/CollectionFragNavTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import android.os.Bundle
4 | import com.ncapdevi.fragnav.FragNavPopController
5 | import com.ncapdevi.fragnav.FragNavSwitchController
6 | import com.ncapdevi.fragnav.FragNavTransactionOptions
7 | import java.util.*
8 |
9 | abstract class CollectionFragNavTabHistoryController(fragNavPopController: FragNavPopController,
10 | private val fragNavSwitchController: FragNavSwitchController) : BaseFragNavTabHistoryController(fragNavPopController) {
11 |
12 | internal abstract val collectionSize: Int
13 |
14 | internal abstract val andRemoveIndex: Int
15 |
16 | internal abstract var history: ArrayList
17 |
18 | @Throws(UnsupportedOperationException::class)
19 | override fun popFragments(popDepth: Int,
20 | transactionOptions: FragNavTransactionOptions?): Boolean {
21 | var localDepth = popDepth
22 | var changed = false
23 | var switched: Boolean
24 | do {
25 | switched = false
26 | val count = fragNavPopController.tryPopFragments(localDepth, transactionOptions)
27 | if (count > 0) {
28 | changed = true
29 | switched = true
30 | localDepth -= count
31 | } else if (collectionSize > 1) {
32 | fragNavSwitchController.switchTab(andRemoveIndex, transactionOptions)
33 | localDepth--
34 | changed = true
35 | switched = true
36 | }
37 | } while (localDepth > 0 && switched)
38 | return changed
39 | }
40 |
41 | override fun restoreFromBundle(savedInstanceState: Bundle?) {
42 | if (savedInstanceState == null) {
43 | return
44 | }
45 | val arrayList = savedInstanceState.getIntegerArrayList(EXTRA_STACK_HISTORY)
46 | if (arrayList != null) {
47 | history = arrayList
48 | }
49 | }
50 |
51 | override fun onSaveInstanceState(outState: Bundle) {
52 | val history = history
53 | if (history.isEmpty()) {
54 | return
55 | }
56 | outState.putIntegerArrayList(EXTRA_STACK_HISTORY, history)
57 | }
58 |
59 | companion object {
60 | private const val EXTRA_STACK_HISTORY = "EXTRA_STACK_HISTORY"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/CurrentTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import android.os.Bundle
4 |
5 | import com.ncapdevi.fragnav.FragNavPopController
6 | import com.ncapdevi.fragnav.FragNavTransactionOptions
7 |
8 | class CurrentTabHistoryController(fragNavPopController: FragNavPopController) : BaseFragNavTabHistoryController(fragNavPopController) {
9 |
10 | @Throws(UnsupportedOperationException::class)
11 | override fun popFragments(popDepth: Int,
12 | transactionOptions: FragNavTransactionOptions?): Boolean {
13 | return fragNavPopController.tryPopFragments(popDepth, transactionOptions) > 0
14 | }
15 |
16 | override fun switchTab(index: Int) {}
17 |
18 | override fun onSaveInstanceState(outState: Bundle) {}
19 |
20 | override fun restoreFromBundle(savedInstanceState: Bundle?) {}
21 | }
22 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/FragNavTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import android.os.Bundle
4 | import androidx.annotation.IntDef
5 | import com.ncapdevi.fragnav.FragNavTransactionOptions
6 |
7 | interface FragNavTabHistoryController {
8 | /**
9 | * Define what happens when we try to pop on a tab where root fragment is at the top
10 | */
11 | @IntDef(CURRENT_TAB, UNIQUE_TAB_HISTORY, UNLIMITED_TAB_HISTORY)
12 | @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
13 | annotation class PopStrategy
14 |
15 | fun popFragments(popDepth: Int, transactionOptions: FragNavTransactionOptions?): Boolean
16 |
17 | fun switchTab(index: Int)
18 |
19 | fun onSaveInstanceState(outState: Bundle)
20 |
21 | fun restoreFromBundle(savedInstanceState: Bundle?)
22 |
23 | companion object {
24 | /**
25 | * We only pop fragments from current tab, don't switch between tabs
26 | */
27 | @Deprecated("This is the default behaviour and will need no extra setup")
28 | const val CURRENT_TAB = 0
29 |
30 | /**
31 | * We keep a history of tabs (each tab is present only once) and we switch to previous tab in history when we pop on root fragment
32 | */
33 | const val UNIQUE_TAB_HISTORY = 1
34 |
35 | /**
36 | * We keep an uncapped history of tabs and we switch to previous tab in history when we pop on root fragment
37 | */
38 | const val UNLIMITED_TAB_HISTORY = 2
39 | }
40 | }
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/NavigationStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import com.ncapdevi.fragnav.FragNavSwitchController
4 |
5 | sealed class NavigationStrategy
6 |
7 | class CurrentTabStrategy : NavigationStrategy()
8 |
9 | class UnlimitedTabHistoryStrategy(val fragNavSwitchController: FragNavSwitchController) : NavigationStrategy()
10 |
11 | class UniqueTabHistoryStrategy(val fragNavSwitchController: FragNavSwitchController) : NavigationStrategy()
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/UniqueTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import com.ncapdevi.fragnav.FragNavPopController
4 | import com.ncapdevi.fragnav.FragNavSwitchController
5 | import java.util.*
6 |
7 | class UniqueTabHistoryController(fragNavPopController: FragNavPopController,
8 | fragNavSwitchController: FragNavSwitchController) : CollectionFragNavTabHistoryController(fragNavPopController, fragNavSwitchController) {
9 | private val tabHistory = LinkedHashSet()
10 |
11 | override val collectionSize: Int
12 | get() = tabHistory.size
13 |
14 | override val andRemoveIndex: Int
15 | get() {
16 | val tabList = history
17 | val currentPage = tabList[tabHistory.size - 1]
18 | val targetPage = tabList[tabHistory.size - 2]
19 | tabHistory.remove(currentPage)
20 | tabHistory.remove(targetPage)
21 | return targetPage
22 | }
23 |
24 | override var history: ArrayList
25 | get() = ArrayList(tabHistory)
26 | set(history) {
27 | tabHistory.clear()
28 | tabHistory.addAll(history)
29 | }
30 |
31 | override fun switchTab(index: Int) {
32 | tabHistory.remove(index)
33 | tabHistory.add(index)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frag-nav/src/main/java/com/ncapdevi/fragnav/tabhistory/UnlimitedTabHistoryController.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import com.ncapdevi.fragnav.FragNavPopController
4 | import com.ncapdevi.fragnav.FragNavSwitchController
5 | import java.util.*
6 |
7 | class UnlimitedTabHistoryController(fragNavPopController: FragNavPopController,
8 | fragNavSwitchController: FragNavSwitchController) : CollectionFragNavTabHistoryController(fragNavPopController, fragNavSwitchController) {
9 | private val tabHistory = Stack()
10 |
11 | override val collectionSize: Int
12 | get() = tabHistory.size
13 |
14 | override val andRemoveIndex: Int
15 | get() {
16 | tabHistory.pop()
17 | return tabHistory.pop()
18 | }
19 |
20 | override var history: ArrayList
21 | get() = ArrayList(tabHistory)
22 | set(history) {
23 | tabHistory.clear()
24 | tabHistory.addAll(history)
25 | }
26 |
27 | override fun switchTab(index: Int) {
28 | tabHistory.push(index)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/frag-nav/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FragNav
3 |
4 |
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/FragNavControllerRaceConditionSpec.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.fragment.app.FragmentTransaction
6 | import com.ncapdevi.fragnav.tabhistory.UniqueTabHistoryStrategy
7 | import com.nhaarman.mockitokotlin2.*
8 | import org.amshove.kluent.shouldBeTrue
9 | import org.spekframework.spek2.Spek
10 | import org.spekframework.spek2.style.gherkin.Feature
11 | import java.util.concurrent.Executors
12 | import java.util.concurrent.TimeUnit
13 |
14 | const val SIMULATE_FRAGMENT_MANAGER_BEHAVIOR = true
15 |
16 | /**
17 | * To be able to run these tests in AS one have to install Spek 2 Idea plugin.
18 | * https://plugins.jetbrains.com/plugin/10915-spek-framework
19 | */
20 | object FragNavControllerRaceConditionSpec : Spek({
21 | Feature("FragNavController") {
22 | Scenario("A fragment navigation controller with two tabs (A, B) starting on A tab") {
23 | val fakeFragmentManager = FakeFragmentManager()
24 | val fragmentA = getFragmentMock(fakeFragmentManager)
25 | val fragmentB = getFragmentMock(fakeFragmentManager)
26 | val rootFragmentListenerMock = getRootFragmentListenerMock(listOf(fragmentA, fragmentB))
27 | val fragNavController = FragNavController(fakeFragmentManager.create(), 0)
28 | .apply {
29 | rootFragmentListener = rootFragmentListenerMock
30 | }
31 | When("switching from current tab A to a new Tab B, then switching swiftly to A then back B") {
32 | fragNavController.initialize()
33 | fragNavController.switchTab(1)
34 | fragNavController.switchTab(0)
35 | waitIfNecessary()
36 | }
37 | Then("should only add 1 A and 1 B fragment") {
38 | verify(rootFragmentListenerMock).getRootFragment(eq(0))
39 | verify(rootFragmentListenerMock).getRootFragment(eq(1))
40 | }
41 | And("should call attach and detach cycle correctly") {
42 | fakeFragmentManager.verify(
43 | listOf(
44 | Add(fragmentA, "androidx.fragment.app.Fragment1"),
45 | Commit,
46 | Detach(fragmentA),
47 | Add(fragmentB, "androidx.fragment.app.Fragment2"),
48 | Commit,
49 | Detach(fragmentB),
50 | Attach(fragmentA),
51 | Commit
52 | )
53 | )
54 | }
55 | }
56 | Scenario("A fragment navigation controller with two three (A, B, C) with eager initialization") {
57 | val fakeFragmentManager = FakeFragmentManager()
58 | val fragmentA = getFragmentMock(fakeFragmentManager)
59 | val fragmentB = getFragmentMock(fakeFragmentManager)
60 | val fragmentC = getFragmentMock(fakeFragmentManager)
61 | val rootFragmentListenerMock =
62 | getRootFragmentListenerMock(listOf(fragmentA, fragmentB, fragmentC))
63 | val fragNavController = FragNavController(fakeFragmentManager.create(), 0)
64 | .apply {
65 | rootFragmentListener = rootFragmentListenerMock
66 | createEager = true
67 | fragmentHideStrategy = FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH
68 | navigationStrategy = UniqueTabHistoryStrategy(mock())
69 | }
70 | When("swiftly switching to tab B") {
71 | fragNavController.initialize()
72 | fragNavController.switchTab(1)
73 | waitIfNecessary()
74 | }
75 | Then("should only add 1 A 1 B and 1 C fragment") {
76 | verify(rootFragmentListenerMock).getRootFragment(eq(0))
77 | verify(rootFragmentListenerMock).getRootFragment(eq(1))
78 | verify(rootFragmentListenerMock).getRootFragment(eq(2))
79 | }
80 | And("should call attach and detach cycle correctly") {
81 | fakeFragmentManager.verify(
82 | listOf(
83 | Add(fragmentA, "androidx.fragment.app.Fragment1"),
84 | Add(fragmentB, "androidx.fragment.app.Fragment2"),
85 | Hide(fragmentB),
86 | Add(fragmentC, "androidx.fragment.app.Fragment3"),
87 | Hide(fragmentC),
88 | Commit,
89 | Hide(fragmentA),
90 | Show(fragmentB),
91 | Commit
92 | )
93 | )
94 | }
95 | }
96 | }
97 | })
98 |
99 | private fun waitIfNecessary() {
100 | if (SIMULATE_FRAGMENT_MANAGER_BEHAVIOR) {
101 | // Make sure we wait for all commits to be done
102 | Thread.sleep(1000)
103 | }
104 | }
105 |
106 | private fun getFragmentMock(fakeFragmentManager: FakeFragmentManager) = mock {
107 | on { isAdded } doAnswer { fakeFragmentManager.activeFragments.containsValue(this.mock) }
108 | on { isDetached } doAnswer { fakeFragmentManager.detachedFragments.contains(this.mock) }
109 | }
110 |
111 | private fun getRootFragmentListenerMock(fragments: List): FragNavController.RootFragmentListener {
112 | return mock {
113 | on { numberOfRootFragments } doReturn fragments.size
114 | on { getRootFragment(any()) } doAnswer { invocationOnMock ->
115 | fragments[invocationOnMock.getArgument(0)]
116 | }
117 | }
118 | }
119 |
120 | class FakeFragmentManager {
121 | private val operations = mutableListOf()
122 | val activeFragments = mutableMapOf()
123 | val detachedFragments = mutableSetOf()
124 |
125 | fun create(): FragmentManager {
126 | operations.clear()
127 | activeFragments.clear()
128 | detachedFragments.clear()
129 | return mock {
130 | on { findFragmentByTag(any()) } doAnswer { invocationOnMock -> activeFragments[invocationOnMock.getArgument(0)] }
131 | }.apply {
132 | doAnswer { FakeFragmentTransaction(this@FakeFragmentManager).create() }.whenever(this)
133 | .beginTransaction()
134 | }
135 | }
136 |
137 | fun add(tag: String, fragment: Fragment) {
138 | activeFragments += tag to fragment
139 | }
140 |
141 | fun remove(fragment: Fragment) {
142 | activeFragments.values.removeAll {
143 | it == fragment
144 | }
145 | detachedFragments -= fragment
146 | }
147 |
148 | fun operation(fakeFragmentOperation: FakeFragmentOperation) {
149 | operations += fakeFragmentOperation
150 | }
151 |
152 | fun verify(expectedOperations: List) {
153 | (expectedOperations == operations).shouldBeTrue()
154 | }
155 |
156 | fun attach(fragment: Fragment) {
157 | detachedFragments -= fragment
158 | }
159 |
160 | fun detach(fragment: Fragment) {
161 | detachedFragments += fragment
162 | }
163 | }
164 |
165 | class FakeFragmentTransaction(private val parent: FakeFragmentManager) {
166 | private val pendingActions = mutableListOf()
167 | private val executor = Executors.newSingleThreadScheduledExecutor()
168 |
169 | fun create(): FragmentTransaction {
170 | return mock {
171 | on { add(any(), any(), any()) } doAnswer { invocationOnMock ->
172 | pendingActions.add(Add(invocationOnMock.getArgument(1), invocationOnMock.getArgument(2)))
173 | this.mock
174 | }
175 | on { remove(any()) } doAnswer { invocationOnMock ->
176 | pendingActions.add(Remove(invocationOnMock.getArgument(0)))
177 | this.mock
178 | }
179 | on { attach(any()) } doAnswer { invocationOnMock ->
180 | pendingActions.add(Attach(invocationOnMock.getArgument(0)))
181 | this.mock
182 | }
183 | on { detach(any()) } doAnswer { invocationOnMock ->
184 | pendingActions.add(Detach(invocationOnMock.getArgument(0)))
185 | this.mock
186 | }
187 | on { show(any()) } doAnswer { invocationOnMock ->
188 | pendingActions.add(Show(invocationOnMock.getArgument(0)))
189 | this.mock
190 | }
191 | on { hide(any()) } doAnswer { invocationOnMock ->
192 | pendingActions.add(Hide(invocationOnMock.getArgument(0)))
193 | this.mock
194 | }
195 | on { commit() } doAnswer {
196 | commit()
197 | 0
198 | }
199 | on { commitAllowingStateLoss() } doAnswer {
200 | commit()
201 | 0
202 | }
203 | }
204 | }
205 |
206 | private fun commit() {
207 | executeIfNecessary {
208 | pendingActions.forEach {
209 | parent.operation(it)
210 | when (it) {
211 | is Add -> parent.add(it.tag, it.fragment)
212 | is Remove -> parent.remove(it.fragment)
213 | is Attach -> parent.attach(it.fragment)
214 | is Detach -> parent.detach(it.fragment)
215 | }
216 | }
217 | parent.operation(Commit)
218 | pendingActions.clear()
219 | }
220 | }
221 |
222 | private fun executeIfNecessary(block: () -> Unit) {
223 | if (SIMULATE_FRAGMENT_MANAGER_BEHAVIOR) {
224 | executor.apply {
225 | schedule({
226 | block()
227 | shutdown()
228 | }, 100, TimeUnit.MILLISECONDS)
229 | }
230 | } else {
231 | block()
232 | }
233 | }
234 | }
235 |
236 | sealed class FakeFragmentOperation
237 | data class Add(val fragment: Fragment, val tag: String) : FakeFragmentOperation()
238 | data class Attach(val fragment: Fragment) : FakeFragmentOperation()
239 | data class Detach(val fragment: Fragment) : FakeFragmentOperation()
240 | data class Remove(val fragment: Fragment) : FakeFragmentOperation()
241 | data class Hide(val fragment: Fragment) : FakeFragmentOperation()
242 | data class Show(val fragment: Fragment) : FakeFragmentOperation()
243 | object Commit : FakeFragmentOperation()
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/FragNavControllerTest.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 | import androidx.fragment.app.FragmentActivity
6 | import android.widget.FrameLayout
7 | import com.nhaarman.mockitokotlin2.any
8 | import com.nhaarman.mockitokotlin2.doReturn
9 | import com.nhaarman.mockitokotlin2.mock
10 | import com.nhaarman.mockitokotlin2.whenever
11 | import org.junit.Assert
12 | import org.junit.Before
13 | import org.junit.Test
14 | import org.junit.runner.RunWith
15 | import org.robolectric.Robolectric
16 | import org.robolectric.RobolectricTestRunner
17 | import java.util.*
18 |
19 |
20 | /**
21 | * Created by niccapdevila on 2/10/18.
22 | */
23 | @RunWith(RobolectricTestRunner::class)
24 | class FragNavControllerTest : FragNavController.TransactionListener {
25 | private val activity = Robolectric.buildActivity(FragmentActivity::class.java).create().get()
26 | private lateinit var mFragNavController: FragNavController
27 |
28 | private var fragmentManager = activity.supportFragmentManager
29 | private val frameLayout = FrameLayout(activity).apply { id = 1 }
30 |
31 | @Before
32 | fun setup() {
33 | activity.setContentView(frameLayout)
34 |
35 | }
36 |
37 | @Test
38 | fun testConstructionWhenRestoringFromBundle() {
39 | val rootFragments = ArrayList()
40 | rootFragments.add(Fragment())
41 | rootFragments.add(Fragment())
42 |
43 | val mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
44 | this.rootFragments = rootFragments
45 | }
46 | mFragNavController.initialize()
47 |
48 |
49 | mFragNavController.switchTab(FragNavController.TAB2)
50 | mFragNavController.pushFragment(Fragment())
51 | mFragNavController.pushFragment(Fragment())
52 | mFragNavController.pushFragment(Fragment())
53 |
54 | val currentFragment = mFragNavController.currentFrag
55 |
56 | val bundle = Bundle()
57 |
58 | mFragNavController.onSaveInstanceState(bundle)
59 |
60 | mFragNavController.initialize(savedInstanceState = bundle)
61 |
62 | Assert.assertEquals(FragNavController.TAB2.toLong(), mFragNavController.currentStackIndex.toLong())
63 | Assert.assertEquals(4, mFragNavController.currentStack!!.size.toLong())
64 | Assert.assertEquals(currentFragment, mFragNavController.currentFrag)
65 | }
66 |
67 | @Test
68 | fun testConstructionWhenMultipleFragments() {
69 | val rootFragments = ArrayList()
70 | rootFragments.add(Fragment())
71 | rootFragments.add(Fragment())
72 |
73 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
74 | this.rootFragments = rootFragments
75 |
76 | }
77 | mFragNavController.initialize()
78 |
79 | Assert.assertEquals(FragNavController.TAB1.toLong(), mFragNavController.currentStackIndex.toLong())
80 | Assert.assertNotNull(mFragNavController.currentStack)
81 | }
82 |
83 | @Test
84 | fun testConstructionWhenMultipleFragmentsEagerMode() {
85 | val rootFragments = ArrayList()
86 | rootFragments.add(Fragment())
87 | rootFragments.add(Fragment())
88 |
89 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
90 | this.rootFragments = rootFragments
91 | fragmentHideStrategy = FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH
92 | createEager = true
93 |
94 | }
95 |
96 | mFragNavController.initialize()
97 |
98 | Assert.assertEquals(FragNavController.TAB1.toLong(), mFragNavController.currentStackIndex.toLong())
99 | Assert.assertNotNull(mFragNavController.currentStack)
100 | Assert.assertEquals(mFragNavController.size.toLong(), 2)
101 | }
102 |
103 | @Test(expected = IllegalArgumentException::class)
104 | fun testConstructionWhenTooManyRootFragments() {
105 | val rootFragments = ArrayList()
106 |
107 | for (i in 0..20) {
108 | rootFragments.add(Fragment())
109 | }
110 |
111 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
112 | this.rootFragments = rootFragments
113 | }
114 | }
115 |
116 | @Test
117 | fun testConstructionWhenMultipleFragmentsAndNoTabSelected() {
118 | val rootFragments = ArrayList()
119 | rootFragments.add(Fragment())
120 | rootFragments.add(Fragment())
121 |
122 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
123 | this.rootFragments = rootFragments
124 | }
125 |
126 | mFragNavController.initialize(FragNavController.NO_TAB)
127 |
128 |
129 | Assert.assertEquals(FragNavController.NO_TAB.toLong(), mFragNavController.currentStackIndex.toLong())
130 | Assert.assertNull(mFragNavController.currentStack)
131 | }
132 |
133 | @Test
134 | fun testConstructionWhenRootFragmentListenerAndTabSelected() {
135 | val rootFragmentListener = mock()
136 | doReturn(Fragment()).whenever(rootFragmentListener)
137 | .getRootFragment(any())
138 | doReturn(5).whenever(rootFragmentListener).numberOfRootFragments
139 |
140 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
141 | this.rootFragmentListener = rootFragmentListener
142 | }
143 | mFragNavController.initialize(FragNavController.TAB3)
144 |
145 | Assert.assertEquals(FragNavController.TAB3.toLong(), mFragNavController.currentStackIndex.toLong())
146 | Assert.assertNotNull(mFragNavController.currentStack)
147 | }
148 |
149 | @Test(expected = IndexOutOfBoundsException::class)
150 | fun testConstructionWhenRootFragmentListenerAndTooManyTabs() {
151 | val rootFragmentListener = mock()
152 |
153 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
154 | this.rootFragmentListener = rootFragmentListener
155 |
156 | }
157 | mFragNavController.initialize(FragNavController.TAB20)
158 | }
159 |
160 |
161 | @Test
162 | fun pushPopClear() {
163 | mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
164 | transactionListener = this@FragNavControllerTest
165 | rootFragments = listOf(Fragment())
166 | }
167 |
168 | mFragNavController.initialize()
169 |
170 | Assert.assertEquals(FragNavController.TAB1.toLong(), mFragNavController.currentStackIndex.toLong())
171 | Assert.assertNotNull(mFragNavController.currentStack)
172 |
173 | var size = mFragNavController.currentStack!!.size
174 |
175 |
176 | mFragNavController.pushFragment(Fragment())
177 | Assert.assertTrue(mFragNavController.currentStack!!.size == ++size)
178 |
179 | mFragNavController.pushFragment(Fragment())
180 | Assert.assertTrue(mFragNavController.currentStack!!.size == ++size)
181 |
182 | mFragNavController.pushFragment(Fragment())
183 | Assert.assertTrue(mFragNavController.currentStack!!.size == ++size)
184 |
185 | mFragNavController.popFragment()
186 | Assert.assertTrue(mFragNavController.currentStack!!.size == --size)
187 |
188 | mFragNavController.clearStack()
189 | Assert.assertTrue(mFragNavController.currentStack!!.size == 1)
190 | Assert.assertTrue(mFragNavController.isRootFragment)
191 | }
192 |
193 | @Test
194 | fun testTabStackClear() {
195 | val rootFragments = ArrayList()
196 | rootFragments.add(Fragment())
197 | rootFragments.add(Fragment())
198 |
199 | val mFragNavController = FragNavController(fragmentManager, frameLayout.id).apply {
200 | this.rootFragments = rootFragments
201 | }
202 | mFragNavController.initialize()
203 |
204 | Assert.assertEquals(FragNavController.TAB1.toLong(), mFragNavController.currentStackIndex.toLong())
205 | Assert.assertNotNull(mFragNavController.currentStack)
206 |
207 | var size = mFragNavController.currentStack?.size ?: 1
208 |
209 | mFragNavController.pushFragment(Fragment())
210 | Assert.assertTrue(mFragNavController.currentStack?.size == ++size)
211 |
212 | mFragNavController.pushFragment(Fragment())
213 | Assert.assertTrue(mFragNavController.currentStack?.size == ++size)
214 |
215 | mFragNavController.switchTab(FragNavController.TAB2)
216 |
217 | mFragNavController.clearStack(FragNavController.TAB1)
218 |
219 | mFragNavController.switchTab(FragNavController.TAB1)
220 | Assert.assertTrue(mFragNavController.currentStack?.size == 1)
221 | }
222 |
223 | override fun onTabTransaction(fragment: Fragment?, index: Int) {
224 | Assert.assertNotNull(fragment)
225 | }
226 |
227 | override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) {
228 | Assert.assertNotNull(mFragNavController)
229 |
230 | }
231 | }
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/FragNavTransactionOptionsTest.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav
2 |
3 | import androidx.annotation.AnimRes
4 | import androidx.annotation.StyleRes
5 | import androidx.fragment.app.FragmentTransaction
6 | import com.nhaarman.mockitokotlin2.mock
7 | import org.junit.Assert.assertTrue
8 | import org.junit.Test
9 |
10 | /**
11 | * Created by niccapdevila on 2/15/17.
12 | */
13 |
14 | class FragNavTransactionOptionsTest {
15 |
16 | @Test
17 | fun buildTransactionOptions() {
18 | val breadCrumbShortTitle = "Short Title"
19 | val breadCrumbTitle = "Long Title"
20 |
21 | @AnimRes
22 | val enterAnim = 1
23 | @AnimRes
24 | val exitAnim = 2
25 | @AnimRes
26 | val popEnterAnim = 3
27 | @AnimRes
28 | val popExitAnim = 4
29 |
30 | @StyleRes
31 | val transitionStyle = 5
32 |
33 | val fragNavTransactionOptions = FragNavTransactionOptions.newBuilder()
34 | .breadCrumbShortTitle(breadCrumbShortTitle)
35 | .breadCrumbTitle(breadCrumbTitle)
36 | .transition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
37 | .customAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
38 | .transitionStyle(transitionStyle)
39 | .addSharedElement(Pair(mock(), "test"))
40 | .addSharedElement(Pair(mock(), "test2")).build()
41 |
42 | assertTrue(breadCrumbShortTitle.equals(fragNavTransactionOptions.breadCrumbShortTitle, ignoreCase = true))
43 | assertTrue(breadCrumbTitle.equals(fragNavTransactionOptions.breadCrumbTitle, ignoreCase = true))
44 |
45 | assertTrue(transitionStyle == fragNavTransactionOptions.transitionStyle)
46 |
47 | assertTrue(FragmentTransaction.TRANSIT_FRAGMENT_FADE == fragNavTransactionOptions.transition)
48 |
49 |
50 | assertTrue(enterAnim == fragNavTransactionOptions.enterAnimation)
51 | assertTrue(exitAnim == fragNavTransactionOptions.exitAnimation)
52 | assertTrue(popEnterAnim == fragNavTransactionOptions.popEnterAnimation)
53 | assertTrue(popExitAnim == fragNavTransactionOptions.popExitAnimation)
54 |
55 |
56 | assertTrue(fragNavTransactionOptions.sharedElements.size == 2)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/tabhistory/CurrentTabHistoryControllerTest.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import com.ncapdevi.fragnav.FragNavPopController
4 | import com.nhaarman.mockitokotlin2.isNull
5 | import com.nhaarman.mockitokotlin2.times
6 | import com.nhaarman.mockitokotlin2.verify
7 | import org.junit.Test
8 | import org.junit.runner.RunWith
9 | import org.mockito.ArgumentMatchers.eq
10 | import org.mockito.Mock
11 | import org.mockito.junit.MockitoJUnitRunner
12 |
13 | @RunWith(MockitoJUnitRunner::class)
14 | class CurrentTabHistoryControllerTest {
15 | @Mock
16 | private lateinit var mockFragNavPopController: FragNavPopController
17 |
18 | @Test
19 | fun testPopDelegatedWhenPopCalled() {
20 | // Given
21 | val currentTabHistoryController = CurrentTabHistoryController(mockFragNavPopController)
22 |
23 | // When
24 | currentTabHistoryController.popFragments(1, null)
25 |
26 | // Then
27 | verify(mockFragNavPopController, times(1)).tryPopFragments(eq(1), isNull())
28 | }
29 | }
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/tabhistory/UniqueTabHistoryControllerTest.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import android.os.Bundle
4 | import com.ncapdevi.fragnav.FragNavPopController
5 | import com.ncapdevi.fragnav.FragNavSwitchController
6 | import com.ncapdevi.fragnav.FragNavTransactionOptions
7 | import com.nhaarman.mockitokotlin2.*
8 | import junit.framework.Assert.*
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.mockito.Mock
13 | import org.mockito.junit.MockitoJUnitRunner
14 | import java.util.*
15 |
16 | @RunWith(MockitoJUnitRunner::class)
17 | class UniqueTabHistoryControllerTest {
18 | @Mock
19 | private lateinit var mockFragNavPopController: FragNavPopController
20 |
21 | @Mock
22 | private lateinit var mockFragNavSwitchController: FragNavSwitchController
23 |
24 | @Mock
25 | private lateinit var mockBundle: Bundle
26 |
27 | @Mock
28 | private lateinit var mockTransactionOptions: FragNavTransactionOptions
29 |
30 | private lateinit var uniqueTabHistoryController: UniqueTabHistoryController
31 |
32 | @Before
33 | fun setUp() {
34 | uniqueTabHistoryController = UniqueTabHistoryController(mockFragNavPopController, mockFragNavSwitchController)
35 | mockNavSwitchController(uniqueTabHistoryController)
36 | mockBundle()
37 | }
38 |
39 | @Test
40 | fun testNoSwitchWhenCurrentStackIsLargerThanPopCount() {
41 | // Given
42 | uniqueTabHistoryController.switchTab(1)
43 | uniqueTabHistoryController.switchTab(2)
44 | whenever(mockFragNavPopController.tryPopFragments(eq(1), eq(mockTransactionOptions))).thenReturn(1)
45 |
46 | // When
47 | val result = uniqueTabHistoryController.popFragments(1, mockTransactionOptions)
48 |
49 | // Then
50 | assertTrue(result)
51 | verify(mockFragNavSwitchController, never()).switchTab(any(), anyOrNull())
52 | }
53 |
54 | @Test
55 | fun testPopDoesNothingWhenPopIsCalledWithNothingToPopWithNoHistory() {
56 | // Given
57 | uniqueTabHistoryController.switchTab(1)
58 |
59 | // When
60 | val result = uniqueTabHistoryController.popFragments(1, mockTransactionOptions)
61 |
62 | // Then
63 | assertFalse(result)
64 | verify(mockFragNavSwitchController, never()).switchTab(any(), anyOrNull())
65 | }
66 |
67 | @Test
68 | fun testPopSwitchesTabWhenPopIsCalledWithNothingToPopAndHasHistory() {
69 | // Given
70 | uniqueTabHistoryController.switchTab(1)
71 | uniqueTabHistoryController.switchTab(2)
72 |
73 | // When
74 | val result = uniqueTabHistoryController.popFragments(1, mockTransactionOptions)
75 |
76 | // Then
77 | assertTrue(result)
78 | verify(mockFragNavSwitchController, times(1)).switchTab(eq(1), eq(mockTransactionOptions))
79 | }
80 |
81 | @Test
82 | fun testSwitchWhenCurrentStackIsNotLargerThanPopCount() {
83 | // Given
84 | uniqueTabHistoryController.switchTab(1)
85 | uniqueTabHistoryController.switchTab(2)
86 | whenever(mockFragNavPopController.tryPopFragments(eq(2), eq(mockTransactionOptions))).thenReturn(1)
87 |
88 | // When
89 | val result = uniqueTabHistoryController.popFragments(2, mockTransactionOptions)
90 |
91 | // Then
92 | assertTrue(result)
93 | verify(mockFragNavSwitchController, times(1)).switchTab(eq(1), eq(mockTransactionOptions))
94 | }
95 |
96 | @Test
97 | fun testTabsUniquelyRollbackWhenPopAllAvailableItemsInOneStep() {
98 | // Given
99 |
100 | // Navigating ahead through tabs
101 | for (i in 1..5) {
102 | uniqueTabHistoryController.switchTab(i)
103 | }
104 |
105 | // Navigating backwards through tabs
106 | for (i in 5 downTo 1) {
107 | uniqueTabHistoryController.switchTab(i)
108 | }
109 |
110 | // Every tab contains 1 item
111 | run {
112 | var i = 7
113 | while (i < 16) {
114 | whenever(mockFragNavPopController.tryPopFragments(eq(i), eq(mockTransactionOptions))).thenReturn(1)
115 | i += 2
116 | }
117 | }
118 |
119 | // When
120 | val result = uniqueTabHistoryController.popFragments(15, mockTransactionOptions)
121 |
122 | // Then
123 | assertTrue(result)
124 | argumentCaptor().apply {
125 | verify(mockFragNavSwitchController, times(4)).switchTab(capture(), eq(mockTransactionOptions))
126 |
127 | // The history is [2, 3, 4, 5]
128 | for (i in 1..4) {
129 | assertEquals(allValues[i - 1], i + 1)
130 | }
131 | }
132 | }
133 |
134 | @Test
135 | fun testTabsUniquelyRollbackWhenPopAllAvailableItemsInDifferentSteps() {
136 | // Given
137 |
138 | // Navigating ahead through tabs
139 | for (i in 1..5) {
140 | uniqueTabHistoryController.switchTab(i)
141 | }
142 |
143 | // Navigating backwards through tabs
144 | for (i in 5 downTo 1) {
145 | uniqueTabHistoryController.switchTab(i)
146 | }
147 |
148 | // When
149 | for (i in 0..3) {
150 | assertTrue(uniqueTabHistoryController.popFragments(1, mockTransactionOptions))
151 | }
152 | assertFalse(uniqueTabHistoryController.popFragments(1, mockTransactionOptions))
153 |
154 | // Then
155 | argumentCaptor().apply {
156 | verify(mockFragNavSwitchController, times(4)).switchTab(capture(), eq(mockTransactionOptions))
157 |
158 | // The history is [2, 3, 4, 5]
159 | for (i in 1..4) {
160 | assertEquals(allValues[i - 1], i + 1)
161 | }
162 | }
163 | }
164 |
165 | @Test
166 | fun testHistoryIsSavedAndRestoredWhenSaveCalledNewInstanceCreatedRestoreCalled() {
167 | // Given
168 | for (i in 5 downTo 1) {
169 | uniqueTabHistoryController.switchTab(i)
170 | }
171 |
172 | // When
173 | uniqueTabHistoryController.onSaveInstanceState(mockBundle)
174 | val newUniqueTabHistoryController =
175 | UniqueTabHistoryController(mockFragNavPopController, mockFragNavSwitchController)
176 | mockNavSwitchController(newUniqueTabHistoryController)
177 | newUniqueTabHistoryController.restoreFromBundle(mockBundle)
178 | for (i in 0..3) {
179 | assertTrue(newUniqueTabHistoryController.popFragments(1, mockTransactionOptions))
180 | }
181 |
182 | // Then
183 | argumentCaptor().apply {
184 | verify(mockFragNavSwitchController, times(4)).switchTab(capture(), eq(mockTransactionOptions))
185 |
186 | // The history is [2, 3, 4, 5]
187 | for (i in 1..4) {
188 | assertEquals(allValues[i - 1], i + 1)
189 | }
190 | }
191 | }
192 |
193 | private fun mockNavSwitchController(uniqueTabHistoryController: UniqueTabHistoryController) {
194 | doAnswer { invocation ->
195 | uniqueTabHistoryController.switchTab(invocation.getArgument(0) as Int)
196 | null
197 | }.whenever(mockFragNavSwitchController).switchTab(any(), anyOrNull())
198 | }
199 |
200 | private fun mockBundle() {
201 | val storage = ArrayList()
202 | doAnswer { invocation ->
203 | storage.clear()
204 | storage.addAll(invocation.getArgument>(1))
205 | null
206 | }.whenever(mockBundle).putIntegerArrayList(any(), any())
207 | doAnswer {
208 | if (storage.size > 0) {
209 | storage
210 | } else null
211 | }.whenever(mockBundle).getIntegerArrayList(any())
212 | }
213 | }
--------------------------------------------------------------------------------
/frag-nav/src/test/java/com/ncapdevi/fragnav/tabhistory/UnlimitedTabHistoryControllerTest.kt:
--------------------------------------------------------------------------------
1 | package com.ncapdevi.fragnav.tabhistory
2 |
3 | import android.os.Bundle
4 | import com.ncapdevi.fragnav.FragNavPopController
5 | import com.ncapdevi.fragnav.FragNavSwitchController
6 | import com.ncapdevi.fragnav.FragNavTransactionOptions
7 | import com.nhaarman.mockitokotlin2.*
8 | import junit.framework.Assert.*
9 | import org.junit.Before
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 | import org.mockito.Mock
13 | import org.mockito.junit.MockitoJUnitRunner
14 |
15 | @RunWith(MockitoJUnitRunner::class)
16 | class UnlimitedTabHistoryControllerTest {
17 | @Mock
18 | private lateinit var mockFragNavPopController: FragNavPopController
19 |
20 | @Mock
21 | private lateinit var mockFragNavSwitchController: FragNavSwitchController
22 |
23 | @Mock
24 | private lateinit var mockBundle: Bundle
25 |
26 | @Mock
27 | private lateinit var mockTransactionOptions: FragNavTransactionOptions
28 |
29 | private lateinit var unlimitedTabHistoryController: UnlimitedTabHistoryController
30 |
31 | @Before
32 | fun setUp() {
33 | unlimitedTabHistoryController =
34 | UnlimitedTabHistoryController(mockFragNavPopController, mockFragNavSwitchController)
35 | mockNavSwitchController(unlimitedTabHistoryController)
36 | mockBundle()
37 | }
38 |
39 | @Test
40 | fun testNoSwitchWhenCurrentStackIsLargerThanPopCount() {
41 | // Given
42 | unlimitedTabHistoryController.switchTab(1)
43 | unlimitedTabHistoryController.switchTab(2)
44 | whenever(mockFragNavPopController.tryPopFragments(eq(1), eq(mockTransactionOptions))).thenReturn(1)
45 |
46 | // When
47 | val result = unlimitedTabHistoryController.popFragments(1, mockTransactionOptions)
48 |
49 | // Then
50 | assertTrue(result)
51 | verify(mockFragNavSwitchController, never()).switchTab(any(), anyOrNull())
52 | }
53 |
54 | @Test
55 | fun testPopDoesNothingWhenPopIsCalledWithNothingToPopWithNoHistory() {
56 | // Given
57 | unlimitedTabHistoryController.switchTab(1)
58 |
59 | // When
60 | val result = unlimitedTabHistoryController.popFragments(1, mockTransactionOptions)
61 |
62 | // Then
63 | assertFalse(result)
64 | verify(mockFragNavSwitchController, never()).switchTab(any(), anyOrNull())
65 | }
66 |
67 | @Test
68 | fun testPopSwitchesTabWhenPopIsCalledWithNothingToPopAndHasHistory() {
69 | // Given
70 | unlimitedTabHistoryController.switchTab(1)
71 | unlimitedTabHistoryController.switchTab(2)
72 |
73 | // When
74 | val result = unlimitedTabHistoryController.popFragments(1, mockTransactionOptions)
75 |
76 | // Then
77 | assertTrue(result)
78 | verify(mockFragNavSwitchController, times(1)).switchTab(eq(1), eq(mockTransactionOptions))
79 | }
80 |
81 | @Test
82 | fun testSwitchWhenCurrentStackIsNotLargerThanPopCount() {
83 | // Given
84 | unlimitedTabHistoryController.switchTab(1)
85 | unlimitedTabHistoryController.switchTab(2)
86 | whenever(mockFragNavPopController.tryPopFragments(eq(2), eq(mockTransactionOptions))).thenReturn(1)
87 |
88 | // When
89 | val result = unlimitedTabHistoryController.popFragments(2, mockTransactionOptions)
90 |
91 | // Then
92 | assertTrue(result)
93 | verify(mockFragNavSwitchController, times(1)).switchTab(eq(1), eq(mockTransactionOptions))
94 | }
95 |
96 | @Test
97 | fun testTabsUnlimitedRollbackWhenPopAllAvailableItemsInOneStep() {
98 | // Given
99 |
100 | // Navigating ahead through tabs
101 | for (i in 1..5) {
102 | unlimitedTabHistoryController.switchTab(i)
103 | }
104 |
105 | // Navigating backwards through tabs
106 | for (i in 5 downTo 1) {
107 | unlimitedTabHistoryController.switchTab(i)
108 | }
109 |
110 | // Every tab contains 1 item
111 | var index = 7
112 | while (index < 16) {
113 | whenever(mockFragNavPopController.tryPopFragments(eq(index), eq(mockTransactionOptions))).thenReturn(1)
114 | index += 2
115 | }
116 |
117 | // When
118 | val result = unlimitedTabHistoryController.popFragments(15, mockTransactionOptions)
119 |
120 | // Then
121 | assertTrue(result)
122 | argumentCaptor().apply {
123 | verify(mockFragNavSwitchController, times(9)).switchTab(capture(), eq(mockTransactionOptions))
124 |
125 | // The history is [2, 3, 4, 5, 5, 4, 3, 2, 1]
126 | for (i in 1..4) {
127 | assertEquals(allValues[i - 1], i + 1)
128 | }
129 | for (i in 4..8) {
130 | assertEquals(allValues[i], 9 - i)
131 | }
132 | }
133 | }
134 |
135 | @Test
136 | fun testTabsUnlimitedRollbackWhenPopAllAvailableItemsInDifferentSteps() {
137 | // Given
138 |
139 | // Navigating ahead through tabs
140 | for (i in 1..5) {
141 | unlimitedTabHistoryController.switchTab(i)
142 | }
143 |
144 | // Navigating backwards through tabs
145 | for (i in 5 downTo 1) {
146 | unlimitedTabHistoryController.switchTab(i)
147 | }
148 |
149 | // When
150 | for (i in 0..8) {
151 | assertTrue(unlimitedTabHistoryController.popFragments(1, mockTransactionOptions))
152 | }
153 | assertFalse(unlimitedTabHistoryController.popFragments(1, mockTransactionOptions))
154 |
155 | // Then
156 | argumentCaptor().apply {
157 | verify(mockFragNavSwitchController, times(9)).switchTab(
158 | capture(),
159 | eq(mockTransactionOptions)
160 | )
161 |
162 | // The history is [2, 3, 4, 5, 5, 4, 3, 2, 1]
163 | for (i in 1..4) {
164 | assertEquals(allValues[i - 1], i + 1)
165 | }
166 | for (i in 4..8) {
167 | assertEquals(allValues[i], 9 - i)
168 | }
169 | }
170 | }
171 |
172 | @Test
173 | fun testHistoryIsSavedAndRestoredWhenSaveCalledNewInstanceCreatedRestoreCalled() {
174 | // Given
175 | for (i in 5 downTo 1) {
176 | unlimitedTabHistoryController.switchTab(i)
177 | }
178 |
179 | // When
180 | unlimitedTabHistoryController.onSaveInstanceState(mockBundle)
181 | val newUniqueTabHistoryController = UnlimitedTabHistoryController(
182 | mockFragNavPopController,
183 | mockFragNavSwitchController
184 | )
185 | mockNavSwitchController(newUniqueTabHistoryController)
186 | newUniqueTabHistoryController.restoreFromBundle(mockBundle)
187 | for (i in 0..3) {
188 | assertTrue(newUniqueTabHistoryController.popFragments(1, mockTransactionOptions))
189 | }
190 |
191 | // Then
192 | argumentCaptor().apply {
193 | verify(mockFragNavSwitchController, times(4)).switchTab(
194 | capture(),
195 | eq(mockTransactionOptions)
196 | )
197 |
198 | // The history is [2, 3, 4, 5]
199 | for (i in 1..4) {
200 | assertEquals(allValues[i - 1], i + 1)
201 | }
202 | }
203 | }
204 |
205 | private fun mockNavSwitchController(uniqueTabHistoryController: UnlimitedTabHistoryController) {
206 | doAnswer { invocation ->
207 | uniqueTabHistoryController.switchTab(invocation.getArgument(0) as Int)
208 | null
209 | }.whenever(mockFragNavSwitchController).switchTab(any(), anyOrNull())
210 | }
211 |
212 | private fun mockBundle() {
213 | val storage = ArrayList()
214 | doAnswer { invocation ->
215 | storage.clear()
216 | storage.addAll(invocation.getArgument>(1))
217 | null
218 | }.whenever(mockBundle).putIntegerArrayList(any(), any())
219 | doAnswer {
220 | if (storage.size > 0) {
221 | storage
222 | } else null
223 | }.whenever(mockBundle).getIntegerArrayList(any())
224 | }
225 | }
--------------------------------------------------------------------------------
/frag-nav/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | android.enableJetifier=true
20 | android.useAndroidX=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncapdevi/FragNav/498317c96c4436edd219f2bc10aa9e4ec3a94720/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jun 24 19:53:12 CDT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save ( ) {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':frag-nav'
2 |
--------------------------------------------------------------------------------