Skip to content

列表、详情页开发

在完成了 API 对接后,我们需要开发新闻列表和详情页,为用户提供良好的阅读体验。在这一节中,我们将学习如何使用 React Native 开发新闻列表和详情页。

1. 创建新闻列表组件

首先,让我们创建一个新闻列表组件,用于展示新闻列表:

js
// src/components/NewsList.js
import React, { useEffect, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, Image } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTopHeadlines, setCategory } from '../redux/slices/newsSlice';
import { useNavigation } from '@react-navigation/native';

const categories = [
  { id: 'general', name: '综合' },
  { id: 'business', name: '商业' },
  { id: 'entertainment', name: '娱乐' },
  { id: 'health', name: '健康' },
  { id: 'science', name: '科技' },
  { id: 'sports', name: '体育' },
  { id: 'technology', name: '技术' },
];

const NewsList = () => {
  const dispatch = useDispatch();
  const navigation = useNavigation();
  const { headlines, loading, error, selectedCategory, selectedCountry } = useSelector(state => state.news);

  useEffect(() => {
    dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }));
  }, [dispatch, selectedCountry, selectedCategory]);

  const handleCategoryChange = (categoryId) => {
    dispatch(setCategory(categoryId));
  };

  const handleNewsPress = (news) => {
    navigation.navigate('NewsDetail', { news });
  };

  const renderNewsItem = useCallback(({ item }) => (
    <TouchableOpacity 
      style={styles.newsItem}
      onPress={() => handleNewsPress(item)}
      activeOpacity={0.7}
    >
      {item.urlToImage && (
        <View style={styles.imageContainer}>
          <Image 
            source={{ uri: item.urlToImage }} 
            style={styles.image}
            resizeMode="cover"
          />
        </View>
      )}
      <View style={styles.newsContent}>
        <Text style={styles.title} numberOfLines={2}>{item.title}</Text>
        <Text style={styles.description} numberOfLines={2}>
          {item.description}
        </Text>
        <View style={styles.newsFooter}>
          <Text style={styles.source}>{item.sourceName}</Text>
          <Text style={styles.date}>{item.formattedDate}</Text>
        </View>
      </View>
    </TouchableOpacity>
  ), [handleNewsPress]);

  const renderCategory = ({ item }) => (
    <TouchableOpacity
      style={[
        styles.categoryButton,
        selectedCategory === item.id && styles.selectedCategoryButton
      ]}
      onPress={() => handleCategoryChange(item.id)}
    >
      <Text 
        style={[
          styles.categoryText,
          selectedCategory === item.id && styles.selectedCategoryText
        ]}
      >
        {item.name}
      </Text>
    </TouchableOpacity>
  );

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>加载失败: {error}</Text>
        <TouchableOpacity 
          style={styles.retryButton}
          onPress={() => dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }))}
        >
          <Text style={styles.retryText}>重试</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <FlatList
        data={categories}
        renderItem={renderCategory}
        keyExtractor={item => item.id}
        horizontal
        showsHorizontalScrollIndicator={false}
        style={styles.categoryList}
        contentContainerStyle={styles.categoryListContent}
      />

      <FlatList
        data={headlines}
        renderItem={renderNewsItem}
        keyExtractor={(item, index) => index.toString()}
        contentContainerStyle={styles.newsListContent}
        showsVerticalScrollIndicator={false}
        ListEmptyComponent={
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyText}>暂无新闻</Text>
          </View>
        }
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  errorText: {
    fontSize: 16,
    color: 'red',
    marginBottom: 16,
  },
  retryButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    backgroundColor: '#007AFF',
    borderRadius: 4,
  },
  retryText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '500',
  },
  categoryList: {
    backgroundColor: 'white',
    maxHeight: 50,
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  categoryListContent: {
    paddingHorizontal: 12,
    paddingVertical: 12,
  },
  categoryButton: {
    paddingVertical: 6,
    paddingHorizontal: 16,
    marginRight: 8,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
  },
  selectedCategoryButton: {
    backgroundColor: '#007AFF',
  },
  categoryText: {
    fontSize: 14,
    color: '#333',
  },
  selectedCategoryText: {
    color: 'white',
    fontWeight: '500',
  },
  newsListContent: {
    padding: 12,
  },
  newsItem: {
    backgroundColor: 'white',
    borderRadius: 8,
    overflow: 'hidden',
    marginBottom: 12,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 1,
    },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  imageContainer: {
    width: '100%',
    height: 180,
  },
  image: {
    width: '100%',
    height: '100%',
  },
  newsContent: {
    padding: 12,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
    color: '#333',
  },
  description: {
    fontSize: 14,
    color: '#666',
    marginBottom: 12,
  },
  newsFooter: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  source: {
    fontSize: 12,
    color: '#999',
  },
  date: {
    fontSize: 12,
    color: '#999',
  },
  emptyContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 40,
  },
  emptyText: {
    fontSize: 16,
    color: '#999',
  },
});

export default NewsList;

2. 创建新闻详情页

接下来,让我们创建新闻详情页,用于展示新闻的详细内容:

js
// src/screens/NewsDetailScreen.js
import React from 'react';
import { View, Text, StyleSheet, ScrollView, Image, TouchableOpacity, Share, Linking } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';

const NewsDetailScreen = () => {
  const route = useRoute();
  const navigation = useNavigation();
  const { news } = route.params;

  const handleShare = async () => {
    try {
      await Share.share({
        message: `${news.title}\n\n${news.description}\n\n阅读全文: ${news.url}`,
        url: news.url,
      });
    } catch (error) {
      console.error('分享失败:', error);
    }
  };

  const handleOpenUrl = async () => {
    try {
      const supported = await Linking.canOpenURL(news.url);
      if (supported) {
        await Linking.openURL(news.url);
      } else {
        console.error('无法打开链接:', news.url);
      }
    } catch (error) {
      console.error('打开链接失败:', error);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <TouchableOpacity 
          style={styles.backButton}
          onPress={() => navigation.goBack()}
        >
          <Ionicons name="arrow-back" size={24} color="#333" />
        </TouchableOpacity>
        <TouchableOpacity 
          style={styles.shareButton}
          onPress={handleShare}
        >
          <Ionicons name="share-outline" size={24} color="#333" />
        </TouchableOpacity>
      </View>

      <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
        {news.urlToImage && (
          <Image 
            source={{ uri: news.urlToImage }} 
            style={styles.image}
            resizeMode="cover"
          />
        )}

        <View style={styles.newsContent}>
          <Text style={styles.title}>{news.title}</Text>
          
          <View style={styles.metaInfo}>
            <Text style={styles.source}>{news.sourceName}</Text>
            <Text style={styles.date}>{news.formattedDate}</Text>
          </View>

          {news.author && (
            <Text style={styles.author}>作者: {news.author}</Text>
          )}

          <Text style={styles.description}>{news.description}</Text>

          <Text style={styles.contentText}>{news.content || '阅读全文请点击下方链接'}</Text>

          <TouchableOpacity 
            style={styles.readMoreButton}
            onPress={handleOpenUrl}
          >
            <Text style={styles.readMoreText}>阅读全文</Text>
            <Ionicons name="arrow-forward" size={16} color="#007AFF" />
          </TouchableOpacity>
        </View>
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  backButton: {
    padding: 8,
  },
  shareButton: {
    padding: 8,
  },
  content: {
    flex: 1,
  },
  image: {
    width: '100%',
    height: 250,
  },
  newsContent: {
    padding: 16,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    marginBottom: 12,
    color: '#333',
  },
  metaInfo: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 12,
  },
  source: {
    fontSize: 14,
    color: '#007AFF',
    fontWeight: '500',
  },
  date: {
    fontSize: 14,
    color: '#999',
  },
  author: {
    fontSize: 14,
    color: '#666',
    marginBottom: 16,
  },
  description: {
    fontSize: 16,
    lineHeight: 24,
    color: '#333',
    marginBottom: 16,
  },
  contentText: {
    fontSize: 16,
    lineHeight: 24,
    color: '#333',
    marginBottom: 24,
  },
  readMoreButton: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 12,
    backgroundColor: '#F5F5F5',
    borderRadius: 8,
  },
  readMoreText: {
    fontSize: 16,
    color: '#007AFF',
    marginRight: 8,
  },
});

export default NewsDetailScreen;

3. 创建轮播图组件

为了展示热门新闻,我们可以创建一个轮播图组件:

js
// src/components/NewsCarousel.js
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, Image, TouchableOpacity, Dimensions } from 'react-native';
import Carousel from 'react-native-snap-carousel';
import { useNavigation } from '@react-navigation/native';

const { width: screenWidth } = Dimensions.get('window');

const NewsCarousel = ({ news }) => {
  const navigation = useNavigation();
  const [activeIndex, setActiveIndex] = useState(0);
  const carouselRef = useRef(null);

  const handleNewsPress = (item) => {
    navigation.navigate('NewsDetail', { news: item });
  };

  const renderItem = ({ item, index }) => (
    <TouchableOpacity 
      style={styles.slide}
      onPress={() => handleNewsPress(item)}
      activeOpacity={0.9}
    >
      <Image 
        source={{ uri: item.urlToImage }} 
        style={styles.image}
        resizeMode="cover"
      />
      <View style={styles.overlay}>
        <Text style={styles.title} numberOfLines={2}>{item.title}</Text>
        <Text style={styles.source}>{item.sourceName}</Text>
      </View>
    </TouchableOpacity>
  );

  if (!news || news.length === 0) {
    return null;
  }

  return (
    <View style={styles.container}>
      <Carousel
        ref={carouselRef}
        data={news.slice(0, 5)} // 只展示前5条新闻
        renderItem={renderItem}
        sliderWidth={screenWidth}
        itemWidth={screenWidth - 32}
        layout="default"
        loop
        autoplay
        autoplayInterval={5000}
        onSnapToItem={(index) => setActiveIndex(index)}
      />
      <View style={styles.pagination}>
        {news.slice(0, 5).map((_, index) => (
          <View 
            key={index} 
            style={[
              styles.paginationDot,
              index === activeIndex && styles.paginationDotActive
            ]}
          />
        ))}
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    height: 250,
    marginVertical: 12,
  },
  slide: {
    flex: 1,
    borderRadius: 12,
    overflow: 'hidden',
  },
  image: {
    width: '100%',
    height: '100%',
  },
  overlay: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    padding: 16,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    color: 'white',
    marginBottom: 8,
  },
  source: {
    fontSize: 14,
    color: 'rgba(255, 255, 255, 0.8)',
  },
  pagination: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginTop: 12,
  },
  paginationDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: '#E0E0E0',
    marginHorizontal: 4,
  },
  paginationDotActive: {
    backgroundColor: '#007AFF',
  },
});

export default NewsCarousel;

4. 更新主屏幕

现在,让我们更新主屏幕,整合新闻列表和轮播图:

js
// src/screens/HomeScreen.js
import React, { useEffect } from 'react';
import { View, StyleSheet, ScrollView, RefreshControl } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTopHeadlines } from '../redux/slices/newsSlice';
import NewsCarousel from '../components/NewsCarousel';
import NewsList from '../components/NewsList';

const HomeScreen = () => {
  const dispatch = useDispatch();
  const { headlines, loading, selectedCountry, selectedCategory } = useSelector(state => state.news);
  const [refreshing, setRefreshing] = React.useState(false);

  useEffect(() => {
    dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }));
  }, [dispatch, selectedCountry, selectedCategory]);

  const onRefresh = React.useCallback(() => {
    setRefreshing(true);
    dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }))
      .finally(() => setRefreshing(false));
  }, [dispatch, selectedCountry, selectedCategory]);

  return (
    <View style={styles.container}>
      <ScrollView 
        style={styles.scrollView}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
            colors={['#007AFF']}
            tintColor="#007AFF"
          />
        }
      >
        <NewsCarousel news={headlines} />
        <NewsList />
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  scrollView: {
    flex: 1,
  },
});

export default HomeScreen;

5. 设置导航

现在,让我们设置导航,将主屏幕和详情页连接起来:

js
// src/navigation/AppNavigator.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import NewsDetailScreen from '../screens/NewsDetailScreen';

const Stack = createStackNavigator();

const AppNavigator = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator
        initialRouteName="Home"
        screenOptions={{
          headerShown: false,
        }}
      >
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="NewsDetail" component={NewsDetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default AppNavigator;

6. 更新 App.js

最后,让我们更新 App.js,整合所有组件:

js
// App.js
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { store } from './src/redux/store';
import AppNavigator from './src/navigation/AppNavigator';

export default function App() {
  return (
    <Provider store={store}>
      <SafeAreaProvider>
        <AppNavigator />
      </SafeAreaProvider>
    </Provider>
  );
}

7. 优化列表性能

为了提高列表的性能,我们可以进行以下优化:

  1. 使用 useCallback 缓存回调函数
  2. 使用 keyExtractor 提供唯一的键
  3. 使用 getItemLayout 优化列表布局
  4. 使用 removeClippedSubviews 减少渲染的视图
js
// src/components/NewsList.js (优化版)
import React, { useEffect, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, Image } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTopHeadlines, setCategory } from '../redux/slices/newsSlice';
import { useNavigation } from '@react-navigation/native';

const categories = [
  { id: 'general', name: '综合' },
  { id: 'business', name: '商业' },
  { id: 'entertainment', name: '娱乐' },
  { id: 'health', name: '健康' },
  { id: 'science', name: '科技' },
  { id: 'sports', name: '体育' },
  { id: 'technology', name: '技术' },
];

const NewsList = () => {
  const dispatch = useDispatch();
  const navigation = useNavigation();
  const { headlines, loading, error, selectedCategory, selectedCountry } = useSelector(state => state.news);

  useEffect(() => {
    dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }));
  }, [dispatch, selectedCountry, selectedCategory]);

  const handleCategoryChange = useCallback((categoryId) => {
    dispatch(setCategory(categoryId));
  }, [dispatch]);

  const handleNewsPress = useCallback((news) => {
    navigation.navigate('NewsDetail', { news });
  }, [navigation]);

  const renderNewsItem = useCallback(({ item, index }) => (
    <TouchableOpacity 
      style={styles.newsItem}
      onPress={() => handleNewsPress(item)}
      activeOpacity={0.7}
    >
      {item.urlToImage && (
        <View style={styles.imageContainer}>
          <Image 
            source={{ uri: item.urlToImage }} 
            style={styles.image}
            resizeMode="cover"
          />
        </View>
      )}
      <View style={styles.newsContent}>
        <Text style={styles.title} numberOfLines={2}>{item.title}</Text>
        <Text style={styles.description} numberOfLines={2}>
          {item.description}
        </Text>
        <View style={styles.newsFooter}>
          <Text style={styles.source}>{item.sourceName}</Text>
          <Text style={styles.date}>{item.formattedDate}</Text>
        </View>
      </View>
    </TouchableOpacity>
  ), [handleNewsPress]);

  const renderCategory = useCallback(({ item }) => (
    <TouchableOpacity
      style={[
        styles.categoryButton,
        selectedCategory === item.id && styles.selectedCategoryButton
      ]}
      onPress={() => handleCategoryChange(item.id)}
    >
      <Text 
        style={[
          styles.categoryText,
          selectedCategory === item.id && styles.selectedCategoryText
        ]}
      >
        {item.name}
      </Text>
    </TouchableOpacity>
  ), [selectedCategory, handleCategoryChange]);

  const keyExtractor = useCallback((item, index) => index.toString(), []);

  const getItemLayout = useCallback((data, index) => ({
    length: 300, // 估计每个 item 的高度
    offset: 300 * index,
    index,
  }), []);

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorText}>加载失败: {error}</Text>
        <TouchableOpacity 
          style={styles.retryButton}
          onPress={() => dispatch(fetchTopHeadlines({ country: selectedCountry, category: selectedCategory }))}
        >
          <Text style={styles.retryText}>重试</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <FlatList
        data={categories}
        renderItem={renderCategory}
        keyExtractor={item => item.id}
        horizontal
        showsHorizontalScrollIndicator={false}
        style={styles.categoryList}
        contentContainerStyle={styles.categoryListContent}
      />

      <FlatList
        data={headlines}
        renderItem={renderNewsItem}
        keyExtractor={keyExtractor}
        contentContainerStyle={styles.newsListContent}
        showsVerticalScrollIndicator={false}
        removeClippedSubviews={true}
        getItemLayout={getItemLayout}
        maxToRenderPerBatch={10}
        windowSize={10}
        ListEmptyComponent={
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyText}>暂无新闻</Text>
          </View>
        }
      />
    </View>
  );
};

const styles = StyleSheet.create({
  // 与之前相同的样式
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  center: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  errorText: {
    fontSize: 16,
    color: 'red',
    marginBottom: 16,
  },
  retryButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    backgroundColor: '#007AFF',
    borderRadius: 4,
  },
  retryText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '500',
  },
  categoryList: {
    backgroundColor: 'white',
    maxHeight: 50,
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  categoryListContent: {
    paddingHorizontal: 12,
    paddingVertical: 12,
  },
  categoryButton: {
    paddingVertical: 6,
    paddingHorizontal: 16,
    marginRight: 8,
    borderRadius: 16,
    backgroundColor: '#F0F0F0',
  },
  selectedCategoryButton: {
    backgroundColor: '#007AFF',
  },
  categoryText: {
    fontSize: 14,
    color: '#333',
  },
  selectedCategoryText: {
    color: 'white',
    fontWeight: '500',
  },
  newsListContent: {
    padding: 12,
  },
  newsItem: {
    backgroundColor: 'white',
    borderRadius: 8,
    overflow: 'hidden',
    marginBottom: 12,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 1,
    },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  imageContainer: {
    width: '100%',
    height: 180,
  },
  image: {
    width: '100%',
    height: '100%',
  },
  newsContent: {
    padding: 12,
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
    color: '#333',
  },
  description: {
    fontSize: 14,
    color: '#666',
    marginBottom: 12,
  },
  newsFooter: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  source: {
    fontSize: 12,
    color: '#999',
  },
  date: {
    fontSize: 12,
    color: '#999',
  },
  emptyContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    paddingVertical: 40,
  },
  emptyText: {
    fontSize: 16,
    color: '#999',
  },
});

export default NewsList;

8. 图片优化

为了优化图片加载性能,我们可以使用 react-native-fast-image 库:

bash
npm install react-native-fast-image

然后,我们可以更新图片组件:

js
// src/components/OptimizedImage.js
import React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import FastImage from 'react-native-fast-image';

const OptimizedImage = ({ source, style, resizeMode = 'cover' }) => {
  return (
    <View style={[styles.container, style]}>
      <FastImage
        source={source}
        style={styles.image}
        resizeMode={resizeMode}
        placeholder={require('../assets/placeholder.png')}
        fallback={require('../assets/fallback.png')}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    overflow: 'hidden',
  },
  image: {
    width: '100%',
    height: '100%',
  },
});

export default OptimizedImage;

9. 总结

通过本章节的学习,我们掌握了如何使用 React Native 开发新闻列表和详情页,包括:

  1. 如何创建新闻列表组件
  2. 如何创建新闻详情页
  3. 如何创建轮播图组件
  4. 如何设置导航
  5. 如何优化列表性能
  6. 如何优化图片加载

这些技能将帮助我们构建更加流畅、美观的新闻/资讯APP。

10. 下一步

接下来,我们将学习如何实现收藏和搜索功能,进一步增强应用的功能。

© 2026 编程马·菜鸟教程 版权所有