I tried to load a Fragment in Compose as below, through the supportFragmentManager as shown below.
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidViewBinding(FragmentContainerBinding::inflate) {
supportFragmentManager.beginTransaction()
.replace(container.id, MyFragment()).commit()
}
}
}
}
However, when the view is shown, the fragment gets committed (loaded) several times (i.e. the onCreate() is called several times)
Any way to prevent committing several times?
Is there a way to resume the state as well (e.g. in case got killed by the system, how to get it restored)?
(note: I'm not using the androidx.fragment.app.FragmentContainerView in the XML as in Developer Doc I do have different fragments per some logic (not shown here), hence I'll have to use supportFragmentManager)
Found a way to get this working
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
FragmentContainer(
modifier = Modifier.fillMaxSize(),
fragmentManager = supportFragmentManager,
commit = { add(it, MyFragment()) }
)
}
}
}
#Composable
fun FragmentContainer(
modifier: Modifier = Modifier,
fragmentManager: FragmentManager,
commit: FragmentTransaction.(containerId: Int) -> Unit
) {
val containerId by rememberSaveable { mutableStateOf(View.generateViewId()) }
AndroidView(
modifier = modifier,
factory = { context ->
fragmentManager.findFragmentById(containerId)?.view
?.also { (it.parent as? ViewGroup)?.removeView(it) }
?: FragmentContainerView(context)
.apply { id = containerId }
.also {
fragmentManager.commit { commit(it.id) }
}
}
)
}
Note this will need Fragment's KTX
implementation "androidx.fragment:fragment-ktx:1.4.1"
Related
I'm using branch.io for handling deep links. Deep links can contain custom metadata in a form of JsonObject. The data can be obtained by setting up a listener, inside MainActivity#onStart() which is triggered when a link is clicked.
override fun onStart() {
super.onStart()
Branch
.sessionBuilder(this)
.withCallback { referringParams, error ->
if (error == null) {
val eventId = referringParams?.getString("id")
//Here I would like to navigate user to event screen
} else {
Timber.e(error.message)
}
}
.withData(this.intent?.data).init()
}
When I retrieve eventId from referringParams I have to navigate the user to the specific event. When I was using Navigation components with fragments I could just do:
findNavController(R.id.navHost).navigate("path to event screen")
But with compose is different because I can't use navController outside of Composable since its located in MainActivity#onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
//I cant access navController outside of composable function
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "HomeScreen",
) {
}
}
}
My question is, how can I navigate the user to a specific screen from MainActivity#onStart() when using jetpack compose navigation
rememberNavController has pretty simple implementation: it creates NavHostController with two navigators, needed by Compose, and makes sure it's restored on configuration change.
Here's how you can do the same in your activity, outside of composable scope:
private lateinit var navController: NavHostController
private val navControllerBundleKey = "navControllerBundleKey"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navController = NavHostController(this).apply {
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
savedInstanceState
?.getBundle(navControllerBundleKey)
?.apply(navController::restoreState)
setContent {
// pass navController to NavHost
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBundle(navControllerBundleKey, navController.saveState())
super.onSaveInstanceState(outState)
}
I'm trying to implement a first time using screen (like any other app when you have to fill some options before using the app for the first time).
I can't go to another Jetpack compose on an main activity on-create state because it check that every recomposition, and take me to the navigation path (I'd like to check the datastore entry once during launch), this what I already try, not seem to be working:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val onBoardingStatus = dataStoreManager.onBoard.first()
setContent {
val navController = rememberNavController()
OnBoardingNavHost(navController)
navController.navigate(if (onBoardingStatus) "on_boarding" else "main") {
launchSingleTop = true
popUpTo(0)
}
}
}
}
it is possible to check that only once (in application class for example and not in oncreate?)
please advice,
thanks in advance
You have to use LaunchedEffect for this, you can do something like this
enum class OnboardState {
Loading,
NoOnboarded,
Onboarded,
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var onboardingState by remember {
mutableStateOf(OnboardState.Loading)
}
LaunchedEffect(Unit) {
onboardingState = getOnboardingState()
}
when (onboardingState) {
OnboardState.Loading -> showSpinner()
OnboardState.NoOnboarded -> LaunchedEffect(onboardingState) {
navigateToOnboarding()
}
OnboardState.Onboarded -> showContent()
}
}
}
I am using lifecycleScope.launch in my activity's onCreate to collect a flow but I also am trying to attach a fragment inside the scope but I just get a black screen when trying to do this like it never gets attached.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
If I dont use lifecycleScope and implement CoroutineScope in my activity with that coroutine scope the fragment attaches fine
class MainActivity : AppCompatActivity(), CoroutineScope{
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
_viewModel.listenForScheduleChanges().flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
}
}
launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
}
I dont understand why, both appear to be using the same context with the Dispatcher as Main.
Can someone provide insight here?
Separate the code that collects data from flow to another coroutine scope
your problem will be solved:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
_viewModel.listenForScheduleChanges().collect {
}
}
}
lifecycleScope.launch {
_viewModel.loadConfig() // suspend method that loads information
_webFragment = WebFragment().apply {
arguments = Bundle().apply {
putParcelable("config", _viewModel.config)
}
}
supportFragmentManager.beginTransaction()
.replace(R.id.contentPanel, _webFragment!!)
.commit()
}
}
the issue is that when you collect a flow in this way it blocks the whole scope and other codes in the scope are not running.
Example, If I replaced 'fragmentA' with 'fragmentB', the 'viewModelA' of fragmentA is still live. why ?
onCreate() of Fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider.NewInstanceFactory().create(InvoicesViewModel::class.java)
}
ViewModel
class InvoicesViewModel : ViewModel() {
init {
getInvoices()
}
private fun getInvoices() {
viewModelScope.launch {
val response = safeApiCall() {
// Call API here
}
while (true) {
delay(1000)
println("Still printing although the fragment of this viewModel destroied")
}
if (response is ResultWrapper.Success) {
// Do work here
}
}
}
}
This method used to replace fragment
fun replaceFragment(activity: Context, fragment: Fragment, TAG: String) {
val myContext = activity as AppCompatActivity
val transaction = myContext.supportFragmentManager.beginTransaction()
transaction.replace(R.id.content_frame, fragment, TAG)
transaction.commitNow()
}
You will note the while loop inside the Coroutine still work although after replace fragment to another fragment.
this is about your implementation of ViewModelProvider.
use this way for creating your viewModel.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(InvoicesViewModel::class.java)
}
in this way you give your fragment as live scope of view model.
Check, if you have created the ViewModel in Activity passing the context of activity or fragment.
Guess I'm missing something obvious here but... I'm storing data in uiModel in the DiaryViewModel class, and since I use architecture components I'm expecting the data to be retained through screen rotation - but it doesn't. I'm blind to why.
Here's a stripped down fragment
class DiaryFragment: Fragment() {
private lateinit var viewModel: DiaryViewModel
override onCreateView(...) {
viewModel = ViewModelProviders.of(this).get(DiaryViewModel::class.java)
viewModel.getModel().observe(this, Observer<DiaryUIModel> { uiModel ->
render(uiModel)
})
}
}
And the corresponding view model.
class DiaryViewModel: ViewModel() {
private var uiModel: MutableLiveData<DiaryUIModel>? = null
fun getModel(): LiveData<DiaryUIModel> {
if (uiModel == null) {
uiModel = MutableLiveData<DiaryUIModel>()
uiModel?.value = DiaryUIModel()
}
return uiModel as MutableLiveData<DiaryUIModel>
}
}
Can any one see what's missing in this simple example? Right now, uiModel is set to null when rotating the screen.
The issue was with how the activity was handling the fragment creation. MainActivity was always creating a new fragment per rotation, as in
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager
.beginTransaction()
.replace(overlay.id, DiaryFragment.newInstance())
.commit()
}
But of course, it works much better when checking if we have a saved instance, as in
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.replace(overlay.id, DiaryFragment.newInstance())
.commit()
}
}