嘘~ 正在从服务器偷取页面 . . .

UWP & Android 复习


UWP 桌面应用开发

0. 写在前面

因为马上就要考试了,所以写一个博客对知识点进行总结。

还有一点在于,自己写 Java 后端也有一定的时间了,已经对 Java 语言的局限性已经有所体会,所以想着尝试用其他的语言进行后端开发。

可能之后会尝试去了解 C#/Go 吧。

1. 文件结构

  • 我们首先会创建了一个解决方案,一个解决方案里面可能有很多项目(加粗的项目作为启动项目,只有启动项目启动之后剩余的项目才能进行加载)。默认的解决方案中,我们只有一个项目;

  • 在 Properties 中,保存我们相关项目的信息;

  • 引用中包含我们调用的各种库;

  • App.xaml/App.xaml.cs 表示应用程序的最先加载的程序;
  • MainPage.xaml UWP 默认加载的主页面。

每一个界面中都会有一个 InitializeComponent 方法,在这个方法的源代码中,会有对 xaml 中各种控件(control)的加载,这样就能在启动之后显示我们的所设置的布局。

InitializeComponent 源代码

2. 生命周期

应用程序的生命周期是指从应用程序部署到计算机上到应用程序从计算机中删除 的整个阶段。

在 app.xaml.cs 中定义的几个状态

显示应用执行状态之间转换的状态图

当启动或激活应用时,这些应用会进入在后台运行状态。 如果应用由于前台应用启动而需要移动到前台,则该应用将获取 LeavingBackground 事件。

由于预启动,应用的 OnLaunched () 方法可能由系统启动,而不是由用户启动。 由于应用在后台预启动,因此可能需要在 OnLaunched() 中采取不同操作。

当用户最小化某个应用时,Windows 会等待数秒,以查看用户是否会切换回该应用。 如果用户在此时间范围内未切换回,并且任何扩展执行、后台任务或活动赞助执行都未处于活动状态,则 Windows 将暂停该应用。 只要应用中不存在任何处于活动状态的扩展执行会话等,该应用也将在出现锁屏界面时暂停 。

2.1 app data/session data

app data 是和应用程序相关的内容,在应用程序的整个生命周期中都存在的。

当我们需要保存应用程序的某些数据以便下一次访问时依然可以 使用时,我们可以将数据保存成应用程序数据(即 app data),也可以将数据写到 文件中。

Windows.Storage.ApplicationDataContainer localSettings = 
    Windows.Storage.ApplicationData.Current.LocalSettings;
localSettings.Values["Name"] = TextBox1.Text;
//将我们的数据暂存到 localSetting 中,可以理解为一个缓存

3. 基本控件

3.1 选择控件

  • RadioButton 单选按钮,在多个选项中可以使用 GroupName 进行分组;

    <StackPanel>
        <RadioButton Content="好" GroupName="comment"/>
        <RadioButton Content="一般" GroupName="comment"/>
        <RadioButton Content="差" GroupName="comment"/>
    </StackPanel>
  • ComboBox 下拉框,可以在下拉框中添加各种奇怪的控件(bushi);

     <ComboBox Header="show something to me" FontSize="32"
                          PlaceholderText="take a ability"
                          PlaceholderForeground="Gray">
    	<x:String>Blue</x:String>
        <x:String>Green</x:String>
        <x:String>Red</x:String>
        <x:String>Yellow</x:String>
        <TextBlock Text="this is just a test"/>
         <!--这种控件添加到 ComboBox 也是被允许的-->
    </ComboBox>
  • CheckBox 复选框,提供多选功能。可以有三种状态,表示未选中、未全选、完全选中;

    <CheckBox Content="hello" FontSize="32" IsThreeState="True" IsChecked="{x:Null}"/>
    <!--对应代码为 false, x:null, true-->
  • Button 最常用的按钮控件(在这里提醒一点,基本所有控件的大小默认都是根据字体大小,即 FontSize 自动调整的,默认的属性为 Auto)。除了在按钮中使用默认的 Content 属性,还可以使用 Image、TextBlock 控件包裹在里面;

    <Button Content="Standard XAML button" Click="Button_Click" />
    
    <Button Content="Button" Click="Button_Click" AutomationProperties.Name="Pie">
        <Image Source="/Assets/Slices.png" AutomationProperties.Name="Slice"/>
    </Button>
    
    <StackPanel>
        <TextBlock Text="The following buttons' content may get 
                         clipped if we don't pay careful attention to their layout containers." 
                   Margin="0,0,0,8" TextWrapping="Wrap"/>
        <TextBlock Text="One option to mitigate clipped content 
                         is to place Buttons underneath each other, allowing for more space to grow horizontally:" 
                   Margin="0,0,0,8" TextWrapping="Wrap"/>
        <Button HorizontalAlignment="Stretch" Margin="0,0,0,5">
            This is some text that is too long and will get cut off
        </Button>
        <Button HorizontalAlignment="Stretch">
            This is another text that would result in being cut off
        </Button>
        <TextBlock Text="Another option is to explicitly wrap the Button's content" Margin="0,8,0,8"/>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <Button MaxWidth="240" Margin="0,0,8,0">
                <TextBlock Text="This is some text that is too long and will get cut off" 
                           TextWrapping="WrapWholeWords"/>
            </Button>
            <Button MaxWidth="240">
                <TextBlock Text="This is another text that would result in being cut off" 
                           TextWrapping="WrapWholeWords"/>
            </Button>
        </StackPanel>
    </StackPanel>
  • HyperlinkButton 表示超链接跳转;

    <HyperlinkButton Content="Microsoft home page" NavigateUri="http://www.microsoft.com"/>
  • RepeatButton 提供一个可以按住之后,连续触发的按钮;

    <RepeatButton Content="Click and hold" Click="RepeatButton_Click"/>
  • ToggleButon 看起来像一个Button,但工作起来像一个CheckBox。它通常有两种状态,但是跟 CheckBox 一样,也是存在三种状态的情况(虽然第三种状态并不是很能看得出来);

    <ToggleButton Content="ToggleButton" Click="Button_Click"/>
  • ToggleSwitch 这就表示一个开关,拥有两种状态,可以设置它开启或者关闭时显示的不同内容。

    <StackPanel Orientation="Horizontal">
        <ToggleSwitch OnContent="开启啦" OffContent="关闭咯" FontSize="32" 
                      x:Name="state" IsOn="True"/>
        <ProgressRing IsActive="{Binding ElementName=state, Path=IsOn, Mode=OneWay}" 
                      Width="40"/>
        <!--使用了一个最基本的绑定,之后会提-->
    </StackPanel>

    3.2 进度控件

  • Slider 进度条控件,是表示连续区间数值的控件;

    <Slider Width="200" Minimum="500" Maximum="1000" StepFrequency="10"
            SmallChange="10" LargeChange="100" Value="800" />
    <!--设置进度条最大值、最小值、每次跳转的步数、当前数值以及最大最小变化量-->
    
    <Slider AutomationProperties.Name="Slider with ticks" 
            TickFrequency="10" TickPlacement="Outside" />
    <!--可以设置平均刻度,以及刻度的位置-->
  • ProcessRing 加载进度条。

    <muxc:ProgressRing IsActive="True" />
    
    <ProgressRing Width="60" Height="60" Value="0" IsIndeterminate="False"/>
    <!--IsIndeterminate 表示进度条是否是不确定的,默认是 true--> 

    3.3 文本控件

  • TextBlock 只读文本显示控件,可以使用内联的元素进行各种精确的设定;

    <TextBlock Text="I am a TextBlock"/>
    
    <Page.Resources>
        <Style TargetType="TextBlock" x:Key="CustomTextBlockStyle">
            <Setter Property="FontFamily" Value="Comic Sans MS"/>
            <Setter Property="FontStyle" Value="Italic"/>
        </Style>
    <Page.Resources>
    <!--设定基本的 TextBlock 样式-->
    <TextBlock Text="I am a styled TextBlock" Style="{StaticResource CustomTextBlockStyle}"/>
        
    <TextBlock>
        <Run FontFamily="Times New Roman" Foreground="DarkGray">
            Text in a TextBlock doesn't have to be a simple string.</Run>
        <LineBreak/><!--进行换行-->
        <Span>Text can be <Bold>bold</Bold>,
    	<Italic>italic</Italic>, or <Underline>underlined</Underline>. </Span>
    </TextBlock>
  • TextBox TextBox 相当于可编辑的 TextBlock(当然,我们可以使用 IsReadOnly 让它不可编辑);

    <TextBox AutomationProperties.Name="simple TextBox"/>
    
    <TextBox Text="I am super excited to be here!"
        AutomationProperties.Name="customized TextBox" IsReadOnly="True"
        FontFamily="Arial" FontSize="24" FontStyle="Italic"
        CharacterSpacing="200" Foreground="CornflowerBlue" />
  • RichTextBlock 富文本控件,使用 Paragraph 进行分段。如果需要在其中加入控件,需要使用 InlineUIContainer。

    <RichTextBlock>
        <Paragraph>
            <InlineUIContainer>
                <Image Source="https://hexoblogimages-1304994718.cos.ap-nanjing.myqcloud.com/202112211555684.jpg"
                Stretch="Uniform" Width="500" Height="600"/>
            </InlineUIContainer>
    	</Paragraph>
        <Paragraph>
            <x:String>hello World</x:String>
            <InlineUIContainer>
            	<TextBlock Text="这只是一个测试,咕咕咕"/>
            </InlineUIContainer>
        </Paragraph>
    </RichTextBlock>

    3.4 媒体控件

  • image 显示图片的控件。image 控件的伸缩有四种情况:

    • None 按照正常图片大小进行填充;
    • Fill 忽视原图的横纵比,将图片缩放到规定的大小;
    • Uniform 保持图片的横纵比,将图片尽可能地缩放到规定地大小,如果超出则留空;
    • UniformToFill 保持图片地横纵比,将图片通过横纵比进行放大,如果过大,会有超出的部分。
    <InlineUIContainer>
    	<Image Source="https://hexoblogimages-1304994718.cos.ap-nanjing.myqcloud.com/202112211555684.jpg"
    		Stretch="Uniform" Width="500" Height="600"/>
    </InlineUIContainer>
  • MediaPlayerElement 媒体播放器,播放视频或者展示图片;

    <MediaPlayerElement Source="/Assets/SampleMedia/ladybug.wmv"
                        MaxWidth="400"
                        AutoPlay="False"
                        AreTransportControlsEnabled="True" />
    <!--AreTransportControlsEnabled 是自带的播放控件-->
  • WebView 显示对应的网络链接内容。

    <WebView Source="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/web-view" />

    3.5 物件集合

  • FlipView 可以将多个图片“打包”成一个整体,拥有自带的前后切换框;

    <FlipView MaxWidth="400" Height="270">
        <Image Source="ms-appx:///Assets/SampleMedia/cliff.jpg" AutomationProperties.Name="Cliff"/>
        <Image Source="ms-appx:///Assets/SampleMedia/grapes.jpg" AutomationProperties.Name="Grapes"/>
        <Image Source="ms-appx:///Assets/SampleMedia/rainier.jpg" AutomationProperties.Name="Rainier"/>
        <Image Source="ms-appx:///Assets/SampleMedia/sunset.jpg" AutomationProperties.Name="Sunset"/>
        <Image Source="ms-appx:///Assets/SampleMedia/valley.jpg" AutomationProperties.Name="Valley"/>
    </FlipView>
  • GridView 可以自动排列行和列,如果图片显示超出范围,会进行自动排版;

  • ListView/ListBox ListBox 算是 ListView 的简化版本,可以使用 SelectIndex 和 SelectItem 获取对应选取的元素。

    <ListBox ItemsSource="{x:Bind Fonts}" DisplayMemberPath="Item1" 
             SelectedValuePath="Item2" Height="164" Loaded="ListBox2_Loaded"/>

    4. 布局

  • Grid 网格布局,可以类比为 Excel 的行和列,在 Grid.RowDefinitions 和 Grid.ColumnDefinitons 中进行定义:

    • 默认是*,表示布局的占比;
    • 可以设置数据,表示正常的像素;
    • 设置 Auto,宽度/高度会根据控件的大小进行调整。

    Grid 可以使用 RowSpan/ColumnSpan 进行“单元格合并”。

    <Grid Width="240" Height="120" Background="Gray">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Rectangle Fill="Red" Grid.Column="0" Grid.Row="0" />
        <Rectangle Fill="Blue" Grid.Row="1" />
        <Rectangle Fill="Green" Grid.Column="1" />
        <Rectangle Fill="Yellow" Grid.Row="1" Grid.Column="1" />
    </Grid>
  • StackPanel 将各种物件整合成一行的布局,也是最基本的布局;

    <StackPanel Orientation="Vertical">
        <Rectangle Fill="Red"/>
        <Rectangle Fill="Blue"/>
        <Rectangle Fill="Green"/>
        <Rectangle Fill="Yellow"/>
    </StackPanel>
  • ViewBox 限制这个布局中的内容放大或是缩小;

    <Viewbox Height="300" Width="300" Stretch="Uniform" StretchDirection="UpOnly">
        <Border BorderBrush="Gray" BorderThickness="15">
            <StackPanel Background="DarkGray">
                <StackPanel Orientation="Horizontal">
                    <Rectangle Fill="Blue" Height="10" Width="40"/>
                    <Rectangle Fill="Green" Height="10" Width="40"/>
                    <Rectangle Fill="Red" Height="10" Width="40"/>
                    <Rectangle Fill="Yellow" Height="10" Width="40"/>
                </StackPanel>
                <Image Source="ms-appx:///Assets/Slices.png"/>
                <TextBlock Text="This is text." HorizontalAlignment="Center"/>
            </StackPanel>
        </Border>
    </Viewbox>
  • Canvas 绝对布局控件,可以使用 Canvas.left 等等进行绝对方位的布局。Canvas 还有一个 Z 轴,表示深度。

    <Canvas Width="120" Height="120" Background="Gray">
        <Rectangle Fill="Red" Canvas.Left="0" Canvas.Top="0" Canvas.ZIndex="0" />
        <Rectangle Fill="Blue" Canvas.Left="20" Canvas.Top="20" Canvas.ZIndex="1" />
        <Rectangle Fill="Green" Canvas.Left="40" Canvas.Top="40" Canvas.ZIndex="2" />
        <Rectangle Fill="Yellow" Canvas.Left="60" Canvas.Top="60" Canvas.ZIndex="3" />
    </Canvas>

    5. 事件触发

5.1 添加控件

  • XAML 在设计阶段中添加控件,即就是在 xaml 中写下控件代码;

  • C# 在运行中添加各种控件。

    Button button = new Button();
    button.Content = "mission complete";
    grid.Children.Add(button);

    源码如下,获得面板的子元素的集合

5.2 事件绑定

事件绑定的函数基本模板 ____(object sender, RoutedEventArgs e)

  • XAML 直接在控件中写,Click="button_name_Click"

  • C# 在初始页面加载时进行绑定 button_name.Click += button_name_Click;

    这里使用 += 的原因在于,对于这个控件,我们进行事件的绑定是一个委托类型。我们正常来说是应该叠加上去,而不是单纯的用 = 把原有的事件替代掉。

5.3 页面跳转

  • Frame.Navigate(typeof(Page1)) Page1 是要跳转的页面,根据要跳转的页面进行修改 ;

  • Frame.Navigate(typeof(Page1), obj); obj 是要传递的参数,可以变化的;

  • Frame.GoBack()/Frame.GoForward() 向后/向前;

  • Frame.CanGoBack/Frame.CanGoForward 是否能够向后/向前。

5.4 数据绑定

对一些控件值,我们除了硬编码或者通过事件的触发进行更改,我们还可能让其根据某个值进行更改

  • oneWay 在数据源更改之后,对绑定数据进行变更;
  • oneTime 只有在初始化的时候才会进行变更;
  • TwoWay 绑定值的更改同样会作用于数据源。
<Slider Width="800" HorizontalAlignment="Center" VerticalAlignment="Center"
        x:Name="slider_value" Maximum="400" Value="50"
        TickFrequency="10" TickPlacement="Inline">
    <Slider.HeaderTemplate>
        <DataTemplate>
            <StackPanel Orientation="Vertical">
                <TextBlock Text="Height" FontSize="40"/>
                <TextBlock Text="全体目光向我看齐,嘉然是我爹" FontSize="40"/>
            </StackPanel>
        </DataTemplate>
    </Slider.HeaderTemplate>
</Slider>
<TextBox Width="400" Grid.Row="1" HorizontalAlignment="Center"
         Height="Auto" FontSize="50"
         VerticalAlignment="Center" x:Name="textBox_value" 
         Text="{Binding ElementName=slider_value, Path=Value, Mode=TwoWay, 
               UpdateSourceTrigger=PropertyChanged}"/>
<!--因为默认是失去焦点之后才会进行更改,这里改成 PropertyChanged-->

5.5 数据模板/样式

data template/style:

  • Inline 内嵌在某一个控件内部;
  • Page resource 定义在页面的 xaml 代码中,紧跟在元素的里面 ;
  • App resource 定义在 app.xaml 代码中,紧跟在元素的里面 ;
  • Resource dictionary 定义在单独的文件中,在里面。

x:Name=”” 控件的命名 x:Key=”” 资源的名称。

在 Style 中,可以使用 BaseOn 继承之前写的数据样式,可以直接在内部进行重写。

style 样例

<Page.Resources>
    <DataTemplate x:Key="ViewNotesDataTemplate">
        <Grid>
            <TextBlock x:Name="txtNoteTitle" Text="{Binding Title}" FontSize="24" ></TextBlock>
        </Grid>
    </DataTemplate>
</Page.Resources>

<ListBox x:Name="lstBoxNotes"  ScrollViewer.HorizontalScrollMode="Auto" 
         ScrollViewer.VerticalScrollMode="Auto" 
         ScrollViewer.HorizontalScrollBarVisibility="Auto" 
         ScrollViewer.VerticalScrollBarVisibility="Auto" 
         Margin="279,97,278,97" Width="809"
         ItemTemplate="{StaticResource ViewNotesDataTemplate}"/>
<!--DataTemplate 使用样例。使用 ScrollViewer 显示下拉框-->

5.6 弹出窗体

注意,MessageDialog、PopupMenu 是在 Windows.UI.Popups 这个包中。

  • ToolTip 表示一个弹出的消息提示框;

    ToolTip tool = new ToolTip();
    tool.Content = "click to start";
    ToolTipService.SetToolTip(tool, button_name);
  • PopupMenu 本质是一个菜单栏,可以类比于 Windows 中的右键菜单。

    添加弹出菜单步骤

    Windows.Ui.Popups.PopupMenu pMenu = new Windows.Ui.Popups.PopupMenu();
    UICommand redCommand = new UICommand();
    redCommand.Label = "Red";
    redCommand.Id = 1;
    pMenu.Command.Add(redCommand);
    var chosenCommand = await pMenu.ShowForSelectionAsync(
    new Rect(e.GetPosition(null)), e.GetPosition(null));
    //因为这里的操作是耗时的,所以我们需要使用异步(async)
    //await 就像服务员点餐给你菜单一样,程序在这里会进行等待;等待结束就像是告诉服务员餐点好了,程序就继续进行了

    各种显示方法

6. 文件存储

每一个通用应用程序默认只有权限访问三个路径(app 安装目录、app data 目录、下载目录),其他的路径如果要访 问的话有两种办法:

(1)在 manifest 文件中声明能力,即增加权限;

(2)使用 FilePicker。

其中第一种办法适用于常见的知名文件夹,如文档库、视频库、音乐库等,第二种方法适用于所有的文件夹。

6.1 使用 FilePicker

通过使用 FilePicker 来增加文件读取写入的范围;

FileOpenPicker openPicker = new FileOpenPicker()
{
    CommitButtonText = "Open this!",
    ViewMode = PickerViewMode.List,
    SuggestedStartLocation = PickerLocationId.Downloads
};
openPicker.FileTypeFilter.Add(".txt");
StorageFile file = await openPicker.PickSingleFileAsync();
if (file != null)
{
    try
    {
        await new MessageDialog(await FileIO.ReadTextAsync(file)).ShowAsync();
    }
    catch (Exception ex)
    {
        await new MessageDialog(ex.ToString()).ShowAsync();
    }
}

FileSavePicker savePicker = new FileSavePicker()
{
    CommitButtonText = "Save Here!",
    SuggestedFileName = "cxy可爱捏",
};
savePicker.FileTypeChoices.Add("the format we can read", 
                               new[] { ".txt", ".md" });
StorageFile store = await savePicker.PickSaveFileAsync();
if (store != null)
{
    try
    {
        await FileIO.WriteTextAsync(store, "114514");
    }
    catch (Exception ex)
    {
        await new MessageDialog(ex.ToString()).ShowAsync();
    }
}

Android 学习

上一次学习 UWP 还是在去年的时候,今年大三就开始学习移动应用开发了,令人唏嘘。

1. 结构目录

1.1 app

  • manifests:只有一个 XML 文件,AndroidManifest.xml,是 App 的运行配置文件;
  • java:第一个包用于存放 Java 源码,其他两个用于存放测试代码;
  • res:存放模块资源文件:
    • drawable:存放图形描述文件与图片文件;
    • layout:存放 App 页面的布局文件;
    • mipmap:存放 App 的启动图标;
    • values:存放一些常量定义文件,例如字符串常量 strings.xml、像素常量 dimens.xml、颜色常量 colors.xml、样式风格定义 styles.xml 等。

1.2 Gradle Scripts

  • build.gradle:该文件分为项目级与模块级两种,用于描述 App 工程的编译规则;
  • proguard-rules.pro:该文件用于描述 Java 代码的混淆规则(将 apk 打包中的代码进行混淆替换,防止逆向解包);
  • gradle.properties:该文件用于配置编译工程的命令行参数,一般无须改动;
  • settings.gradle:该文件配置了需要编译哪些模块。初始内容为 include ‘:app’,表示只编译 app 模块;
  • local.properties:项目的本地配置文件,它在工程编译时自动生成,用于描述开发者电脑的环境配置,包括 SDK 的本地路径 .NDK 的本地路径等。

1.3 AndroidManifest.xml

可见 AndroidManifest.xml 的根节点为 manifest,它的 package 属性指定了该 App 的包名。manifest 下面有个 application 节点,它的各属性说明如下:

  • android:allowBackup:是否允许应用备份。允许用户备份系统应用和第三方应用的apk安装包和应用数据,以便在刷机或者数据丢失后恢复应用,用户即可通过 adb backup 和 adb restore 来进行对应用数据的备份和恢复。为 true 表示允许,为 false 则表示不允许。;
  • android:icon,指定 App 在手机屏幕上显示的图标;
  • android:label,指定 App 在手机屏幕上显示的名称;
  • android:roundlcon,指定 App 的圆角图标;
  • android:supportsRtl:是否支持阿拉伯语/波斯语这种从右往左的文字排列顺序。为 true 表示支持,为 false 则表示不支持;
  • android:theme,指定 App 的显示风格(Google 为 Android 设计了 material design)。

1.4 Activity

Activity 是一个应用程序组件,提供一个屏幕,供用户进行交互并完成某项任务。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!--命名空间,区分不同的组件-->

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FirstActivity"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <!--最开始启动的 Activity-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

在 Android 开发中,使用 XML 描述 APP 见面,Java/kotlin 处理内部逻辑(类比 UWP 中的 XAML 和 C#)。

2. activity_main.xml

2.1 点击事件

高版本的 Android 并不支持直接在 xml 中配置 onClick 属性。除了性能低之外,还考虑到页面与逻辑分离的问题。

// 不推荐使用,效率不高。每次 Click 都会使用 new 新建一个 intent
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    findViewById(R.id.Button03).setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Intent intent = new Intent(mainActivity.this, fristActivity.class);
            intent.putExtra("data", "mainActivity");
            startActivity(intent);			
        }		
    });
}

// 推荐使用,同时解决同一个 activity 中所有的 onClick 问题
public class mainActivity extends Activity implements OnClickListener {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        findViewById(R.id.Button02).setOnClickListener(this);
        findViewById(R.id.Button03).setOnClickListener(this);
    }
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.Button03:
                Intent intent = new Intent(mainActivity.this, fristActivity.class);
                intent.putExtra("data", "mainActivity");
                startActivity(intent);	
                break;			
            case R.id.Button02:
                Intent intent = new Intent(mainActivity.this, loginActivity.class);
                intent.putExtra("data", "mainActivity");
                startActivity(intent);	
                break;	
            default:
                break;
        }
    }
    ……
}

而在版本更新之后,switch-case 又不推荐使用。因为 id 为非 final 值,谷歌官方推荐使用 if-else 替代,并且宣称性能几乎没有损失。

@Override
public void onClick(View v) {
    int id = v.getId();
    if (id == button_count) {
        mCount++;
        if (showCount != null) {
            showCount.setText(String.format(Locale.getDefault(), "%d", mCount));
        }
    } else if (id == button_zero) {
        mCount = 0;
        showCount.setText(String.format(Locale.getDefault(), "%d", mCount));
    } else if (id == button_toast) {
        Toast.makeText(MainActivity.this, R.string.toast_message, Toast.LENGTH_SHORT).show();
    }
}

2.2 属性

对于文本的大小,有 px(像素大小)、dpi(像素密度,每英寸距离中含有的像素点数量)、Density(密度,每平方英寸中含有的像素数量)、dp(同一个单位在不同设备上有不同的显示效果,px = dp * dpi / 160)。

对于控件的长宽有三种选项:

  • 自定义大小单位;
  • wrap_content:根据控件中的内容自动设置长宽;
  • match_parent:跟上一级视图一致(控件会获取 Layout 的大小,Layout 如果依然使用 match_parent,那就是整个屏幕的长宽)。

(R.Java 是 Android Studio 自动生成的一个特殊 Java 类,用于跟踪 res 目录下所有资源。除了 R.java 之外,还有很多辅助的功能类)。

2.2.1 间距

  • android:layout_margin:表示视图和外部的距离;

  • android:padding:表示试图内部的距离。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="300dp"
    android:background="#00AAFF"
    tools:context=".MainActivity"
    android:orientation="vertical">
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:background="#FFFF99"
        android:padding="60dp">

        <View
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#FF0000"
    </LinearLayout>
        
</LinearLayout>

margin 20dp padding 60dp

2.2.2 内容

对于 TextView 无法直接将 int 转为 String 的 warning 问题。推荐使用 String.format,具体代码如下:

textView.setText(String.format("%d", getIntent().getExtras().getInt("level"))));

2.3 布局

在学习 UWP 时我们已经学习了一部分跟布局有关的内容,Android 因为有 Google 提供的 material。所以在原生开发中会有更多复杂且功能齐全的控件/布局。

2.3.1 通用布局

LinearLayout

LinearLayout,按照直译为线性布局。通过 Orientation 指定水平布局或垂直布局。内部视图之间的排列顺序是固定的,要么从左到右排列,要么从上到下排列。在 XML 文件中,LinearLayout 通过属性 android:orientation 区分两种方向,其中从左到右排列叫作水平方向,属性值为 horizontal;从上到下排列叫作垂直方向,属性值为 vertical。如果 LinearLayout 标签不指定具体方向,则系统默认该布局为水平方向排列。

RelativeLayout

RelativeLayout 表示相对布局,可以让控件指定放置在父控件的不同位置。

Relative 属性

GridLayout

通过 Grid 通过 weight 指定内部组件的占比,用法类似于 UWP 的 GridLayout。

weight 指定对应占比排列控件。

2.3.2 特有布局

ConstraintLayout

ConstraintLayout 主要针对于 Android 视图进行“拖拽”布局。ConstraintLayout 会将一个控件约束在几个范围内。ConstraintLayout可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
Behavior

在说明下面的 Coordinatorlayout(协调布局) 之前,需要先说明 Behavior。

Behavior 是一系列回调。让开发者有机会以非侵入的为 View 添加动态的依赖布局,和处理父布局(CoordinatorLayout)滑动手势的机会。如果我们想实现控件之间任意的交互效果,完全可以通过自定义 Behavior 的方式达到。

官方有很多内置的 Behavior

以 BottomSheetBehavior 为例,在设置好对应参数之后,调用 behavior 仅需要一行代码。

弹出效果

<RelativeLayout
    android:id="@+id/design_bottom_sheet"
    android:layout_width="match_parent"
    android:layout_gravity="center"
    android:layout_height="match_parent"
    android:background="@color/colorAccent"
    app:behavior_hideable="true"
    app:behavior_peekHeight="100dp"
    app:elevation="4dp"
    app:layout_behavior="@string/bottom_sheet_behavior">
    <!--app:behavior_peekHeight="10dp" 折叠高度
		app:behavior_hideable="true" 是否可以隐藏
		app:behavior_skipCollapsed="true" 是否跳过折叠状态-->
</RelativeLayout>
// 将 behavior 设置为按钮触发
bottomSheetBehavior = BottomSheetBehavior.from((View)rlBottom);

// 处理按钮触发事件
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
    bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
} else {
    bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
  • STATE_COLLAPSED:关闭 Bottom Sheets,显示 peekHeight 的高度,默认是0;
  • STATE_DRAGGING:用户拖拽 Bottom Sheets 时的状态;
  • STATE_SETTLING:当 Bottom Sheets view 释放时记录的状态;
  • STATE_EXPANDED:当 Bottom Sheets 展开的状态;
  • STATE_HIDDEN:当 Bottom Sheets 隐藏的状态。

bottomSheetBehavior.setBottomSheetCallback 中,可以在回调的时候进行自定义操作。

Coordinatorlayout

官方文档的第一句话就非常醒目:CoordinatorLayout is a super-powered FrameLayout,非常直白,CoordinatorLayout 继承于 ViewGroup,它就是一个超级强大 Framelayout。说白了就是可以通过 Behavior 协调子 View 。

Material Design 里面的 CoordinatorLayout 是一个非常强大的控件,它接管了 child 组件之间的交互。让你滑动交互使用更加方便简单,效果也更加强大,不需要像以前那样自己处理一坨什么乱七八槽的滑动,事件传递之类的处理了。

CoordinatorLayout 作为一个 “super-powered FrameLayout”,主要有以下两个作用:

  1. 作为顶层布局;
  2. 作为协调子 View 之间交互的容器。

使用 CoordinatorLayout 我们就可以配合 AppBarLayout 以及一系列其他的组件使用:

<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
		<!--AppBarLayout 表示 App 中最顶层的布局-->
        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

CoordinatorLayout 默认布局为 vertical。

DrawerLayout

DrawerLayout 是谷歌官方实现的方便使用的侧边栏效果的类,也就是常见的“抽屉”效果。

<androidx.drawerlayout.widget.DrawerLayout 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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity"
    tools:openDrawer="start">
    <!--openDrawer 指定打开的位置-->
    <!--侧边栏整体通过 NavigationView 进行实现-->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/header_main"
        app:menu="@menu/drawer" />
</androidx.drawerlayout.widget.DrawerLayout>

NavigationView 在属性中,又分为 header 和 menu 部分。

<menu xmlns:android="http://schemas.android.com/apk/res/android">
	<!--使用多个 item 包裹来实现多重标题的效果-->
    <item android:title="@string/catalogue">
        <menu>
            <group android:checkableBehavior="single">
                <item
                    android:id="@+id/dinner"
                    android:icon="@drawable/ic_dinner"
                    android:title="@string/dinner" />
                <item
                    android:id="@+id/drink"
                    android:icon="@drawable/ic_drink"
                    android:title="@string/drink" />
                <item
                    android:id="@+id/fast_food"
                    android:icon="@drawable/ic_fast_food"
                    android:title="@string/fastfood" />
                <item
                    android:id="@+id/desert"
                    android:icon="@drawable/ic_desert"
                    android:title="@string/desert" />
            </group>
        </menu>
    </item>

    <item android:title="@string/personal_data">
        <menu>
            <group android:checkableBehavior="single">
                <item
                    android:id="@+id/data"
                    android:icon="@drawable/ic_personal_data"
                    android:title="@string/info" />
                <item
                    android:id="@+id/history_order"
                    android:icon="@drawable/ic_order"
                    android:title="@string/order" />
                <item
                    android:id="@+id/share"
                    android:icon="@drawable/ic_share"
                    android:title="@string/share" />
            </group>
        </menu>
    </item>
</menu>

整体布局如下

3. activity

activity 通过 startActivity 进行启动,跳转至下一个界面。

返回时表示跳转的结束,使用 finish() 结束。

3.1 activity 生命周期

activity 生命周期

  • onCreate:当 Activity 创建实例完成,并调用 attach 方法赋值 PhoneWindow、ContextImpl 等属性之后,调用此方法。该方法在整个 Activity 生命周期内只会调用一次。调用该方法后 Activity 进入 ON_CREATE 状态;

    需要在这个方法中初始化基础组件和视图。如 viewmodel,textview。同时必须在该方法中调用 setContentView 来给 activity 设置布局

  • onStart:当 Activity 准备进入前台时会调用此方法。调用后 Activity 会进入 ON_START 状态;

    前台并不意味着 Activity 可见,只是表示 activity 处于活跃状态。前台 activity 一般只有一个,所以这也意味着其他的 activity 都进入后台了

  • onResume:当 Activity 准备与用户交互的时候调用。调用之后 Activity 进入 ON_RESUME 状态;

    这个方法一直被认为是 activity 一定可见,且准备好与用户交互的状态。但事实并不一直是这样。如果在 onReume 方法中弹出 popupWindow 你会收获一个异常:token is null,表示界面尚没有被添加到屏幕上。

    但是,这种情况只出现在第一次启动 activity 的时候。当 activity 启动后 decorview 就已经拥有 token 了,再次在 onReume 方法中弹出 popupWindow 就不会出现问题了。

    因此,在 onResume 调用的时候 activity 是否可见要区分是否是第一次创建 activity

    onStart 方法是后台与前台的区分,而这个方法是是否可交互的区分。使用场景最多是在当弹出别的 activity 的窗口时,原 activity 就会进入 ON_PAUSE 状态,但是仍然可见;当再次回到原 activity 的时候,就会回调 onResume 方法了。

  • onPause:当前 activity 窗口失去焦点的时候,会调用此方法。调用后 activity 进入 ON_PAUSE 状态,并进入后台;

    这个方法一般在另一个 activity 要进入前台前被调用。只有当前 activity 进入后台,其他的 activity 才能进入前台。所以,该方法不能做重量级的操作,不然则会引用界面切换卡顿。一般的使用场景为界面进入后台时的轻量级资源释放。

    最好理解这个状态就是弹出另一个 activity 的窗口的时候。因为前台 activity 只能有一个,所以当前可交互的 activity 变成另一个 activity 后,原 activity 就必须调用 onPause 方法进入 ON_PAUSE 状态;但是!!仍然是可见的,只是无法进行交互。

    这里也可以更好地体会前台可交互与可见性的区别。

  • onStop:当 activity 不可见的时候进行调用。调用后 activity 进入 ON_STOP 状态;

    这里的不可见是严谨意义上的不可见。

    当 activity 不可交互时会回调 onPause 方法并进入 ON_PAUSE 状态。但如果进入的是另一个全屏的 activity 而不是小窗口,那么当新的 activity 界面显示出来的时候,原 Activity 才会进入 ON_STOP 状态,并回调 onStop 方法。同时,activity 第一创建的时候,界面是在 onResume 方法之后才显示出来,所以 onStop 方法会在新 activity 的 onResume 方法回调之后再被回调。

    注意,被启动的 activity 并不会等待 onStop 执行完毕之后再显示。因而如果 onStop 方法里做一些比较耗时的操作也不会导致被启动的 activity 启动延迟。

    onStop 方法的目的就是做资源释放操作。因为是在另一个 activity 显示之后再被回调,所以这里可以做一些相对重量级的资源释放操作,如中断网络请求、断开数据库连接、释放相机资源等。

    如果一个应用的全部 activity 都处于 ON_STOP 状态,那么这个应用是很有可能被系统杀死的。而如果一个 ON_STOP 状态的 activity 被系统回收的话,系统会保留该 activity 中 view 的相关信息到 bundle 中,下一次恢复的时候,可以在 onCreate 或者 onRestoreInstanceState 中进行恢复。

  • onRestart :当从另一个activity切回到该activity的时候会调用。调用该方法后会立即调用onStart方法,之后activity进入ON_START状态;

    这个方法一般在 activity 从 ON_STOP 状态被重新启动的时候会调用。执行该方法后会立即执行 onStart 方法,然后 Activity 进入 ON_START 状态,进入前台。

  • onDestroy:当 activity 被系统杀死或者调用 finish 方法之后,会回调该方法。调用该方法之后 activity 进入 ON_DESTROY 状态。

    这个方法是 activity 在被销毁前回调的最后一个方法。我们需要在这个方法中释放所有的资源,防止造成内存泄漏问题。

    回调该方法后的 activity 就等待被系统回收了。如果再次打开该 activity 需要从 onCreate 开始执行,重新创建 activity。

3.2 Intent

Intent 表示意图,是各个组件之间信息沟通的桥梁,既能在 Activity 之间沟通,又能在 Activity 与 Service 之间沟通,也能在 Activity 与 Broadcast 之间沟通。总而言之,Intent 用于 Android 各组件之间的通信。

3.2.1 显式 Intent & 隐式 Intent

Intent 主要完成下列 3 部分工作:

  1. 标明本次通信请求从哪里来、到哪里去 、要怎么走;
  2. 发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据;
  3. 发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。

Intent 内部元素

显式 Intent

显式 Intent,直接指定来源活动与目标活动,属于精确匹配在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源页面,即“来源 Activity.this”;第二个参数表示待跳转的页面,即“目标 Activity.class”。

Intent intent = new Intent(this, ActNextActivity.class);  // 创建一个目标确定的意图

Intent intent = new Intent();  // 创建一个新意图
intent.setClass(this, ActNextActivity.class); // 设置意图要跳转的目标活动

Intent intent = new Intent();  // 创建一个新意图 
// 创建包含目标活动在内的组件名称对象
ComponentName component = new ComponentName(this, ActNextActivity.class); 
intent.setComponent(component);  // 设置意图携带的组件信息
隐式 Intent

隐式 Intent,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配通常 App 不希望向外部暴露活动名称,只给出一个事先定义好的标记串,这样大家约定俗成、按图索骥就好,隐式 Intent 便起到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已有的系统动作。

动作常量名

String phoneNo = "12345";
Intent intent = new Intent();  // 创建一个新意图
intent.setAction(Intent.ACTION_DIAL);  // 设置意图动作为准备拨号 
Uri uri = Uri.parse("tel:" + phoneNo);  // 声明一个拨号的 Uri 
intent.setData(uri);  // 设置意图前往的路径
startActivity(intent);  // 启动意图通往的活动页面
/* 隐式 Intent 还用到了过滤器的概念,把不符合匹配条件的过滤掉,剩下符合条件的按照优先顺序调用。
譬如创建一个 App 模块,AndroidManifest.xml 里的 intent-filter 就是配置文件中的过滤器。
像最常见的首页活动 MainAcitivity,它的 activity 节点下面便设置了 action 和 category 的过滤条件。
其中 android.intent.action.MAIN 表示 App 的入口动作,而 android.intent.category.LAUNCHER 表示在桌面。*/

3.2.2 发送数据

Intent 对象的 setData 方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在 Extras 中。Intent 重载了很多种 putExtra 方法传递各种类型的参数,包括整型、双精度型、字符串等基本数据类型,甚至 Serializable 这样的序列化结构。只是调用 putExtra 方法显然不好管理,像送快递一样大小包裹随便扔,不但找起来不方便,丢了也难以知道。所以 Android 引入了 Bundle 概念,可以把 Bundle 理解为超市的寄包柜或快递收件柜,大小包裹由 Bundle 统一存取,方便又安全。

Bundle 内部用于存放消息的数据结构是 Map 映射,既可添加或删除元素,还可判断元素是否存在。开发者若要把 Bundle 数据全部打包好,只需调用一次意图对象的 putExtras 方法;若要把 Bundle 数据全部取出来,也只需调用一次意图对象的 getExtras 方法。

// 包装数据进行发送
// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this,ActReceiveActivity.class); 
Bundle bundle = new Bundle();  // 创建一个新包裹
// 往包裹存入名为 request_time 的字符串
bundle.putString("request_time", DateUtil.getNowTime()); 
// 往包裹存入名为 request_content 的字符串
bundle.putString("request_content", tv_send.getText().toString()); 
intent.putExtras(bundle);  // 把快递包裹塞给意图
startActivity(intent);  // 跳转到意图指定的活动页面

// 获取发送的数据
// 从布局文件中获取名为 tv_receive 的文本视图
TextView tv_receive = findViewById(R.id.tv_receive); 
// 从上一个页面传来的意图中获取快递包裹
Bundle bundle = getIntent().getExtras(); 
// 从包裹中取出名为 request_time 的字符串
String request_time = bundle.getString("request_time"); 
// 从包裹中取出名为 request_content 的字符串
String request_content = bundle.getString("request_content"); 
String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s", 
                           request_time, request_content);
tv_receive.setText(desc);  // 把请求消息的详情显示在文本视图上

3.2.3 返回数据

// 返回对应数据
String response = "我吃过了,还是你来我家吃"; 
Intent intent = new Intent();  // 创建一个新意图 
Bundle bundle = new Bundle();  // 创建一个新包裹 
// 往包裹存入名为response_time的字符串
bundle.putString("response_time", DateUtil.getNowTime()); 
// 往包裹存入名为response_content的字符串
bundle.putString("response_content", response); 
intent.putExtras(bundle);  // 把快递包裹塞给意图 
// 携带意图返回上一个页面。RESULT_OK表示处理成功 
setResult(Activity.RESULT_OK, intent);
finish();  // 结束当前的活动页面


// 从下一个页面携带参数返回当前页面时触发。其中 requestCode 为请求代码, 
// resultCode 为结果代码,intent 为下一个页面返回的意图对象
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 
    // 接收返回数据
    super.onActivityResult(requestCode, resultCode, intent);
    // 意图非空,且请求代码为之前传的 0,结果代码也为成功
    if (intent!=null && requestCode==0 && resultCode== Activity.RESULT_OK) {
        Bundle bundle = intent.getExtras(); // 从返回的意图中获取快递包裹
        // 从包裹中取出名叫 response_time 的字符串
        String response_time = bundle.getString("response_time");
        // 从包裹中取出名叫 response_content 的字符串
        String response_content = bundle.getString("response_content");
        String desc = String.format("收到返回消息:\n应答时间为:%s\n应答内容为:%s",
                                    response_time, response_content);
        tv_response.setText(desc); // 把返回消息的详情显示在文本视图上
    } 
}

而对于高版本的 Android API,onActivityResult 方法已经不推荐使用。现在一般直接使用 registerForActivityResult(),作为 startActivityForResult() 的替代,简化了数据回调的写法。

// Java 写法
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
    @Override
    public void onActivityResult(ActivityResult result) {
        Intent data = result.getData();
        int resultCode = result.getResultCode();
    }
}).launch(new Intent(context,BActivity.class));

// launch() 方法,输入 Intent, ActivityResultCallback: 获取返回的数据,
// ActivityResultContracts.StartActivityForResult 是官方提供用来处理回调数据的 ActivityResultContract 类
// 跳转到 BActivity 后,调用 setResult() 方法传递数据,这部分和以前一样
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static final String EXTRA_MESSAGE = "com.example.intentactivity.extra.MESSAGE";

    private EditText mMessageEditText;

    private TextView mReplyHeadTextView;

    private TextView mReplyTextView;

    private ActivityResultLauncher<Intent> intentActivityResultLauncher;

    private static final String LOG_TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mMessageEditText = findViewById(R.id.editText_main);
        mReplyHeadTextView = findViewById(R.id.text_header_reply);
        mReplyTextView = findViewById(R.id.text_message_reply);

        findViewById(R.id.button_send).setOnClickListener(this);

        // 在 onCreate 时注册相应内容
        intentActivityResultLauncher = registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(), result -> {
            Intent data = result.getData();
            int resultCode = result.getResultCode();
            if (data != null && resultCode == RESULT_OK) {
                Bundle replyBundle = data.getExtras();
                String reply = replyBundle.getString(SecondActivity.EXTRA_REPLY);
                mReplyTextView.setText(reply);
                mReplyHeadTextView.setVisibility(View.VISIBLE);
                mReplyTextView.setVisibility(View.VISIBLE);
            }
        });

        if (savedInstanceState != null) {
            boolean isVisible = savedInstanceState.getBoolean("reply_visible");
            if (isVisible) {
                mReplyHeadTextView.setVisibility(View.VISIBLE);
                mReplyTextView.setVisibility(View.VISIBLE);
                mReplyTextView.setText(savedInstanceState.getString("reply_text"));
            }
        }

        Log.d(LOG_TAG, "-------");
        Log.d(LOG_TAG, "onCreate");

        Log.d(LOG_TAG, "Button clicked!");
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, SecondActivity.class);
        String message = mMessageEditText.getText().toString();
        Bundle bundle = new Bundle();
        bundle.putString(EXTRA_MESSAGE, message);
        intent.putExtras(bundle);
        // 在 onClik 触发时启动
        intentActivityResultLauncher.launch(intent);
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mReplyHeadTextView.getVisibility() == View.VISIBLE) {
            outState.putBoolean("reply_visible", true);
            outState.putString("reply_text", mReplyTextView.getText().toString());
        }
    }
}

3.3 Adapter

Adapter 基于 MVC 思想中的 Controller。

Adaper 继承结构图

  • BaseAdapter:抽象类,实际开发中我们会继承这个类并且重写相关方法,用得最多的一个 Adapter;
  • ArrayAdapter:支持泛型操作,最简单的一个 Adapter,只能展现一行文字;
  • SimpleAdapter:同样具有良好扩展性的一个 Adapter,可以自定义多种效果。
  • SimpleCursorAdapter:用于显示简单文本类型的 listView,一般在数据库那里会用到,不过有点过时, 不推荐使用。

之后可以对 layout 设置绑定 adapter。

4. 高级控件

在正篇开始前,使用 Android studio 的 emulator 时,有时会出现 Waiting for Target Device to Come Online 的问题。可能是因为模拟机的一些文件没有及时更新,导致无法使用。这毕竟不是真机,无法联网,所以我们需要重新一部分存在的缓存问题。

强制 cold boot 可以解决此类问题

4.1 Inflater

从官方的角度来说,inflater 的功能是将 xml 文件资源转化为 view。通过 inflater 可以配合 Fragment 进行“多模块”开发。

4.1.1 配合 menu 使用

创建 Android Resource Directory,指定为 menu 类别。

通过 menu_main 设定为 view

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu_main, menu);
    // 将 menu.xml 转化为 view
    return true;
}

4.1.2 自定义 view

给 Fragment 填充布局时,一般会有这样的代码:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, 
                         @Nullable ViewGroup container, 
                         @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.activity_main, container, false);
    return view;
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    // 设置 attachToRoot = true 时, 表示 xml 实例化的 View 会被添加到第二个参数 rootView 中
    ...
}

4.2 ViewHolder

4.2.1 Adapter

如果进行类比,那么 Adapter 就像是 MVC 中的 Controller 部分。

Adapter 整体继承链

4.2.2 RecyclerView

RecyclerView 标准化了 ViewHolder,而且异常的灵活,可以轻松实现 ListView 实现不了的样式和功能,通过布局管理 LayoutManager 可控制 Item 的布局方式,通过设置 Item 操作动画自定义 Item 添加和删除的动画,通过设置 Item 之间的间隔样式,自定义间隔。

RecyclerView 库会根据需要动态创建元素,可以轻松高效地显示大量数据。在自定义 ViewHolder 并继承 RecyclerView.Adapter 之后,就可以使用 RecyclerView:

class ShopCartAdapter(
    private val context: Context,
    private val productList: List<Product>,
    private val register: ActivityResultLauncher<Intent>
) : RecyclerView.Adapter<ShopCartAdapter.ShopCartViewHolder>() {
    private val mInflater: LayoutInflater = LayoutInflater.from(context)
    var mPosition = -1

    // 在 ViewHolder 中设置逻辑操作
    inner class ShopCartViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
        OnClickListener, OnLongClickListener {
        val mTitleView: TextView = itemView.findViewById(R.id.shop_cart_item)
        init {
            mTitleView.setOnClickListener(this)
            mTitleView.setOnLongClickListener(this)
        }
        override fun onClick(v: View?) {
            val position = layoutPosition
            val intent = Intent(context, MoreContent::class.java)
            val bundle = Bundle()
            bundle.putString("title", productList[position].name)
            bundle.putInt("description", productList[position].word)
            bundle.putInt("picture", productList[position].picture)
            bundle.putInt("flag", 1)
            intent.putExtras(bundle)
            register.launch(intent)
        }
        override fun onLongClick(v: View?): Boolean {
            mPosition = adapterPosition
            return false
        }
    }

    override fun onCreateViewHolder(
        parent: ViewGroup, viewType: Int
    ): ShopCartViewHolder {
        val mItemView = mInflater.inflate(R.layout.shop_cart_item, parent, false)
        return ShopCartViewHolder(mItemView)
    }

    // 使用规范的 placeholder 格式设置 TextView
    override fun onBindViewHolder(holder: ShopCartViewHolder, position: Int) {
        holder.mTitleView.text =
            context.getString(
                R.string.shop_cart_item,
                productList[position].name,
                productList[position].num
            )
    }

    override fun getItemCount(): Int {
        return productList.size
    }
}
// 创建上下文选择的 menu
override fun onCreateContextMenu(
    menu: ContextMenu?,
    v: View?,
    menuInfo: ContextMenu.ContextMenuInfo?
) {
    super.onCreateContextMenu(menu, v, menuInfo)
    menuInflater.inflate(R.menu.shop_cart, menu)
}

// 上下文选择的操作
override fun onContextItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.shop_cart_delete -> {
            val str = productList!![mAdapter!!.mPosition].name
            productDao!!.updateNum(str, 0)
            Toast.makeText(applicationContext, "${str}删除已经完成", Toast.LENGTH_SHORT).show()
            clearData()
            initData()
        }

        R.id.shop_cart_edit -> {
            val str = productList!![mAdapter!!.mPosition].name
            val builder = AlertDialog.Builder(this)
            val numbers = arrayOf("1份", "2份", "3份", "4份", "5份")
            builder.setTitle("请选择你需要的数量")
            builder.setSingleChoiceItems(numbers, 0) { _, i ->
                                                      selectId = i
                                                      Toast.makeText(
                                                          applicationContext,
                                                          "你选择了${numbers[i]}${str}",
                                                          Toast.LENGTH_SHORT
                                                      ).show()
                                                     }
            builder.setPositiveButton("确定") { _, _ ->
                                             productDao!!.updateNum(str, selectId + 1)
                                             Toast.makeText(applicationContext, "修改完成", Toast.LENGTH_SHORT).show()
                                             clearData()
                                             initData()
                                            }
            builder.create().show()
        }
    }
    return super.onContextItemSelected(item)
}

最后使用 registerForContextMenu(mRecyclerView) 为对应控件创建 ContextMenu。

4.3 AppBarLayout

Android 的历史进程中,大概有 TitleBar、ActionBar、Toolbar 的进化,这是 Android 设计语言的改良过程。而后来随着 Material Design 设计的出现,它又提供了 AppBar 的概念,而 AppBarLayout 则是 AppBar 在 Android 中的代码实现。

AppBarLayout 需要和一个独立的兄弟 View 配合使用,这个兄弟 View 是一个嵌套滑动组件,只有这样 AppBarLayout 才能知道什么时候开始滑动。它们之间关系的绑定通过给嵌套滑动的组件设立特定的 Behavior那就是 AppBarLayout.ScrollingViewBehavior。

AppbarLayout 要实现酷炫的滑动效果必须依赖于 CoordinatorLayout 使用,作为 CoordinatorLayout 的直接子 view,如果父view是其他的 viewGroup 是没有效果的。

标题栏的进化

通过这一些强大的控件,开发者可以实现很多官方定义或者自定义动画效果。

4.3.1 ToolBar 使用

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            app:title="Order Anything"
            app:navigationIcon="@drawable/ic_drawer_setting"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

    </com.google.android.material.appbar.AppBarLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

4.3.2 menu & 跳转

在 Android Studio 中,通过创建 menu 类型资源,可以让我们添加 Android 中的资源。

menu 定义了一系列规范排列的 item,可以用于 appbar 中的 menu,或者是长按显示的选项。

本身创建 empty activity 时就有几个默认的资源

<?xml version="1.0" encoding="utf-8"?>
<menu 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"
    tools:context="com.example.orderplatform.MainActivity">
    <item
        android:id="@+id/action_shop"
        android:icon="@drawable/ic_shop_cart"
        android:orderInCategory="10"
        android:title="@string/shop_cart"
        app:showAsAction="always" />
    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search"
        android:orderInCategory="20"
        android:title="@string/search"
        app:showAsAction="ifRoom" />
    <item
        android:id="@+id/action_setting"
        android:icon="@drawable/ic_setting"
        android:orderInCategory="30"
        android:title="@string/settings"
        app:showAsAction="never" />
    <item
        android:id="@+id/todo"
        android:orderInCategory="40"
        android:title="@string/todo"
        app:showAsAction="never" />
</menu>
  • always:item 会一直显示;
  • ifRoom:如果有空间,item 才会显示;
  • never:item 永远不会显示。

Android 中有自带的关于顶部 option menu 的方法。除此之外,如果进入源码中可以看到,内部自带一个 mNavButtonView,类型是 ImageButton,作为每个 ToolBar 左边的跳转按钮。

val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
// 设置 navigation 必须在设置 SupportActionBar 之后,不然会报错
// 这用于设定按钮的跳转
toolbar.setNavigationOnClickListener {
    drawer?.openDrawer(GravityCompat.START)
}

// 通过 menuInflater 将创建的 menu 封装为 view
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.main_app_bar, menu)
    return super.onCreateOptionsMenu(menu)
}

// 设置 AppBar 中的切换->针对于 menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.action_setting -> {
            drawer?.openDrawer(GravityCompat.START)
        }

        R.id.action_search -> {
            val intent = Intent(this, Search::class.java)
            startActivity(intent)
        }

        R.id.action_shop -> {
            val intent = Intent(this, ShopCart::class.java)
            startActivity(intent)
        }
    }
    return super.onOptionsItemSelected(item)
}

左边跳转的 Button,右边为一些 Menu Options

4.3.3 可能错误

注意,这里有个非常关键的错误This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.。这个是在 ToolBar 替换 ActionBar 之前,主界面没有设置对应主题导致的。我们需要提前设置一个 NoActionBar 的主题。因为在默认设置中,会对原来的 ActionBar 进行加载。

在每个 activity 创建时,都会有一个默认的 ActionBar,如果没有对这个 ActionBar 进行替换,屏幕上会显示一个 ActionBar 和一个 ToolBar。

官方注释表明不能让应用调用 ActionBar

在 Manifest 中需要设置 NoActionBar

java.lang.Object
   ↳	android.view.View
 	   ↳	android.view.ViewGroup
 	 	   ↳	android.widget.LinearLayout
 	 	 	   ↳	com.google.android.material.appbar.AppBarLayout
继承自 LinearLayout

4.4 ViewPager

ViewPager 是一个简单的页面切换组件,可以往里面填充多个 view,通过左右滑动进行切换。需要一个 Adapter (适配器)将 View 和 ViewPager 进行绑定。Google 官方是建议使用 Fragment 来填充 ViewPager ,这样可以更加方便的生成每个 Page,以及管理每个 Page 的生命周期。

ViewPager 是 android 扩展包 v4 包中的类,这个类可以让用户左右切换当前的 view。ViewPager 类直接继承了 ViewGroup 类,是一个容器类,可以在其中添加其他的 view 类。ViewPager 类需要一个 PagerAdapter 适配器类给它提供数据。ViewPager 经常和 Fragment 一起使用,并且提供了专门的 FragmentPagerAdapter 和 FragmentStatePagerAdapter 类供 Fragment 中的ViewPager 使用。

4.4.1 Fragment

Fragment 表示应用界面中可重复使用的一部分。Fragment 定义和管理自己的布局,具有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独立存在,而是必须由 Activity 或另一个 Fragment 托管。Fragment 的视图层次结构会成为宿主的视图层次结构的一部分,或附加到宿主的视图层次结构。

同一屏幕的采用不同屏幕尺寸的两个版本。

Activity 是一个相对来说很重的概念,如果进行页面跳转需要执行多个生命周期的变化,整体开销会变得更大。在某些情况进行页面切换时,我们可以考虑使用更轻的 Fragment。

4.4.2 ViewPager2

在 Android 更新之后,ViewPager 已经升级到了 v2 版本。

基于 RecyclerView 实现。这意味着 RecyclerView 的优点将会被 ViewPager2 所继承。ViewPager2 与 ViewPager 同是继承自 ViewGrop,但是 ViewPager2 被声明成了 final。意味着我们不可能再像 ViewPager 一样通过继承来修改 ViewPager2 的代码;

支持竖直滑动,只需要一个参数就可以改变滑动方向。支持关闭用户输入。通过setUserInputEnabled来设置是否禁止用户滑动页面。支持通过编程方式滚动。通过fakeDragBy(offsetPx)代码模拟用户滑动页面;

CompositePageTransformer 支持同时添加多个 PageTransformer。支持 DiffUtil ,可以添加数据集合改变的 item 动画。支持RTL (right-to-left) 布局;

FragmentStatePagerAdapter 被 FragmentStateAdapter 替代,FragmentStatePagerAdapter 已经被标为 deprecated。PagerAdapter 被 RecyclerView.Adapter 替代。

FragmentStateAdapter 实现多 Fragment 切换。在 ViewPager2 中,实现了很多默认的动画效果(包括设置滑动等等),方便开发者进行配置。

当想要监听页面变化时需要重写这三个方法。而 ViewPager2的registerOnPageChangeCallback 方法接收的是一个叫OnPageChangeCallback 的抽象类,因此我们可以选择性的重写需要的方法即可。

class PagerAdapter(
    fragmentManager: FragmentManager,
    lifecycle: Lifecycle,
    private val itemsCount: Int
) :
FragmentStateAdapter(fragmentManager, lifecycle) {

    override fun getItemCount(): Int {
        return itemsCount
    }
    // 所有的 Fragment 会在创建时统一生成好
    // 同时,绑定点击时的切换操作
    override fun createFragment(position: Int): Fragment {
        when (position) {
            0, 1, 2, 3 -> {
                val bundle = Bundle()
                val productFragment = ProductFragment()
                bundle.putInt("kind", position)
                productFragment.arguments = bundle
                return productFragment
            }
            4 -> {
                return InfoFragment()
            }
            5 -> {
                return OrderFragment()
            }
        }
        return InfoFragment()
    }
}
// 配置 NavigationView 进行多 Fragment 切换
// 配合 ViewPager2 进行切换
override fun onNavigationItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.dinner -> {
            mViewPager?.currentItem = 0
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
        R.id.drink -> {
            mViewPager?.currentItem = 1
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
        R.id.fast_food -> {
            mViewPager?.currentItem = 2
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
        R.id.desert -> {
            mViewPager?.currentItem = 3
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
        R.id.data -> {
            mViewPager?.currentItem = 4
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
        R.id.history_order -> {
            mViewPager?.currentItem = 5
            drawer?.closeDrawer(GravityCompat.START)
            return true
        }
    }
    return false
}

在传递 Count 是需要注意的是,ViewPager2 是默认启动用户滑动,也就就是说用户可以通过滑动的方式切换 Fragment。但是为了流畅性,会提前加载一个页面相邻的页面,如果数组加载两边值一样,在规定时需要注意。以防出现资源浪费或者无法载入的情况。

5. 数据库操作

Android 中针对于数据库,更多是依赖于 SQLite(这个 SQLite 是跟 App 绑定的,也就是说,如果 App 删了这个数据库也会一并删除)。数据的操作通过继承 SQLiteOpenHelper 来实现。

class MDataBaseHelper(context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    // 单例模式进行初始化
    // 针对内存泄露问题 https://juejin.cn/post/6987258309648597005
    // 但是因为涉及多页面切换,所以放弃单例模式初始化
    companion object {
        const val PRODUCT_TABLE = "product"
        const val ORDER_TABLE = "buy"
        const val INFO_TABLE = "info"
        const val DB_NAME = "main.db"
        const val DB_VERSION = 1
        
        const val createProduct =
        "CREATE TABLE IF NOT EXISTS " + PRODUCT_TABLE +
        "(id integer NOT NULL PRIMARY KEY AUTOINCREMENT, " +
        "name varchar, " +
        "kind integer, " +
        "price integer, " +
        "word integer, " +
        "picture integer, " +
        "num INTEGER)"

        const val createORDER =
        "CREATE TABLE IF NOT EXISTS " + ORDER_TABLE +
        "(id integer NOT NULL PRIMARY KEY AUTOINCREMENT, " +
        "name varchar, " +
        "address varchar, " +
        "other varchar, " +
        "time varchar, " +
        "radio integer, " +
        "tool integer, " +
        "touch integer, " +
        "pay integer, " +
        "feedback varchar, " +
        "star double)"
        
        const val createInfo =
        "CREATE TABLE IF NOT EXISTS " + INFO_TABLE +
        "(id integer NOT NULL PRIMARY KEY AUTOINCREMENT, " +
        "address varchar, " +
        "radio integer, " +
        "normal integer)"
    }

	// 可以使用 execSQL 直接执行硬编码的 SQL 操作
    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(createProduct)
        db?.execSQL(createORDER)
        db?.execSQL(createInfo)
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    }
}

这里原本考虑内存重复使用的问题,考虑使用单例模式对整个数据库进行初始化。但是因为 Android 的不同页面对于 SQLite 的调度是不共通的(在参数中也能看出来,需要传入 Context)。如果不同页面调用同一个 Dao 文件,就会出现不同步的问题进而出现错误。

5.1 Dao 层

为了将数据库连接和 SQL 操作分开,依然是采取传统的架构方式进行分布,Dao 专门用于 SQL 操作的处理。

// 对 Product 表进行数据库操作
// 在执行 CRUD 操作的时候,需要手动开启关闭数据库的连接和读入/写入的连接
class ProductDao(private val mHelper: MDataBaseHelper) {
	// 插入操作
    fun insert(product: Product) {
        val values = ContentValues()
        values.put("name", product.name)
        values.put("kind", product.kind)
        values.put("price", product.price)
        values.put("word", product.word)
        values.put("picture", product.picture)
        values.put("num", product.num)
        // nullColumnHack 防止插入时出现 empty body 的情况。可以通过指定对应列插入 null
        // 因为 values 已经不为空,所以不需要指定,设定为 null 即可
        val db = mHelper.writableDatabase
        db.insert(MDataBaseHelper.PRODUCT_TABLE, null, values)
        db.close()
    }

    fun findAll(): List<Product> {
        val ret = mutableListOf<Product>()
        val db = mHelper.readableDatabase
        /**
         *
        query 中每个参数的作用
        table: 表名称
        columns: 列名称数组
        selection: 条件语句,相当于 where
        selectionArgs: 条件字句,参数数组
        groupBy: 分组列
        having: 分组条件
        orderBy: 排序列
        limit: 分页查询限制
        Cursor: 返回值,相当于结果集 ResultSet
         */
        val cursor = db.query(
            MDataBaseHelper.PRODUCT_TABLE,
            null,
            null,
            null,
            null,
            null,
            null,
            null
        )
        while (cursor.moveToNext()) {
            val product = Product(
                cursor.getInt(0),
                cursor.getString(1),
                cursor.getInt(2),
                cursor.getInt(3),
                cursor.getInt(4),
                cursor.getInt(5),
                cursor.getInt(6)
            )
            ret.add(product)
        }
        cursor.close()
        db.close()
        return ret
    }

    fun findProductByCatalogue(kind: Int): List<Product> {
        val ret = mutableListOf<Product>()
        val db = mHelper.readableDatabase
        // 指定参数中可以通过 string 数组进行指定
        val cursor = db.query(
            MDataBaseHelper.PRODUCT_TABLE,
            null,
            "kind=?",
            arrayOf(kind.toString()),
            null,
            null,
            null,
            null
        )
        while (cursor.moveToNext()) {
            val product = Product(
                cursor.getInt(0),
                cursor.getString(1),
                cursor.getInt(2),
                cursor.getInt(3),
                cursor.getInt(4),
                cursor.getInt(5),
                cursor.getInt(6)
            )
            ret.add(product)
        }
        cursor.close()
        db.close()
        return ret
    }

    fun searchProductByName(name: String): List<Product> {
        val ret = mutableListOf<Product>()
        val db = mHelper.readableDatabase
        // 因为模糊查询无法正确识别,所以使用这种方式
        val cursor = db.query(
            MDataBaseHelper.PRODUCT_TABLE,
            null,
            "name like '%$name%'",
            null,
            null,
            null,
            null,
            null
        )
        while (cursor.moveToNext()) {
            val product = Product(
                cursor.getInt(0),
                cursor.getString(1),
                cursor.getInt(2),
                cursor.getInt(3),
                cursor.getInt(4),
                cursor.getInt(5),
                cursor.getInt(6)
            )
            ret.add(product)
        }
        cursor.close()
        db.close()
        return ret
    }

    fun updateNum(name: String, num: Int): Int {
        val values = ContentValues()
        val db = mHelper.writableDatabase
        values.put("num", num)
        val value = db.update(
            MDataBaseHelper.PRODUCT_TABLE,
            values,
            "name=?",
            arrayOf(name)
        )
        db.close()
        return value
    }
}

用于保存 database query 结果的接口,需要开发者主动处理多线程问题

项目中整体架构如上

Kotlin 学习

在安卓的开发中,因为一直听说 kotlin 更适合进行开发,所以进行两周内从零开始的 kotlin 学习。

1. 基本类型

1.1 unit

参考文章

  1. 动脑学院 Android 课程
  2. 第三章:简单控件
  3. Number formatting does not take into account locale settings. Consider using String.format instead android studio
  4. button 的 OnClickListener 的三种实现方法
  5. Android 全面解析之 Activity 生命周期
  6. 第四章:活动 Activity
  7. registerForActivityResult()
  8. 解决 Android Studio 报的警告
  9. AppBarLayout | Android Developers
  10. 使用 CoordinatorLayout
  11. android - Waiting for Target Device to Come Online
  12. RecyclerView 使用完全指南(一)
  13. 探索 Android Inflater.inflate()
  14. ViewPager 全面剖析及使用详解
  15. 深入了解 ViewPager2
  16. 安卓 behavior 详解
  17. 细说 AppbarLayout,如何理解可折叠 Toolbar 的定制
  18. AppbarLayout 最详细使用说明

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录