单击底部导航视图时数据重置
Data resets when bottom navigation views are clicked
我的天气应用程序有一个底部导航视图,包含 3 个面板(今天、每小时和每天)。我的 activity 为 3 个面板托管了 3 个片段。目前,如果我在 today 片段上搜索任何城市,它会提供这些城市的数据。现在的问题是,如果我点击任何底部导航视图(向下),它会重置今天片段上显示的数据。
这是问题的说明:
搜索城市后显示的数据(打红勾的部分):
单击这些底部导航视图(带有红色勾号的部分)后,数据将重置:
无论单击那些导航视图,我都希望数据保持完整。
我尝试使用 建议。但它失败了
异常:
java.lang.IllegalArgumentException: No view found for id 0x7f0a0116 (com.viz.lightweatherforecast:id/my_nav) for fragment ThirdFragment{90bc0de} (8e129d17-010d-41dc-9311-82e273b4e522 id=0x7f0a0116 tag=3)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:513)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3134)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:3068)
at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:251)
at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:501)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1391)
at android.app.Activity.performStart(Activity.java:7157)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3037)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1861)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6819)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)
I/weatherforecas: Compiler allocated 4MB to compile void android.widget.TextView.<init>(android.content.Context, android.util.AttributeSet, int, int)
我觉得很难实现他的代码,关于那个问题的其余建议都是用 kotlin 写的。
请问我该如何解决这个问题?
这是我的代码:
my_nav.xml(导航布局):
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_nav"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.viz.lightweatherforecast.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/secondFragment"
android:name="com.viz.lightweatherforecast.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
<fragment
android:id="@+id/thirdFragment"
android:name="com.viz.lightweatherforecast.ThirdFragment"
android:label="fragment_third"
tools:layout="@layout/fragment_third" />
</navigation>
activity_home.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/layout"
android:background="@drawable/dubai"
tools:context=".Activity.HomeActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="409dp"
android:layout_height="599dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:navGraph="@navigation/my_nav"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
主页活动:
public class HomeActivity extends AppCompatActivity {
// Last update time, click sound, search button, search panel.
TextView time_field;
MediaPlayer player;
ImageView Search;
EditText textfield;
// For scheduling background image change(using constraint layout, start counting from dubai, down to statue of liberty.
ConstraintLayout constraintLayout;
public static int count=0;
int[] drawable =new int[]{R.drawable.dubai,R.drawable.central_bank_of_nigeria,R.drawable.eiffel_tower,R.drawable.hong_kong,R.drawable.statue_of_liberty};
Timer _t;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
// use home activity layout.
time_field = findViewById(R.id.textView9);
Search = findViewById(R.id.imageView4);
textfield = findViewById(R.id.textfield);
// find the id's of specific variables.
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
// host 3 fragments along with bottom navigation.
final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
assert navHostFragment != null;
final NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNavigationView, navController);
// For scheduling background image change
constraintLayout = findViewById(R.id.layout);
constraintLayout.setBackgroundResource(R.drawable.dubai);
_t = new Timer();
_t.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// run on ui thread
runOnUiThread(() -> {
if (count < drawable.length) {
constraintLayout.setBackgroundResource(drawable[count]);
count = (count + 1) % drawable.length;
}
});
}
}, 5000, 5000);
Search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// make click sound when search button is clicked.
player = MediaPlayer.create(HomeActivity.this, R.raw.click);
player.start();
getWeatherData(textfield.getText().toString().trim());
// make use of some fragment's data
FirstFragment firstFragment = (FirstFragment) navHostFragment.getChildFragmentManager().getFragments().get(0);
firstFragment.getWeatherData(textfield.getText().toString().trim());
}
});
}
});
}
}
编辑
片段class:
public class FirstFragment extends Fragment {
// User current time, current temperature, current condition, sunrise, sunset, temperature, pressure, humidity, wind_speed, visibility, clouds
TextView current_temp, current_output, rise_time, set_time, temp_out, Press_out, Humid_out, Ws_out, Visi_out, Cloud_out;
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public FirstFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SecondFragment.
*/
// TODO: Rename and change types and number of parameters
public static FirstFragment newInstance(String param1, String param2) {
FirstFragment fragment = new FirstFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
current_temp = rootView.findViewById(R.id.textView10);
current_output = rootView.findViewById(R.id.textView11);
rise_time = rootView.findViewById(R.id.textView25);
set_time = rootView.findViewById(R.id.textView26);
temp_out = rootView.findViewById(R.id.textView28);
Press_out = rootView.findViewById(R.id.textView29);
Humid_out = rootView.findViewById(R.id.textView30);
Ws_out = rootView.findViewById(R.id.textView33);
Visi_out = rootView.findViewById(R.id.textView34);
Cloud_out = rootView.findViewById(R.id.textView35);
return rootView;
}
public void getWeatherData(String name) {
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
try {
assert response.body() != null;
current_temp.setVisibility(View.VISIBLE);
current_temp.setText(response.body().getMain().getTemp() + " ℃");
current_output.setVisibility(View.VISIBLE);
current_output.setText(response.body().getWeather().get(0).getDescription());
rise_time.setVisibility(View.VISIBLE);
rise_time.setText(response.body().getSys().getSunrise() + " ");
set_time.setVisibility(View.VISIBLE);
set_time.setText(response.body().getSys().getSunset() + " ");
temp_out.setVisibility(View.VISIBLE);
temp_out.setText(response.body().getMain().getTemp() + " ℃");
Press_out.setVisibility(View.VISIBLE);
Press_out.setText(response.body().getMain().getPressure() + " hpa");
Humid_out.setVisibility(View.VISIBLE);
Humid_out.setText(response.body().getMain().getHumidity() + " %");
Ws_out.setVisibility(View.VISIBLE);
Ws_out.setText(response.body().getWind().getSpeed() + " Km/h");
Visi_out.setVisibility(View.VISIBLE);
Visi_out.setText(response.body().getVisibility() + " m");
Cloud_out.setVisibility(View.VISIBLE);
Cloud_out.setText(response.body().getClouds().getAll() + " %");
} catch (Exception e) {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
rise_time.setVisibility(View.GONE);
set_time.setVisibility(View.GONE);
temp_out.setVisibility(View.GONE);
Press_out.setVisibility(View.GONE);
Humid_out.setVisibility(View.GONE);
Ws_out.setVisibility(View.GONE);
Visi_out.setVisibility(View.GONE);
Cloud_out.setVisibility(View.GONE);
Toast.makeText(getActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
}
我没有粘贴所有内容,因为我正在关注 https://whosebug.com/help/minimal-reproducible-example
我认为,这是因为您的 FirstFragment 每次都会重新实例化。要保存状态,请修改 FirstFragment 的 onCreate()
,如下所示:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mParam1 = savedInstanceState.getString(ARG_PARAM1);
mParam2 = savedInstanceState.getString(ARG_PARAM2);
} else if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
并将 onSaveInstanceState()
添加到您的 FirstFragment:
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ARG_PARAM1, mParam1);
outState.putString(ARG_PARAM2, mParam2);
}
错误似乎是每次您 select 底部导航栏中的项目时,片段都会重新实例化,从而创建一个新片段(相同类型,但该片段的新对象) .
我建议你像这样在开头创建一个所有片段的对象
private final Fragment[] fragments = {
new HomeFragment(),
new MyNotesFragment(),
new PreferencesFragment()
};
然后对于 Bottom Nav Listener 部分,我们将根据单击的按钮更改片段,方法是传递已经创建的片段实例而不是创建新片段。
bottom_navigation_view.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
// Checking which bottom_nav items is clicked by comparing with string names..
if (item.getTitle().toString().equals("Home")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
// we passed the already made instance of the home fragment rather than creating a new one
ft.replace(R.id.my_fragment, fragments[0], "HomeFrag");
ft.commit();
} else if (item.getTitle().toString().equals("My Notes")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
ft.replace(R.id.my_fragment, fragments[1], "MyNotesFrag");
ft.commit();
} else if (item.getTitle().toString().equals("Preferences")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
ft.replace(R.id.my_fragment, fragments[2], "PrefFrag");
ft.commit();
}
return true;
}
});
(虽然这是针对 ViewPager 库,但概念相同)
你的问题是你的 FirstFragment
没有正确保存它的状态。根据 Saving state with fragments guide:
To ensure the user's state is saved, the Android framework automatically saves and restores the fragments and the back stack. Therefore, you need to ensure that any data in your fragment is saved and restored as well.
但是您没有保存传递给 getWeatherData
的最后一个 name
,也没有保存从 API 调用中获得的 Example
对象以便在重新创建视图时重新填充您的视图。
因此需要实际使用该指南中描述的 APIs 来保存您的状态。即,您应该提交 Guide to app architecture, which explains how you can separate your from your data loading by using ViewModels to store data across configuration changes (like rotating your device) and LiveData 以在加载数据时自动填充 UI。
我们要做的第一件事是将数据加载移动到 ViewModel
。此对象在配置更改后仍然存在,这意味着当您旋转设备时,存储在此 class 中的任何数据都会自动保存。这就是我们如何可以节省您的 Example
class 并避免一遍又一遍地调用服务器。
通过使用 Saved State module for ViewModel 中的 API(具体来说,SavedStateHandle
class),您保存在其中的任何数据都将在您的进程被终止和稍后重新创建(例如,如果您的设备内存不足等)。这就是我们如何保存最后一个 name
以便我们自动重新查询您的数据。
在这里,我们的 ViewModel 处理来自服务器的所有加载,并使用 LiveData 让我们的 UI 在加载数据时自动更新。
public class WeatherDataViewModel extends ViewModel {
// This will save the city name
private SavedStateHandle state;
// This is where we'll store our result from the server
private MutableLiveData<Example> mutableWeatherData = new MutableLiveData<>();
public WeatherDataViewModel(SavedStateHandle savedStateHandle) {
state = savedStateHandle;
String savedCityName = state.get("name");
if (savedCityName != null) {
// We already had a previously saved name, so we'll
// start loading right away
loadData();
}
}
// This is what our Fragment will use to get the latest weather data
public LiveData<Example> getWeatherDataLiveData() {
return mutableWeatherData;
}
// When you get a new city name, we'll save that in our
// state, then load the new data from the server
public void setCityName(String name) {
state.set("name", name);
loadData();
}
private void loadData() {
// Get the last name that was set
String name = state.get("name");
// Now kick off a load from the server
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
// Save the response we've gotten
// This will automatically update our UI
mutableWeatherData.setValue(response.body());
}
@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
}
现在您可以重写 FirstFragment
以使用 WeatherDataViewModel
作为 UI:
的真实来源
public class FirstFragment extends Fragment {
private WeatherDataViewModel viewModel;
public FirstFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
final TextView current_temp = rootView.findViewById(R.id.textView10);
final TextView current_output = rootView.findViewById(R.id.textView11);
final TextView rise_time = rootView.findViewById(R.id.textView25);
final TextView set_time = rootView.findViewById(R.id.textView26);
final TextView temp_out = rootView.findViewById(R.id.textView28);
final TextView Press_out = rootView.findViewById(R.id.textView29);
final TextView Humid_out = rootView.findViewById(R.id.textView30);
final TextView Ws_out = rootView.findViewById(R.id.textView33);
final TextView Visi_out = rootView.findViewById(R.id.textView34);
final TextView Cloud_out = rootView.findViewById(R.id.textView35);
// Get our ViewModel instance
viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);
// And whenever the data changes, refresh the UI
viewModel.getWeatherDataLiveData().observe(getViewLifecycleOwner(), data -> {
if (data != null) {
current_temp.setVisibility(View.VISIBLE);
current_temp.setText(data.getMain().getTemp() + " ℃");
current_output.setVisibility(View.VISIBLE);
current_output.setText(data.getWeather().get(0).getDescription());
rise_time.setVisibility(View.VISIBLE);
rise_time.setText(data.getSys().getSunrise() + " ");
set_time.setVisibility(View.VISIBLE);
set_time.setText(data.getSys().getSunset() + " ");
temp_out.setVisibility(View.VISIBLE);
temp_out.setText(data.getMain().getTemp() + " ℃");
Press_out.setVisibility(View.VISIBLE);
Press_out.setText(data.getMain().getPressure() + " hpa");
Humid_out.setVisibility(View.VISIBLE);
Humid_out.setText(data.getMain().getHumidity() + " %");
Ws_out.setVisibility(View.VISIBLE);
Ws_out.setText(data.getWind().getSpeed() + " Km/h");
Visi_out.setVisibility(View.VISIBLE);
Visi_out.setText(data.getVisibility() + " m");
Cloud_out.setVisibility(View.VISIBLE);
Cloud_out.setText(data.getClouds().getAll() + " %");
} else {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
rise_time.setVisibility(View.GONE);
set_time.setVisibility(View.GONE);
temp_out.setVisibility(View.GONE);
Press_out.setVisibility(View.GONE);
Humid_out.setVisibility(View.GONE);
Ws_out.setVisibility(View.GONE);
Visi_out.setVisibility(View.GONE);
Cloud_out.setVisibility(View.GONE);
Toast.makeText(requireActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
});
return rootView;
}
public void getWeatherData(String name) {
// The ViewModel controls loading the data, so we just
// tell it what the new name is - this kicks off loading
// the data, which will automatically call through to
// our observe() call when the data load completes
viewModel.setCityName(name);
}
}
通过这些更改,您会发现您的 Fragment 现在可以正确处理:
- 正在放入 Fragment 返回堆栈
- 配置更改(即旋转您的设备)
- 处理死亡和娱乐(即测试“不要保持活动”)
您会注意到我们如何使用 new ViewModelProvider(this).get(WeatherDataViewModel.class)
- 创建一个 WeatherDataViewModel
绑定到 this
- 您的 Fragment 本身。如果您的 ViewModel 加载的数据仅在那个 Fragment 中使用,这是最好的。
如果您还想在 Activity 中使用相同的数据,您的 activity 可以使用 new ViewModelProvider(this).get(WeatherDataViewModel.class)
创建一个 WeatherDataViewModel
范围为整个 Activity。然后任何片段都可以使用 new ViewModelProvider(requireActivity()).get(WeatherDataViewModel.class)
来获取 Activity 拥有的 ViewModel。这可能意味着您根本不需要 Fragment 上的 getWeatherData()
方法 - 相反,您的 Activity 会直接调用 viewModel.setCityName(name)
本身,所有 Fragment 都会立即更新(因为它们从同一个 ViewModel 读取)。
如果您还没有解决它,您可能想试试这个使用 SharedPreferences.
的解决方案
public class FirstFragment extends Fragment
{
// ...
private SharedPreferences prefs;
private SharedPreferences.Editor prefsEditor;
// ...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
current_temp = rootView.findViewById(R.id.textView10);
current_output = rootView.findViewById(R.id.textView11);
// ...
prefs = new SharedPreferences(getActivity().getApplicationContext());
prefsEditor = prefs.edit();
current_temp.setText(prefs.getString("current_temp", "not yet initialised"));
current_output.setText(prefs.getString("current_output", "not yet initialised"));
// ...
return rootView;
}
public void getWeatherData(String name)
{
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
try {
assert response.body() != null;
final String currentTemp = response.body().getMain().getTemp() + " ℃";
editor.putString("current_temp", currentTemp);
editor.commit();
current_temp.setText(currentTemp);
current_temp.setVisibility(View.VISIBLE);
final String currentOutput = response.body().getWeather().get(0).getDescription();
editor.putString("current_output", currentOutput);
editor.commit();
current_output.setText(currentOutput);
current_output.setVisibility(View.VISIBLE);
// ...
} catch (Exception e) {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
// ...
Toast.makeText(getActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
}
// ...
});
}
}
我的天气应用程序有一个底部导航视图,包含 3 个面板(今天、每小时和每天)。我的 activity 为 3 个面板托管了 3 个片段。目前,如果我在 today 片段上搜索任何城市,它会提供这些城市的数据。现在的问题是,如果我点击任何底部导航视图(向下),它会重置今天片段上显示的数据。
这是问题的说明:
搜索城市后显示的数据(打红勾的部分):
单击这些底部导航视图(带有红色勾号的部分)后,数据将重置:
无论单击那些导航视图,我都希望数据保持完整。
我尝试使用
java.lang.IllegalArgumentException: No view found for id 0x7f0a0116 (com.viz.lightweatherforecast:id/my_nav) for fragment ThirdFragment{90bc0de} (8e129d17-010d-41dc-9311-82e273b4e522 id=0x7f0a0116 tag=3)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:513)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3134)
at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:3068)
at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:251)
at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:501)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1391)
at android.app.Activity.performStart(Activity.java:7157)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3037)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1861)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6819)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:497)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:912)
I/weatherforecas: Compiler allocated 4MB to compile void android.widget.TextView.<init>(android.content.Context, android.util.AttributeSet, int, int)
我觉得很难实现他的代码,关于那个问题的其余建议都是用 kotlin 写的。
请问我该如何解决这个问题?
这是我的代码:
my_nav.xml(导航布局):
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/my_nav"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.viz.lightweatherforecast.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" />
<fragment
android:id="@+id/secondFragment"
android:name="com.viz.lightweatherforecast.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
<fragment
android:id="@+id/thirdFragment"
android:name="com.viz.lightweatherforecast.ThirdFragment"
android:label="fragment_third"
tools:layout="@layout/fragment_third" />
</navigation>
activity_home.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/layout"
android:background="@drawable/dubai"
tools:context=".Activity.HomeActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="409dp"
android:layout_height="599dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:navGraph="@navigation/my_nav"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
主页活动:
public class HomeActivity extends AppCompatActivity {
// Last update time, click sound, search button, search panel.
TextView time_field;
MediaPlayer player;
ImageView Search;
EditText textfield;
// For scheduling background image change(using constraint layout, start counting from dubai, down to statue of liberty.
ConstraintLayout constraintLayout;
public static int count=0;
int[] drawable =new int[]{R.drawable.dubai,R.drawable.central_bank_of_nigeria,R.drawable.eiffel_tower,R.drawable.hong_kong,R.drawable.statue_of_liberty};
Timer _t;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
// use home activity layout.
time_field = findViewById(R.id.textView9);
Search = findViewById(R.id.imageView4);
textfield = findViewById(R.id.textfield);
// find the id's of specific variables.
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
// host 3 fragments along with bottom navigation.
final NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment);
assert navHostFragment != null;
final NavController navController = navHostFragment.getNavController();
NavigationUI.setupWithNavController(bottomNavigationView, navController);
// For scheduling background image change
constraintLayout = findViewById(R.id.layout);
constraintLayout.setBackgroundResource(R.drawable.dubai);
_t = new Timer();
_t.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// run on ui thread
runOnUiThread(() -> {
if (count < drawable.length) {
constraintLayout.setBackgroundResource(drawable[count]);
count = (count + 1) % drawable.length;
}
});
}
}, 5000, 5000);
Search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// make click sound when search button is clicked.
player = MediaPlayer.create(HomeActivity.this, R.raw.click);
player.start();
getWeatherData(textfield.getText().toString().trim());
// make use of some fragment's data
FirstFragment firstFragment = (FirstFragment) navHostFragment.getChildFragmentManager().getFragments().get(0);
firstFragment.getWeatherData(textfield.getText().toString().trim());
}
});
}
});
}
}
编辑
片段class:
public class FirstFragment extends Fragment {
// User current time, current temperature, current condition, sunrise, sunset, temperature, pressure, humidity, wind_speed, visibility, clouds
TextView current_temp, current_output, rise_time, set_time, temp_out, Press_out, Humid_out, Ws_out, Visi_out, Cloud_out;
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public FirstFragment() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SecondFragment.
*/
// TODO: Rename and change types and number of parameters
public static FirstFragment newInstance(String param1, String param2) {
FirstFragment fragment = new FirstFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
current_temp = rootView.findViewById(R.id.textView10);
current_output = rootView.findViewById(R.id.textView11);
rise_time = rootView.findViewById(R.id.textView25);
set_time = rootView.findViewById(R.id.textView26);
temp_out = rootView.findViewById(R.id.textView28);
Press_out = rootView.findViewById(R.id.textView29);
Humid_out = rootView.findViewById(R.id.textView30);
Ws_out = rootView.findViewById(R.id.textView33);
Visi_out = rootView.findViewById(R.id.textView34);
Cloud_out = rootView.findViewById(R.id.textView35);
return rootView;
}
public void getWeatherData(String name) {
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
try {
assert response.body() != null;
current_temp.setVisibility(View.VISIBLE);
current_temp.setText(response.body().getMain().getTemp() + " ℃");
current_output.setVisibility(View.VISIBLE);
current_output.setText(response.body().getWeather().get(0).getDescription());
rise_time.setVisibility(View.VISIBLE);
rise_time.setText(response.body().getSys().getSunrise() + " ");
set_time.setVisibility(View.VISIBLE);
set_time.setText(response.body().getSys().getSunset() + " ");
temp_out.setVisibility(View.VISIBLE);
temp_out.setText(response.body().getMain().getTemp() + " ℃");
Press_out.setVisibility(View.VISIBLE);
Press_out.setText(response.body().getMain().getPressure() + " hpa");
Humid_out.setVisibility(View.VISIBLE);
Humid_out.setText(response.body().getMain().getHumidity() + " %");
Ws_out.setVisibility(View.VISIBLE);
Ws_out.setText(response.body().getWind().getSpeed() + " Km/h");
Visi_out.setVisibility(View.VISIBLE);
Visi_out.setText(response.body().getVisibility() + " m");
Cloud_out.setVisibility(View.VISIBLE);
Cloud_out.setText(response.body().getClouds().getAll() + " %");
} catch (Exception e) {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
rise_time.setVisibility(View.GONE);
set_time.setVisibility(View.GONE);
temp_out.setVisibility(View.GONE);
Press_out.setVisibility(View.GONE);
Humid_out.setVisibility(View.GONE);
Ws_out.setVisibility(View.GONE);
Visi_out.setVisibility(View.GONE);
Cloud_out.setVisibility(View.GONE);
Toast.makeText(getActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
}
我没有粘贴所有内容,因为我正在关注 https://whosebug.com/help/minimal-reproducible-example
我认为,这是因为您的 FirstFragment 每次都会重新实例化。要保存状态,请修改 FirstFragment 的 onCreate()
,如下所示:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mParam1 = savedInstanceState.getString(ARG_PARAM1);
mParam2 = savedInstanceState.getString(ARG_PARAM2);
} else if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
并将 onSaveInstanceState()
添加到您的 FirstFragment:
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ARG_PARAM1, mParam1);
outState.putString(ARG_PARAM2, mParam2);
}
错误似乎是每次您 select 底部导航栏中的项目时,片段都会重新实例化,从而创建一个新片段(相同类型,但该片段的新对象) .
我建议你像这样在开头创建一个所有片段的对象
private final Fragment[] fragments = {
new HomeFragment(),
new MyNotesFragment(),
new PreferencesFragment()
};
然后对于 Bottom Nav Listener 部分,我们将根据单击的按钮更改片段,方法是传递已经创建的片段实例而不是创建新片段。
bottom_navigation_view.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
// Checking which bottom_nav items is clicked by comparing with string names..
if (item.getTitle().toString().equals("Home")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
// we passed the already made instance of the home fragment rather than creating a new one
ft.replace(R.id.my_fragment, fragments[0], "HomeFrag");
ft.commit();
} else if (item.getTitle().toString().equals("My Notes")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
ft.replace(R.id.my_fragment, fragments[1], "MyNotesFrag");
ft.commit();
} else if (item.getTitle().toString().equals("Preferences")) {
// Code to change the fragments
final FragmentTransaction ft =
getFragmentManager().beginTransaction();
ft.replace(R.id.my_fragment, fragments[2], "PrefFrag");
ft.commit();
}
return true;
}
});
(虽然这是针对 ViewPager 库,但概念相同)
你的问题是你的 FirstFragment
没有正确保存它的状态。根据 Saving state with fragments guide:
To ensure the user's state is saved, the Android framework automatically saves and restores the fragments and the back stack. Therefore, you need to ensure that any data in your fragment is saved and restored as well.
但是您没有保存传递给 getWeatherData
的最后一个 name
,也没有保存从 API 调用中获得的 Example
对象以便在重新创建视图时重新填充您的视图。
因此需要实际使用该指南中描述的 APIs 来保存您的状态。即,您应该提交 Guide to app architecture, which explains how you can separate your from your data loading by using ViewModels to store data across configuration changes (like rotating your device) and LiveData 以在加载数据时自动填充 UI。
我们要做的第一件事是将数据加载移动到 ViewModel
。此对象在配置更改后仍然存在,这意味着当您旋转设备时,存储在此 class 中的任何数据都会自动保存。这就是我们如何可以节省您的 Example
class 并避免一遍又一遍地调用服务器。
通过使用 Saved State module for ViewModel 中的 API(具体来说,SavedStateHandle
class),您保存在其中的任何数据都将在您的进程被终止和稍后重新创建(例如,如果您的设备内存不足等)。这就是我们如何保存最后一个 name
以便我们自动重新查询您的数据。
在这里,我们的 ViewModel 处理来自服务器的所有加载,并使用 LiveData 让我们的 UI 在加载数据时自动更新。
public class WeatherDataViewModel extends ViewModel {
// This will save the city name
private SavedStateHandle state;
// This is where we'll store our result from the server
private MutableLiveData<Example> mutableWeatherData = new MutableLiveData<>();
public WeatherDataViewModel(SavedStateHandle savedStateHandle) {
state = savedStateHandle;
String savedCityName = state.get("name");
if (savedCityName != null) {
// We already had a previously saved name, so we'll
// start loading right away
loadData();
}
}
// This is what our Fragment will use to get the latest weather data
public LiveData<Example> getWeatherDataLiveData() {
return mutableWeatherData;
}
// When you get a new city name, we'll save that in our
// state, then load the new data from the server
public void setCityName(String name) {
state.set("name", name);
loadData();
}
private void loadData() {
// Get the last name that was set
String name = state.get("name");
// Now kick off a load from the server
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
// Save the response we've gotten
// This will automatically update our UI
mutableWeatherData.setValue(response.body());
}
@Override
public void onFailure(@NotNull Call<Example> call, @NotNull Throwable t) {
t.printStackTrace();
}
});
}
}
现在您可以重写 FirstFragment
以使用 WeatherDataViewModel
作为 UI:
public class FirstFragment extends Fragment {
private WeatherDataViewModel viewModel;
public FirstFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
final TextView current_temp = rootView.findViewById(R.id.textView10);
final TextView current_output = rootView.findViewById(R.id.textView11);
final TextView rise_time = rootView.findViewById(R.id.textView25);
final TextView set_time = rootView.findViewById(R.id.textView26);
final TextView temp_out = rootView.findViewById(R.id.textView28);
final TextView Press_out = rootView.findViewById(R.id.textView29);
final TextView Humid_out = rootView.findViewById(R.id.textView30);
final TextView Ws_out = rootView.findViewById(R.id.textView33);
final TextView Visi_out = rootView.findViewById(R.id.textView34);
final TextView Cloud_out = rootView.findViewById(R.id.textView35);
// Get our ViewModel instance
viewModel = new ViewModelProvider(this).get(WeatherDataViewModel.class);
// And whenever the data changes, refresh the UI
viewModel.getWeatherDataLiveData().observe(getViewLifecycleOwner(), data -> {
if (data != null) {
current_temp.setVisibility(View.VISIBLE);
current_temp.setText(data.getMain().getTemp() + " ℃");
current_output.setVisibility(View.VISIBLE);
current_output.setText(data.getWeather().get(0).getDescription());
rise_time.setVisibility(View.VISIBLE);
rise_time.setText(data.getSys().getSunrise() + " ");
set_time.setVisibility(View.VISIBLE);
set_time.setText(data.getSys().getSunset() + " ");
temp_out.setVisibility(View.VISIBLE);
temp_out.setText(data.getMain().getTemp() + " ℃");
Press_out.setVisibility(View.VISIBLE);
Press_out.setText(data.getMain().getPressure() + " hpa");
Humid_out.setVisibility(View.VISIBLE);
Humid_out.setText(data.getMain().getHumidity() + " %");
Ws_out.setVisibility(View.VISIBLE);
Ws_out.setText(data.getWind().getSpeed() + " Km/h");
Visi_out.setVisibility(View.VISIBLE);
Visi_out.setText(data.getVisibility() + " m");
Cloud_out.setVisibility(View.VISIBLE);
Cloud_out.setText(data.getClouds().getAll() + " %");
} else {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
rise_time.setVisibility(View.GONE);
set_time.setVisibility(View.GONE);
temp_out.setVisibility(View.GONE);
Press_out.setVisibility(View.GONE);
Humid_out.setVisibility(View.GONE);
Ws_out.setVisibility(View.GONE);
Visi_out.setVisibility(View.GONE);
Cloud_out.setVisibility(View.GONE);
Toast.makeText(requireActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
});
return rootView;
}
public void getWeatherData(String name) {
// The ViewModel controls loading the data, so we just
// tell it what the new name is - this kicks off loading
// the data, which will automatically call through to
// our observe() call when the data load completes
viewModel.setCityName(name);
}
}
通过这些更改,您会发现您的 Fragment 现在可以正确处理:
- 正在放入 Fragment 返回堆栈
- 配置更改(即旋转您的设备)
- 处理死亡和娱乐(即测试“不要保持活动”)
您会注意到我们如何使用 new ViewModelProvider(this).get(WeatherDataViewModel.class)
- 创建一个 WeatherDataViewModel
绑定到 this
- 您的 Fragment 本身。如果您的 ViewModel 加载的数据仅在那个 Fragment 中使用,这是最好的。
如果您还想在 Activity 中使用相同的数据,您的 activity 可以使用 new ViewModelProvider(this).get(WeatherDataViewModel.class)
创建一个 WeatherDataViewModel
范围为整个 Activity。然后任何片段都可以使用 new ViewModelProvider(requireActivity()).get(WeatherDataViewModel.class)
来获取 Activity 拥有的 ViewModel。这可能意味着您根本不需要 Fragment 上的 getWeatherData()
方法 - 相反,您的 Activity 会直接调用 viewModel.setCityName(name)
本身,所有 Fragment 都会立即更新(因为它们从同一个 ViewModel 读取)。
如果您还没有解决它,您可能想试试这个使用 SharedPreferences.
的解决方案public class FirstFragment extends Fragment
{
// ...
private SharedPreferences prefs;
private SharedPreferences.Editor prefsEditor;
// ...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// Inflate the layout for this fragment
View rootView = inflater.inflate(R.layout.fragment_first, container, false);
// For displaying weather data
current_temp = rootView.findViewById(R.id.textView10);
current_output = rootView.findViewById(R.id.textView11);
// ...
prefs = new SharedPreferences(getActivity().getApplicationContext());
prefsEditor = prefs.edit();
current_temp.setText(prefs.getString("current_temp", "not yet initialised"));
current_output.setText(prefs.getString("current_output", "not yet initialised"));
// ...
return rootView;
}
public void getWeatherData(String name)
{
ApiInterface apiInterface = ApiClient.getClient().create(ApiInterface.class);
Call<Example> call = apiInterface.getWeatherData(name);
call.enqueue(new Callback<Example>() {
@Override
public void onResponse(@NonNull Call<Example> call, @NonNull Response<Example> response) {
try {
assert response.body() != null;
final String currentTemp = response.body().getMain().getTemp() + " ℃";
editor.putString("current_temp", currentTemp);
editor.commit();
current_temp.setText(currentTemp);
current_temp.setVisibility(View.VISIBLE);
final String currentOutput = response.body().getWeather().get(0).getDescription();
editor.putString("current_output", currentOutput);
editor.commit();
current_output.setText(currentOutput);
current_output.setVisibility(View.VISIBLE);
// ...
} catch (Exception e) {
Log.e("TAG", "No City found");
current_temp.setVisibility(View.GONE);
current_output.setVisibility(View.GONE);
// ...
Toast.makeText(getActivity(), "No City found", Toast.LENGTH_SHORT).show();
}
}
// ...
});
}
}